diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index e28d8a814..000000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.env.example b/.env.example index c591e0b13..2e933f939 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -TEST_PRIVATE_KEY= +PRIVATE_KEY= ALCHEMY_KEY= diff --git a/.gas-snapshot b/.gas-snapshot new file mode 100644 index 000000000..59fb47f8b --- /dev/null +++ b/.gas-snapshot @@ -0,0 +1,61 @@ +AABenchmarkPrepare:test_prepareBenchmarkFile() (gas: 2926370) +AccountBenchmarkTest:test_state_accountReceivesNativeTokens() (gas: 11037) +AccountBenchmarkTest:test_state_addAndWithdrawDeposit() (gas: 83332) +AccountBenchmarkTest:test_state_contractMetadata() (gas: 56507) +AccountBenchmarkTest:test_state_createAccount_viaEntrypoint() (gas: 432040) +AccountBenchmarkTest:test_state_createAccount_viaFactory() (gas: 334122) +AccountBenchmarkTest:test_state_executeBatchTransaction() (gas: 39874) +AccountBenchmarkTest:test_state_executeBatchTransaction_viaAccountSigner() (gas: 392782) +AccountBenchmarkTest:test_state_executeBatchTransaction_viaEntrypoint() (gas: 82915) +AccountBenchmarkTest:test_state_executeTransaction() (gas: 35735) +AccountBenchmarkTest:test_state_executeTransaction_viaAccountSigner() (gas: 378632) +AccountBenchmarkTest:test_state_executeTransaction_viaEntrypoint() (gas: 75593) +AccountBenchmarkTest:test_state_receiveERC1155NFT() (gas: 39343) +AccountBenchmarkTest:test_state_receiveERC721NFT() (gas: 78624) +AccountBenchmarkTest:test_state_transferOutsNativeTokens() (gas: 81713) +AirdropERC1155BenchmarkTest:test_benchmark_airdropERC1155_airdrop() (gas: 38083572) +AirdropERC20BenchmarkTest:test_benchmark_airdropERC20_airdrop() (gas: 32068413) +AirdropERC721BenchmarkTest:test_benchmark_airdropERC721_airdrop() (gas: 41912536) +DropERC1155BenchmarkTest:test_benchmark_dropERC1155_claim() (gas: 185032) +DropERC1155BenchmarkTest:test_benchmark_dropERC1155_lazyMint() (gas: 123913) +DropERC1155BenchmarkTest:test_benchmark_dropERC1155_setClaimConditions_five_conditions() (gas: 492121) +DropERC20BenchmarkTest:test_benchmark_dropERC20_claim() (gas: 230505) +DropERC20BenchmarkTest:test_benchmark_dropERC20_setClaimConditions_five_conditions() (gas: 500858) +DropERC721BenchmarkTest:test_benchmark_dropERC721_claim_five_tokens() (gas: 210967) +DropERC721BenchmarkTest:test_benchmark_dropERC721_lazyMint() (gas: 124540) +DropERC721BenchmarkTest:test_benchmark_dropERC721_lazyMint_for_delayed_reveal() (gas: 226149) +DropERC721BenchmarkTest:test_benchmark_dropERC721_reveal() (gas: 13732) +DropERC721BenchmarkTest:test_benchmark_dropERC721_setClaimConditions_five_conditions() (gas: 500494) +EditionStakeBenchmarkTest:test_benchmark_editionStake_claimRewards() (gas: 65081) +EditionStakeBenchmarkTest:test_benchmark_editionStake_stake() (gas: 185144) +EditionStakeBenchmarkTest:test_benchmark_editionStake_withdraw() (gas: 46364) +MultiwrapBenchmarkTest:test_benchmark_multiwrap_unwrap() (gas: 88950) +MultiwrapBenchmarkTest:test_benchmark_multiwrap_wrap() (gas: 473462) +NFTStakeBenchmarkTest:test_benchmark_nftStake_claimRewards() (gas: 68287) +NFTStakeBenchmarkTest:test_benchmark_nftStake_stake_five_tokens() (gas: 539145) +NFTStakeBenchmarkTest:test_benchmark_nftStake_withdraw() (gas: 38076) +PackBenchmarkTest:test_benchmark_pack_addPackContents() (gas: 219188) +PackBenchmarkTest:test_benchmark_pack_createPack() (gas: 1412868) +PackBenchmarkTest:test_benchmark_pack_openPack() (gas: 141860) +PackVRFDirectBenchmarkTest:test_benchmark_packvrf_createPack() (gas: 1379604) +PackVRFDirectBenchmarkTest:test_benchmark_packvrf_openPack() (gas: 119953) +PackVRFDirectBenchmarkTest:test_benchmark_packvrf_openPackAndClaimRewards() (gas: 3621) +SignatureDropBenchmarkTest:test_benchmark_signatureDrop_claim_five_tokens() (gas: 140517) +SignatureDropBenchmarkTest:test_benchmark_signatureDrop_lazyMint() (gas: 124311) +SignatureDropBenchmarkTest:test_benchmark_signatureDrop_lazyMint_for_delayed_reveal() (gas: 225891) +SignatureDropBenchmarkTest:test_benchmark_signatureDrop_reveal() (gas: 10647) +SignatureDropBenchmarkTest:test_benchmark_signatureDrop_setClaimConditions() (gas: 73699) +TokenERC1155BenchmarkTest:test_benchmark_tokenERC1155_burn() (gas: 5728) +TokenERC1155BenchmarkTest:test_benchmark_tokenERC1155_mintTo() (gas: 122286) +TokenERC1155BenchmarkTest:test_benchmark_tokenERC1155_mintWithSignature_pay_with_ERC20() (gas: 267175) +TokenERC1155BenchmarkTest:test_benchmark_tokenERC1155_mintWithSignature_pay_with_native_token() (gas: 296172) +TokenERC20BenchmarkTest:test_benchmark_tokenERC20_mintTo() (gas: 118586) +TokenERC20BenchmarkTest:test_benchmark_tokenERC20_mintWithSignature_pay_with_ERC20() (gas: 183032) +TokenERC20BenchmarkTest:test_benchmark_tokenERC20_mintWithSignature_pay_with_native_token() (gas: 207694) +TokenERC721BenchmarkTest:test_benchmark_tokenERC721_burn() (gas: 8954) +TokenERC721BenchmarkTest:test_benchmark_tokenERC721_mintTo() (gas: 151552) +TokenERC721BenchmarkTest:test_benchmark_tokenERC721_mintWithSignature_pay_with_ERC20() (gas: 262344) +TokenERC721BenchmarkTest:test_benchmark_tokenERC721_mintWithSignature_pay_with_native_token() (gas: 286914) +TokenStakeBenchmarkTest:test_benchmark_tokenStake_claimRewards() (gas: 67554) +TokenStakeBenchmarkTest:test_benchmark_tokenStake_stake() (gas: 177180) +TokenStakeBenchmarkTest:test_benchmark_tokenStake_withdraw() (gas: 47396) \ No newline at end of file diff --git a/.github/composite-actions/setup/action.yml b/.github/composite-actions/setup/action.yml new file mode 100644 index 000000000..a080f4495 --- /dev/null +++ b/.github/composite-actions/setup/action.yml @@ -0,0 +1,22 @@ +name: "Install" +description: "Sets up Node.js and runs install" + +runs: + using: composite + steps: + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 18 + registry-url: "https://registry.npmjs.org" + cache: "yarn" + + - name: Install dependencies + shell: bash + run: yarn + + - name: Setup lcov + shell: bash + run: | + sudo apt update + sudo apt install -y lcov diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 99e1bf9c4..9546895ab 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,26 +6,32 @@ name: Solhint Lint on: # Triggers the workflow on push or pull request events but only for the main branch push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] + +# cancel previous runs if new commits are pushed to the branch +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" - build: + lint: # The type of runner that the job will run on runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: - - uses: actions/checkout@v2 + - name: Checkout repository + uses: actions/checkout@v3 with: submodules: recursive - - name: Setup Node.js environment - uses: actions/setup-node@v2.4.1 - # Runs a single command using the runners shell - - name: Run npm install - run: yarn - - name: Run lint + fetch-depth: 25 + + - name: Setup Project + uses: ./.github/composite-actions/setup + + - name: Run Lint run: yarn lint diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml index 548195ad3..6b45fffa2 100644 --- a/.github/workflows/prettier.yml +++ b/.github/workflows/prettier.yml @@ -6,26 +6,31 @@ name: Prettier Formatting on: # Triggers the workflow on push or pull request events but only for the main branch push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] + +# cancel previous runs if new commits are pushed to the branch +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true -# A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" - build: + lint: # The type of runner that the job will run on runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: - - uses: actions/checkout@v2 + - name: Checkout repository + uses: actions/checkout@v3 with: submodules: recursive - - name: Setup Node.js environment - uses: actions/setup-node@v2.4.1 - # Runs a single command using the runners shell - - name: Run npm install - run: yarn - - name: Run prettier:contracts + fetch-depth: 25 + + - name: Setup Project + uses: ./.github/composite-actions/setup + + - name: Run Prettier run: yarn prettier:contracts diff --git a/.github/workflows/slither.yml b/.github/workflows/slither.yml index 216160123..139eef493 100644 --- a/.github/workflows/slither.yml +++ b/.github/workflows/slither.yml @@ -2,9 +2,14 @@ name: Slither Analysis on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] + +# cancel previous runs if new commits are pushed to the branch +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: analyze: @@ -13,31 +18,30 @@ jobs: contents: read security-events: write steps: - - name: Checkout repository - uses: actions/checkout@v2 - with: - submodules: recursive - - - name: Setup Node.js environment - uses: actions/setup-node@v2.4.1 + - name: Checkout repository + uses: actions/checkout@v3 + with: + submodules: recursive + fetch-depth: 25 + node-version: 18 - - name: Run npm install - run: yarn + - name: Setup Project + uses: ./.github/composite-actions/setup - - name: Install Foundry - uses: onbjerg/foundry-toolchain@v1 - with: - version: nightly + - name: Install Foundry + uses: onbjerg/foundry-toolchain@v1 + with: + version: nightly - - name: Run Slither - uses: crytic/slither-action@v0.1.0 - continue-on-error: true - id: slither - with: - node-version: 16 - sarif: results.sarif + - name: Run Slither + uses: crytic/slither-action@v0.3.0 + continue-on-error: true + id: slither + with: + sarif: results.sarif + slither-args: --foundry-out-directory artifacts_forge - - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@v1 - with: - sarif_file: ${{ steps.slither.outputs.sarif }} + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: ${{ steps.slither.outputs.sarif }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 612b33205..81bdc28dd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,27 +10,43 @@ on: pull_request: branches: [main] +# cancel previous runs if new commits are pushed to the branch +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" - build: + test: # The type of runner that the job will run on - runs-on: ubuntu-latest + # 16 core paid runner + runs-on: ubuntu-latest-16 # Steps represent a sequence of tasks that will be executed as part of the job steps: - - uses: actions/checkout@v2 + - name: Checkout repository + uses: actions/checkout@v3 with: submodules: recursive - - name: Setup Node.js environment - uses: actions/setup-node@v2.4.1 - # Runs a single command using the runners shell - - name: Run npm install - run: yarn + fetch-depth: 25 + node-version: 18 + + - name: Setup Project + uses: ./.github/composite-actions/setup + - name: Install Foundry uses: onbjerg/foundry-toolchain@v1 with: version: nightly - - name: Run tests + - name: Run coverage and tests run: | + forge coverage --report lcov + lcov --remove lcov.info -o lcov.info 'src/test/**' + lcov --remove lcov.info -o lcov.info 'contracts/external-deps/**' + lcov --remove lcov.info -o lcov.info 'contracts/eip/**' forge test + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + with: + files: ./lcov.info, diff --git a/.gitignore b/.gitignore index cf74dac2d..a474e7333 100644 --- a/.gitignore +++ b/.gitignore @@ -7,18 +7,21 @@ artifacts/@openzeppelin artifacts/build-info build/ scripts/reference-scripts -cache/ +cache*/ coverage/ dist/ node_modules/ typechain/ +typechain-types/ .parcel-cache/ abi/ contracts/abi/ contracts/README.md artifacts/ +artifacts-*/ artifacts_forge/ +contract_artifacts/ # files *.env @@ -29,6 +32,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* *.dbg.json +deployArgs.json # Dev /relayerTest @@ -36,12 +40,11 @@ yarn-error.log* /notes.txt .yalc/ yalc.lock -package-lock.json -yarn.lock # Forge #/lib /out +lcov.info #Build .swc/ @@ -54,3 +57,5 @@ corpus/ # IDES .idea + +*.DS_Store diff --git a/.gitmodules b/.gitmodules index 9b90b5f46..db5ccba93 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,4 +3,55 @@ url = https://github.com/brockelmore/forge-std [submodule "lib/ds-test"] path = lib/ds-test - url = https://github.com/dapphub/ds-test \ No newline at end of file + url = https://github.com/dapphub/ds-test +[submodule "lib/chainlink"] + path = lib/chainlink + url = https://github.com/smartcontractkit/chainlink +[submodule "lib/ERC721A-Upgradeable"] + path = lib/ERC721A-Upgradeable + url = https://github.com/chiru-labs/ERC721A-Upgradeable +[submodule "lib/ERC721A"] + path = lib/ERC721A + url = https://github.com/chiru-labs/ERC721A +[submodule "lib/dynamic-contracts"] + path = lib/dynamic-contracts + url = https://github.com/thirdweb-dev/dynamic-contracts +[submodule "lib/solady"] + path = lib/solady + url = https://github.com/vectorized/solady +[submodule "lib/seaport"] + path = lib/seaport + url = https://github.com/ProjectOpenSea/seaport +[submodule "lib/murky"] + path = lib/murky + url = https://github.com/dmfxyz/murky +[submodule "lib/solmate"] + path = lib/solmate + url = https://github.com/transmissions11/solmate +[submodule "lib/solarray"] + path = lib/solarray + url = https://github.com/emo-eth/solarray +[submodule "lib/seaport-types"] + path = lib/seaport-types + url = https://github.com/projectopensea/seaport-types +[submodule "lib/seaport-core"] + path = lib/seaport-core + url = https://github.com/projectopensea/seaport-core +[submodule "lib/seaport-sol"] + path = lib/seaport-sol + url = https://github.com/projectopensea/seaport-sol +[submodule "lib/openzeppelin-contracts-upgradeable"] + path = lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/v3-periphery"] + path = lib/v3-periphery + url = https://github.com/uniswap/v3-periphery +[submodule "lib/v3-core"] + path = lib/v3-core + url = https://github.com/uniswap/v3-core +[submodule "lib/swap-router-contracts"] + path = lib/swap-router-contracts + url = https://github.com/Uniswap/swap-router-contracts diff --git a/.prettierignore b/.prettierignore index 4971c404b..11631ff81 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,4 +9,5 @@ node_modules/ typechain/ # files +src/test/smart-wallet/utils/AABenchmarkArtifacts.sol coverage.json diff --git a/.prettierrc b/.prettierrc index 68c86bb0e..09e99ec7c 100644 --- a/.prettierrc +++ b/.prettierrc @@ -11,8 +11,7 @@ { "files": "*.sol", "options": { - "tabWidth": 4, - "explicitTypes": "always" + "tabWidth": 4 } } ] diff --git a/.solhint.json b/.solhint.json index 930af7069..aa45d9874 100644 --- a/.solhint.json +++ b/.solhint.json @@ -4,10 +4,9 @@ "rules": { "imports-on-top": "error", "no-unused-vars": "error", - "code-complexity": ["error", 7], + "code-complexity": ["error", 9], "compiler-version": ["error", "^0.8.0"], "const-name-snakecase": "error", - "contract-name-camelcase": "error", "event-name-camelcase": "error", "constructor-syntax": "error", "func-name-mixedcase": "off", @@ -17,6 +16,9 @@ "var-name-mixedcase": "error", "func-visibility": ["error", { "ignoreConstructors": true }], "not-rely-on-time": "off", + "no-empty-blocks": "off", + "contract-name-camelcase": "off", + "no-inline-assembly": "off", "prettier/prettier": [ "error", { diff --git a/README.md b/README.md index e1ad5a875..8643c0471 100644 --- a/README.md +++ b/README.md @@ -16,24 +16,45 @@ ## Installation ```shell +# Forge projects +forge install https://github.com/thirdweb-dev/contracts + +# Hardhat / npm based projects npm i @thirdweb-dev/contracts ``` -## Deployed addresses - -### Production - -- `TWRegistry`: [0x7c487845f98938Bb955B1D5AD069d9a30e4131fd](https://blockscan.com/address/0x7c487845f98938Bb955B1D5AD069d9a30e4131fd) - -- `TWFactory`: [0x11c34F062Cb10a20B9F463E12Ff9dA62D76FDf65](https://blockscan.com/address/0x11c34F062Cb10a20B9F463E12Ff9dA62D76FDf65) - -### Dev - (Mumbai only) - -- `TWRegistry`: [0x3F17972CB27506eb4a6a3D59659e0B57a43fd16C](https://blockscan.com/address/0x3F17972CB27506eb4a6a3D59659e0B57a43fd16C#code) - -- `ByocRegistry`: [0x61Bb02795b4fF5248169A54D9f149C4557B0B7de](https://mumbai.polygonscan.com/address/0x61Bb02795b4fF5248169A54D9f149C4557B0B7de#code) - -- `ByocFactory`: [0x3c3D901Acb5f7746dCf06B26fCe881d21970d2B6](https://mumbai.polygonscan.com/address/0x3c3D901Acb5f7746dCf06B26fCe881d21970d2B6#code) +```bash +contracts +| +|-- extension: "extensions that can be inherited by NON-upgradeable contracts" +| |-- interface: "interfaces of all extension contracts" +| |-- upgradeable: "extensions that can be inherited by upgradeable contracts" +| |-- [$prebuilt-category]: "legacy extensions written specifically for a prebuilt contract" +| +|-- base: "NON-upgradeable base contracts to build on top of" +| |-- interface: "interfaces for all base contracts" +| |-- upgradeable: "upgradeable base contracts to build on top of" +| +|-- prebuilt: "audited, ready-to-deploy thirdweb smart contracts" +| |-- interface: "interfaces for all prebuilt contracts" +| |--[$prebuilt-category]: "feature-based group of prebuilt contracts" +| |-- unaudited: "yet-to-audit thirdweb smart contracts" +| |-- [$prebuilt-category]: "feature-based group of prebuilt contracts" +| +|-- infra: "onchain infrastructure contracts" +| |-- interface: "interfaces for all infrastructure contracts" +| +|-- eip: "implementations of relevant EIP standards" +| |-- interface "all interfaces of relevant EIP standards" +| +|-- lib: "Solidity libraries" +| +|-- external-deps: "modified / copied over external dependencies" +| |-- openzeppelin: "modified / copied over openzeppelin dependencies" +| |-- chainlink: "modified / copied over chainlink dependencies" +| +|-- legacy-contracts: "maintained legacy thirdweb contracts" +``` ## Running Tests @@ -41,7 +62,7 @@ npm i @thirdweb-dev/contracts 2. `forge install`: install tests dependencies 3. `forge test`: run the tests -This repository is a hybrid [hardhat](https://hardhat.org/) and [forge](https://github.com/foundry-rs/foundry/tree/master/forge) project. +This repository is a [forge](https://github.com/foundry-rs/foundry/tree/master/forge) project. First install the relevant dependencies of the project: @@ -57,51 +78,54 @@ To compile contracts, run: forge build ``` -Or, if you prefer hardhat, you can run: - -```bash -npx hardhat compile -``` - To run tests: ```bash forge test ``` -To export the ABIs of the contracts in the `/contracts` directory, run: +## Pre-built Contracts -``` -npx hardhat export-abi -``` +Pre-built contracts are written by the thirdweb team, and cover the most common use cases for smart contracts. -To run any scripts in the `/scripts` directory, run: +- [DropERC20](https://thirdweb.com/deployer.thirdweb.eth/DropERC20) +- [DropERC721](https://thirdweb.com/deployer.thirdweb.eth/DropERC721) +- [DropERC1155](https://thirdweb.com/deployer.thirdweb.eth/DropERC1155) +- [SignatureDrop](https://thirdweb.com/deployer.thirdweb.eth/SignatureDrop) +- [Marketplace](https://thirdweb.com/deployer.thirdweb.eth/Marketplace) +- [Multiwrap](https://thirdweb.com/deployer.thirdweb.eth/Multiwrap) +- [TokenERC20](https://thirdweb.com/deployer.thirdweb.eth/TokenERC20) +- [TokenERC721](https://thirdweb.com/deployer.thirdweb.eth/TokenERC721) +- [TokenERC1155](https://thirdweb.com/deployer.thirdweb.eth/TokenERC1155) +- [VoteERC20](https://thirdweb.com/deployer.thirdweb.eth/VoteERC20) +- [Split](https://thirdweb.com/deployer.thirdweb.eth/Split) -``` -npx hardhat run scripts/{path to the script} -``` +[Learn more about pre-built contracts](https://portal.thirdweb.com/pre-built-contracts) -## Deployments +## Extensions -The thirdweb registry (`TWRegistry`) and factory (`TWFactory`) have been deployed on the following chains: +Extensions are building blocks that help enrich smart contracts with features. -- [Ethereum mainnet](https://etherscan.io/) -- [Rinkeby](https://rinkeby.etherscan.io/) -- [Goerli](https://goerli.etherscan.io/) -- [Polygon mainnet](https://polygonscan.com/) -- [Polygon Mumbai testnet](https://mumbai.polygonscan.com/) -- [Avalanche mainnet](https://snowtrace.io/) -- [Avalanche Fuji testnet](https://testnet.snowtrace.io/) -- [Fantom mainnet](https://ftmscan.com/) -- [Fantom testnet](https://testnet.ftmscan.com/) +Some blocks come packaged together as Base Contracts, which come with a full set of features out of the box that you can modify and extend. These contracts are available at `contracts/base/`. -`TWRegistry` is deployed to a common address on all mentioned networks. `TWFactory` is deployed to a common address on all mentioned networks except Fantom mainnet. +Other (smaller) blocks are Features, which provide a way for you to pick and choose which individual pieces you want to put into your contract; with full customization of how those features work. These are available at `contracts/extension/`. -- `TWRegistry`: [0x7c487845f98938Bb955B1D5AD069d9a30e4131fd](https://blockscan.com/address/0x7c487845f98938Bb955B1D5AD069d9a30e4131fd) +[Learn more about extensions](https://portal.thirdweb.com/extensions) -- `TWFactory`: [0x5DBC7B840baa9daBcBe9D2492E45D7244B54A2A0](https://blockscan.com/address/0x5DBC7B840baa9daBcBe9D2492E45D7244B54A2A0) -- `TWFactory` (Fantom mainnet): [0x97EA0Fcc552D5A8Fb5e9101316AAd0D62Ea0876B](https://blockscan.com/address/0x97EA0Fcc552D5A8Fb5e9101316AAd0D62Ea0876B) +## Contract Audits +- [Audit 1](audit-reports/audit-1.pdf) +- [Audit 2](audit-reports/audit-2.pdf) +- [Audit 3](audit-reports/audit-3.pdf) +- [Audit 4](audit-reports/audit-4.pdf) +- [Audit 5](audit-reports/audit-5.pdf) +- [Audit 6](audit-reports/audit-6.pdf) +- [Audit 7](audit-reports/audit-7.pdf) +- [Audit 8](audit-reports/audit-8.pdf) +- [Audit 9](audit-reports/audit-9.pdf) +- [Audit 10](audit-reports/audit-10.pdf) +- [Audit 11](audit-reports/audit-11.pdf) +- [Audit 12](audit-reports/audit-12.pdf) ## Bug reports diff --git a/audit-reports/audit-1.pdf b/audit-reports/audit-1.pdf new file mode 100644 index 000000000..8a459857a Binary files /dev/null and b/audit-reports/audit-1.pdf differ diff --git a/audit-reports/audit-10.pdf b/audit-reports/audit-10.pdf new file mode 100644 index 000000000..2de17d271 Binary files /dev/null and b/audit-reports/audit-10.pdf differ diff --git a/audit-reports/audit-11.pdf b/audit-reports/audit-11.pdf new file mode 100644 index 000000000..de58f2bb8 Binary files /dev/null and b/audit-reports/audit-11.pdf differ diff --git a/audit-reports/audit-12.pdf b/audit-reports/audit-12.pdf new file mode 100644 index 000000000..b3a954ef5 Binary files /dev/null and b/audit-reports/audit-12.pdf differ diff --git a/audit-reports/audit-13.pdf b/audit-reports/audit-13.pdf new file mode 100644 index 000000000..fb3ca86c7 Binary files /dev/null and b/audit-reports/audit-13.pdf differ diff --git a/audit-reports/audit-14.pdf b/audit-reports/audit-14.pdf new file mode 100644 index 000000000..35d8df4d3 Binary files /dev/null and b/audit-reports/audit-14.pdf differ diff --git a/audit-reports/audit-15.pdf b/audit-reports/audit-15.pdf new file mode 100644 index 000000000..804e33b49 Binary files /dev/null and b/audit-reports/audit-15.pdf differ diff --git a/audit-reports/audit-18.pdf b/audit-reports/audit-18.pdf new file mode 100644 index 000000000..e9a834521 Binary files /dev/null and b/audit-reports/audit-18.pdf differ diff --git a/audit-reports/audit-2.pdf b/audit-reports/audit-2.pdf new file mode 100644 index 000000000..2dd1ae41b Binary files /dev/null and b/audit-reports/audit-2.pdf differ diff --git a/audit-reports/audit-3.pdf b/audit-reports/audit-3.pdf new file mode 100644 index 000000000..5c36d9b13 Binary files /dev/null and b/audit-reports/audit-3.pdf differ diff --git a/audit-reports/audit-4.pdf b/audit-reports/audit-4.pdf new file mode 100644 index 000000000..bfe2a4a6b Binary files /dev/null and b/audit-reports/audit-4.pdf differ diff --git a/audit-reports/audit-5.pdf b/audit-reports/audit-5.pdf new file mode 100644 index 000000000..44a51b0c7 Binary files /dev/null and b/audit-reports/audit-5.pdf differ diff --git a/audit-reports/audit-6.pdf b/audit-reports/audit-6.pdf new file mode 100644 index 000000000..7c16f8262 Binary files /dev/null and b/audit-reports/audit-6.pdf differ diff --git a/audit-reports/audit-7.pdf b/audit-reports/audit-7.pdf new file mode 100644 index 000000000..418113d63 Binary files /dev/null and b/audit-reports/audit-7.pdf differ diff --git a/audit-reports/audit-8.pdf b/audit-reports/audit-8.pdf new file mode 100644 index 000000000..fc6679d73 Binary files /dev/null and b/audit-reports/audit-8.pdf differ diff --git a/audit-reports/audit-9.pdf b/audit-reports/audit-9.pdf new file mode 100644 index 000000000..27cbc5a6d Binary files /dev/null and b/audit-reports/audit-9.pdf differ diff --git a/audit-reports/preliminary-audits/airdroperc20-claimable.md b/audit-reports/preliminary-audits/airdroperc20-claimable.md new file mode 100644 index 000000000..aa22f074b --- /dev/null +++ b/audit-reports/preliminary-audits/airdroperc20-claimable.md @@ -0,0 +1,11 @@ +This document contains details on fixes / response to the preliminary audit reports added to this repository. + +## [AirdropERC20Claimable](./airdroperc20-claimable.pdf) + +### 01: Governance: TrustedForwarder can execute claims on behalf of other addresses + +- The contract doesn't add a trusted-forwarder address by default. The deployer of AirdropERC20Claimable can specify which forwarder they want to use (if any), or leave as address zero. + +### 02: Malicious users can steal the entire balance of the contract + +- This refers to the possibility of a sybil attack on open/public claims, where multiple wallets can be created to claim the quantity specified by `openClaimLimitPerWallet`. To prevent this scenario or any kind of public claiming, deployer can set `openClaimLimitPerWallet` to zero when setting claim conditions during deployment. diff --git a/audit-reports/preliminary-audits/airdroperc20-claimable.pdf b/audit-reports/preliminary-audits/airdroperc20-claimable.pdf new file mode 100644 index 000000000..64a02af70 Binary files /dev/null and b/audit-reports/preliminary-audits/airdroperc20-claimable.pdf differ diff --git a/contracts/ContractPublisher.sol b/contracts/ContractPublisher.sol deleted file mode 100644 index 10b3432ff..000000000 --- a/contracts/ContractPublisher.sol +++ /dev/null @@ -1,195 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -// ========== External imports ========== -import "@openzeppelin/contracts/metatx/ERC2771Context.sol"; -import "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; -import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import "@openzeppelin/contracts/utils/Multicall.sol"; - -// ========== Internal imports ========== -import { IContractPublisher } from "./interfaces/IContractPublisher.sol"; - -contract ContractPublisher is IContractPublisher, ERC2771Context, AccessControlEnumerable, Multicall { - using EnumerableSet for EnumerableSet.Bytes32Set; - - /*/////////////////////////////////////////////////////////////// - State variables - //////////////////////////////////////////////////////////////*/ - - /// @dev Whether the registry is paused. - bool public isPaused; - - /*/////////////////////////////////////////////////////////////// - Mappings - //////////////////////////////////////////////////////////////*/ - - /// @dev Mapping from publisher address => set of published contracts. - mapping(address => CustomContractSet) private contractsOfPublisher; - /// @dev Mapping publisher address => profile uri - mapping(address => string) private profileUriOfPublisher; - /// @dev Mapping compilerMetadataUri => publishedMetadataUri - mapping(string => PublishedMetadataSet) private compilerMetadataUriToPublishedMetadataUris; - - /*/////////////////////////////////////////////////////////////// - Constructor + modifiers - //////////////////////////////////////////////////////////////*/ - - /// @dev Checks whether caller is publisher TODO enable external approvals - modifier onlyPublisher(address _publisher) { - require(_msgSender() == _publisher, "unapproved caller"); - - _; - } - - /// @dev Checks whether contract is unpaused or the caller is a contract admin. - modifier onlyUnpausedOrAdmin() { - require(!isPaused || hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "registry paused"); - - _; - } - - constructor(address _trustedForwarder) ERC2771Context(_trustedForwarder) { - _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /*/////////////////////////////////////////////////////////////// - Getter logic - //////////////////////////////////////////////////////////////*/ - - /// @notice Returns the latest version of all contracts published by a publisher. - function getAllPublishedContracts(address _publisher) - external - view - returns (CustomContractInstance[] memory published) - { - uint256 total = EnumerableSet.length(contractsOfPublisher[_publisher].contractIds); - - published = new CustomContractInstance[](total); - - for (uint256 i = 0; i < total; i += 1) { - bytes32 contractId = EnumerableSet.at(contractsOfPublisher[_publisher].contractIds, i); - published[i] = contractsOfPublisher[_publisher].contracts[contractId].latest; - } - } - - /// @notice Returns all versions of a published contract. - function getPublishedContractVersions(address _publisher, string memory _contractId) - external - view - returns (CustomContractInstance[] memory published) - { - bytes32 id = keccak256(bytes(_contractId)); - uint256 total = contractsOfPublisher[_publisher].contracts[id].total; - - published = new CustomContractInstance[](total); - - for (uint256 i = 0; i < total; i += 1) { - published[i] = contractsOfPublisher[_publisher].contracts[id].instances[i]; - } - } - - /// @notice Returns the latest version of a contract published by a publisher. - function getPublishedContract(address _publisher, string memory _contractId) - external - view - returns (CustomContractInstance memory published) - { - published = contractsOfPublisher[_publisher].contracts[keccak256(bytes(_contractId))].latest; - } - - /*/////////////////////////////////////////////////////////////// - Publish logic - //////////////////////////////////////////////////////////////*/ - - /// @notice Let's an account publish a contract. The account must be approved by the publisher, or be the publisher. - function publishContract( - address _publisher, - string memory _contractId, - string memory _publishMetadataUri, - string memory _compilerMetadataUri, - bytes32 _bytecodeHash, - address _implementation - ) external onlyPublisher(_publisher) onlyUnpausedOrAdmin { - CustomContractInstance memory publishedContract = CustomContractInstance({ - contractId: _contractId, - publishTimestamp: block.timestamp, - publishMetadataUri: _publishMetadataUri, - bytecodeHash: _bytecodeHash, - implementation: _implementation - }); - - bytes32 contractIdInBytes = keccak256(bytes(_contractId)); - EnumerableSet.add(contractsOfPublisher[_publisher].contractIds, contractIdInBytes); - - contractsOfPublisher[_publisher].contracts[contractIdInBytes].latest = publishedContract; - - uint256 index = contractsOfPublisher[_publisher].contracts[contractIdInBytes].total; - contractsOfPublisher[_publisher].contracts[contractIdInBytes].total += 1; - contractsOfPublisher[_publisher].contracts[contractIdInBytes].instances[index] = publishedContract; - - uint256 metadataIndex = compilerMetadataUriToPublishedMetadataUris[_compilerMetadataUri].index; - compilerMetadataUriToPublishedMetadataUris[_compilerMetadataUri].uris[index] = _publishMetadataUri; - compilerMetadataUriToPublishedMetadataUris[_compilerMetadataUri].index = metadataIndex + 1; - - emit ContractPublished(_msgSender(), _publisher, publishedContract); - } - - /// @notice Lets an account unpublish a contract and all its versions. The account must be approved by the publisher, or be the publisher. - function unpublishContract(address _publisher, string memory _contractId) - external - onlyPublisher(_publisher) - onlyUnpausedOrAdmin - { - bytes32 contractIdInBytes = keccak256(bytes(_contractId)); - - bool removed = EnumerableSet.remove(contractsOfPublisher[_publisher].contractIds, contractIdInBytes); - require(removed, "given contractId DNE"); - - delete contractsOfPublisher[_publisher].contracts[contractIdInBytes]; - - emit ContractUnpublished(_msgSender(), _publisher, _contractId); - } - - /// @notice Lets an account set its own publisher profile uri - function setPublisherProfileUri(address publisher, string memory uri) public onlyPublisher(publisher) { - profileUriOfPublisher[publisher] = uri; - } - - // @notice Get a publisher profile uri - function getPublisherProfileUri(address publisher) public view returns (string memory uri) { - uri = profileUriOfPublisher[publisher]; - } - - /// @notice Retrieve the published metadata URI from a compiler metadata URI - function getPublishedUriFromCompilerUri(string memory compilerMetadataUri) - public - view - returns (string[] memory publishedMetadataUris) - { - uint256 length = compilerMetadataUriToPublishedMetadataUris[compilerMetadataUri].index; - publishedMetadataUris = new string[](length); - for (uint256 i = 0; i < length; i += 1) { - publishedMetadataUris[i] = compilerMetadataUriToPublishedMetadataUris[compilerMetadataUri].uris[i]; - } - } - - /*/////////////////////////////////////////////////////////////// - Miscellaneous - //////////////////////////////////////////////////////////////*/ - - /// @dev Lets a contract admin pause the registry. - function setPause(bool _pause) external { - require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "unapproved caller"); - isPaused = _pause; - emit Paused(_pause); - } - - function _msgSender() internal view virtual override(Context, ERC2771Context) returns (address sender) { - return ERC2771Context._msgSender(); - } - - function _msgData() internal view virtual override(Context, ERC2771Context) returns (bytes calldata) { - return ERC2771Context._msgData(); - } -} diff --git a/contracts/Forwarder.sol b/contracts/Forwarder.sol deleted file mode 100644 index e6a6ae9f9..000000000 --- a/contracts/Forwarder.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -import "@openzeppelin/contracts/metatx/MinimalForwarder.sol"; - -/* - * @dev Minimal forwarder for GSNv2 - */ -contract Forwarder is MinimalForwarder { - // solhint-disable-next-line no-empty-blocks - constructor() MinimalForwarder() {} -} diff --git a/contracts/Split.sol b/contracts/Split.sol deleted file mode 100644 index 71d9a2bb6..000000000 --- a/contracts/Split.sol +++ /dev/null @@ -1,195 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -// Thirdweb top-level -import "./interfaces/ITWFee.sol"; - -// Base -import "./openzeppelin-presets/finance/PaymentSplitterUpgradeable.sol"; -import "./interfaces/IThirdwebContract.sol"; - -// Meta-tx -import "./openzeppelin-presets/metatx/ERC2771ContextUpgradeable.sol"; - -// Access -import "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; - -// Utils -import "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; -import "./lib/FeeType.sol"; - -contract Split is - IThirdwebContract, - Initializable, - MulticallUpgradeable, - ERC2771ContextUpgradeable, - AccessControlEnumerableUpgradeable, - PaymentSplitterUpgradeable -{ - bytes32 private constant MODULE_TYPE = bytes32("Split"); - uint128 private constant VERSION = 1; - - /// @dev Max bps in the thirdweb system - uint128 private constant MAX_BPS = 10_000; - - /// @dev The thirdweb contract with fee related information. - ITWFee public immutable thirdwebFee; - - /// @dev Contract level metadata. - string public contractURI; - - constructor(address _thirdwebFee) initializer { - thirdwebFee = ITWFee(_thirdwebFee); - } - - /// @dev Performs the job of the constructor. - /// @dev shares_ are scaled by 10,000 to prevent precision loss when including fees - function initialize( - address _defaultAdmin, - string memory _contractURI, - address[] memory _trustedForwarders, - address[] memory _payees, - uint256[] memory _shares - ) external initializer { - // Initialize inherited contracts: most base -> most derived - __ERC2771Context_init(_trustedForwarders); - __PaymentSplitter_init(_payees, _shares); - - contractURI = _contractURI; - _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); - } - - /// @dev Returns the module type of the contract. - function contractType() external pure returns (bytes32) { - return MODULE_TYPE; - } - - /// @dev Returns the version of the contract. - function contractVersion() external pure returns (uint8) { - return uint8(VERSION); - } - - /** - * @dev Triggers a transfer to `account` of the amount of Ether they are owed, according to their percentage of the - * total shares and their previous withdrawals. - */ - function release(address payable account) public virtual override { - uint256 payment = _release(account); - require(payment != 0, "PaymentSplitter: account is not due payment"); - } - - /** - * @dev Triggers a transfer to `account` of the amount of `token` tokens they are owed, according to their - * percentage of the total shares and their previous withdrawals. `token` must be the address of an IERC20 - * contract. - */ - function release(IERC20Upgradeable token, address account) public virtual override { - uint256 payment = _release(token, account); - require(payment != 0, "PaymentSplitter: account is not due payment"); - } - - /// @dev Returns the amount of Ether that `account` is owed, according to their percentage of the total shares and returns the payment - function _release(address payable account) internal returns (uint256) { - require(shares(account) > 0, "PaymentSplitter: account has no shares"); - - uint256 totalReceived = address(this).balance + totalReleased(); - uint256 payment = _pendingPayment(account, totalReceived, released(account)); - - if (payment == 0) { - return 0; - } - - _released[account] += payment; - _totalReleased += payment; - - // fees - uint256 fee = 0; - (address feeRecipient, uint256 feeBps) = thirdwebFee.getFeeInfo(address(this), FeeType.SPLIT); - if (feeRecipient != address(0) && feeBps > 0) { - fee = (payment * feeBps) / MAX_BPS; - AddressUpgradeable.sendValue(payable(feeRecipient), fee); - } - - AddressUpgradeable.sendValue(account, payment - fee); - emit PaymentReleased(account, payment); - - return payment; - } - - /// @dev Returns the amount of `token` that `account` is owed, according to their percentage of the total shares and returns the payment - function _release(IERC20Upgradeable token, address account) internal returns (uint256) { - require(shares(account) > 0, "PaymentSplitter: account has no shares"); - - uint256 totalReceived = token.balanceOf(address(this)) + totalReleased(token); - uint256 payment = _pendingPayment(account, totalReceived, released(token, account)); - - if (payment == 0) { - return 0; - } - - _erc20Released[token][account] += payment; - _erc20TotalReleased[token] += payment; - - // fees - uint256 fee = 0; - (address feeRecipient, uint256 feeBps) = thirdwebFee.getFeeInfo(address(this), FeeType.SPLIT); - if (feeRecipient != address(0) && feeBps > 0) { - fee = (payment * feeBps) / MAX_BPS; - SafeERC20Upgradeable.safeTransfer(token, feeRecipient, fee); - } - - SafeERC20Upgradeable.safeTransfer(token, account, payment - fee); - emit ERC20PaymentReleased(token, account, payment); - - return payment; - } - - /** - * @dev Release the owed amount of token to all of the payees. - */ - function distribute() public virtual { - uint256 count = payeeCount(); - for (uint256 i = 0; i < count; i++) { - // note: `_release` should not fail because payee always has shares, protected by `_appPay` - _release(payable(payee(i))); - } - } - - /** - * @dev Release owed amount of the `token` to all of the payees. - */ - function distribute(IERC20Upgradeable token) public virtual { - uint256 count = payeeCount(); - for (uint256 i = 0; i < count; i++) { - // note: `_release` should not fail because payee always has shares, protected by `_appPay` - _release(token, payee(i)); - } - } - - /// @dev See ERC2771 - function _msgSender() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (address sender) - { - return ERC2771ContextUpgradeable._msgSender(); - } - - /// @dev See ERC2771 - function _msgData() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (bytes calldata) - { - return ERC2771ContextUpgradeable._msgData(); - } - - /// @dev Sets contract URI for the contract-level metadata of the contract. - function setContractURI(string calldata _uri) external onlyRole(DEFAULT_ADMIN_ROLE) { - contractURI = _uri; - } -} diff --git a/contracts/TWFactory.sol b/contracts/TWFactory.sol deleted file mode 100644 index 5bd813382..000000000 --- a/contracts/TWFactory.sol +++ /dev/null @@ -1,131 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -import "./TWRegistry.sol"; -import "./interfaces/IThirdwebContract.sol"; - -import "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; -import "@openzeppelin/contracts/metatx/ERC2771Context.sol"; -import "@openzeppelin/contracts/utils/Create2.sol"; -import "@openzeppelin/contracts/utils/Multicall.sol"; -import "@openzeppelin/contracts/proxy/Clones.sol"; - -contract TWFactory is Multicall, ERC2771Context, AccessControlEnumerable { - /// @dev Only FACTORY_ROLE holders can approve/unapprove implementations for proxies to point to. - bytes32 public constant FACTORY_ROLE = keccak256("FACTORY_ROLE"); - - TWRegistry public immutable registry; - - /// @dev Emitted when a proxy is deployed. - event ProxyDeployed(address indexed implementation, address proxy, address indexed deployer); - event ImplementationAdded(address implementation, bytes32 indexed contractType, uint256 version); - event ImplementationApproved(address implementation, bool isApproved); - - /// @dev mapping of implementation address to deployment approval - mapping(address => bool) public approval; - - /// @dev mapping of implementation address to implementation added version - mapping(bytes32 => uint256) public currentVersion; - - /// @dev mapping of contract type to module version to implementation address - mapping(bytes32 => mapping(uint256 => address)) public implementation; - - /// @dev mapping of proxy address to deployer address - mapping(address => address) public deployer; - - constructor(address _trustedForwarder, address _registry) ERC2771Context(_trustedForwarder) { - _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); - _setupRole(FACTORY_ROLE, _msgSender()); - - registry = TWRegistry(_registry); - } - - /// @dev Deploys a proxy that points to the latest version of the given contract type. - function deployProxy(bytes32 _type, bytes memory _data) external returns (address) { - bytes32 salt = bytes32(registry.count(_msgSender())); - return deployProxyDeterministic(_type, _data, salt); - } - - /** - * @dev Deploys a proxy at a deterministic address by taking in `salt` as a parameter. - * Proxy points to the latest version of the given contract type. - */ - function deployProxyDeterministic( - bytes32 _type, - bytes memory _data, - bytes32 _salt - ) public returns (address) { - address _implementation = implementation[_type][currentVersion[_type]]; - return deployProxyByImplementation(_implementation, _data, _salt); - } - - /// @dev Deploys a proxy that points to the given implementation. - function deployProxyByImplementation( - address _implementation, - bytes memory _data, - bytes32 _salt - ) public returns (address deployedProxy) { - require(approval[_implementation], "implementation not approved"); - - bytes32 salthash = keccak256(abi.encodePacked(_msgSender(), _salt)); - deployedProxy = Clones.cloneDeterministic(_implementation, salthash); - - deployer[deployedProxy] = _msgSender(); - - emit ProxyDeployed(_implementation, deployedProxy, _msgSender()); - - registry.add(_msgSender(), deployedProxy); - - if (_data.length > 0) { - // slither-disable-next-line unused-return - Address.functionCall(deployedProxy, _data); - } - } - - /// @dev Lets a contract admin set the address of a contract type x version. - function addImplementation(address _implementation) external { - require(hasRole(FACTORY_ROLE, _msgSender()), "not admin."); - - IThirdwebContract module = IThirdwebContract(_implementation); - - bytes32 ctype = module.contractType(); - require(ctype.length > 0, "invalid module"); - - uint8 version = module.contractVersion(); - uint8 currentVersionOfType = uint8(currentVersion[ctype]); - require(version >= currentVersionOfType, "wrong module version"); - - currentVersion[ctype] = version; - implementation[ctype][version] = _implementation; - approval[_implementation] = true; - - emit ImplementationAdded(_implementation, ctype, version); - } - - /// @dev Lets a contract admin approve a specific contract for deployment. - function approveImplementation(address _implementation, bool _toApprove) external { - require(hasRole(FACTORY_ROLE, _msgSender()), "not admin."); - - approval[_implementation] = _toApprove; - - emit ImplementationApproved(_implementation, _toApprove); - } - - /// @dev Returns the implementation given a contract type and version. - function getImplementation(bytes32 _type, uint256 _version) external view returns (address) { - return implementation[_type][_version]; - } - - /// @dev Returns the latest implementation given a contract type. - function getLatestImplementation(bytes32 _type) external view returns (address) { - return implementation[_type][currentVersion[_type]]; - } - - function _msgSender() internal view virtual override(Context, ERC2771Context) returns (address sender) { - return ERC2771Context._msgSender(); - } - - function _msgData() internal view virtual override(Context, ERC2771Context) returns (bytes calldata) { - return ERC2771Context._msgData(); - } -} diff --git a/contracts/TWProxy.sol b/contracts/TWProxy.sol deleted file mode 100644 index 61c643e5c..000000000 --- a/contracts/TWProxy.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -import "@openzeppelin/contracts/proxy/Proxy.sol"; -import "@openzeppelin/contracts/utils/Address.sol"; -import "@openzeppelin/contracts/utils/StorageSlot.sol"; - -contract TWProxy is Proxy { - /** - * @dev Storage slot with the address of the current implementation. - * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is - * validated in the constructor. - */ - bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - - constructor(address _logic, bytes memory _data) payable { - assert(_IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)); - StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = _logic; - if (_data.length > 0) { - // slither-disable-next-line unused-return - Address.functionDelegateCall(_logic, _data); - } - } - - /** - * @dev Returns the current implementation address. - */ - function _implementation() internal view override returns (address impl) { - return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; - } -} diff --git a/contracts/TWRegistry.sol b/contracts/TWRegistry.sol deleted file mode 100644 index ddf7183cb..000000000 --- a/contracts/TWRegistry.sol +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; -import "@openzeppelin/contracts/utils/Multicall.sol"; -import "@openzeppelin/contracts/metatx/ERC2771Context.sol"; - -contract TWRegistry is Multicall, ERC2771Context, AccessControlEnumerable { - bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); - - using EnumerableSet for EnumerableSet.AddressSet; - - /// @dev wallet address => [contract addresses] - mapping(address => EnumerableSet.AddressSet) private deployments; - - event Added(address indexed deployer, address indexed deployment); - event Deleted(address indexed deployer, address indexed deployment); - - constructor(address _trustedForwarder) ERC2771Context(_trustedForwarder) { - _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - // slither-disable-next-line similar-names - function add(address _deployer, address _deployment) external { - require(hasRole(OPERATOR_ROLE, _msgSender()) || _deployer == _msgSender(), "not operator or deployer."); - - bool added = deployments[_deployer].add(_deployment); - require(added, "failed to add"); - - emit Added(_deployer, _deployment); - } - - // slither-disable-next-line similar-names - function remove(address _deployer, address _deployment) external { - require(hasRole(OPERATOR_ROLE, _msgSender()) || _deployer == _msgSender(), "not operator or deployer."); - - bool removed = deployments[_deployer].remove(_deployment); - require(removed, "failed to remove"); - - emit Deleted(_deployer, _deployment); - } - - function getAll(address _deployer) external view returns (address[] memory) { - return deployments[_deployer].values(); - } - - function count(address _deployer) external view returns (uint256) { - return deployments[_deployer].length(); - } - - function _msgSender() internal view virtual override(Context, ERC2771Context) returns (address sender) { - return ERC2771Context._msgSender(); - } - - function _msgData() internal view virtual override(Context, ERC2771Context) returns (bytes calldata) { - return ERC2771Context._msgData(); - } -} diff --git a/contracts/base/ERC1155Base.sol b/contracts/base/ERC1155Base.sol new file mode 100644 index 000000000..d19da2065 --- /dev/null +++ b/contracts/base/ERC1155Base.sol @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import { ERC1155 } from "../eip/ERC1155.sol"; + +import "../extension/ContractMetadata.sol"; +import "../extension/Multicall.sol"; +import "../extension/Ownable.sol"; +import "../extension/Royalty.sol"; +import "../extension/BatchMintMetadata.sol"; + +import "../lib/Strings.sol"; + +/** + * The `ERC1155Base` smart contract implements the ERC1155 NFT standard. + * It includes the following additions to standard ERC1155 logic: + * + * - Ability to mint NFTs via the provided `mintTo` and `batchMintTo` functions. + * + * - Contract metadata for royalty support on platforms such as OpenSea that use + * off-chain information to distribute roaylties. + * + * - Ownership of the contract, with the ability to restrict certain functions to + * only be called by the contract's owner. + * + * - Multicall capability to perform multiple actions atomically + * + * - EIP 2981 compliance for royalty support on NFT marketplaces. + */ + +contract ERC1155Base is ERC1155, ContractMetadata, Ownable, Royalty, Multicall, BatchMintMetadata { + using Strings for uint256; + + /*////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + /// @dev The tokenId of the next NFT to mint. + uint256 internal nextTokenIdToMint_; + + /*////////////////////////////////////////////////////////////// + Mappings + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the total supply of NFTs of a given tokenId + * @dev Mapping from tokenId => total circulating supply of NFTs of that tokenId. + */ + mapping(uint256 => uint256) public totalSupply; + + /*////////////////////////////////////////////////////////////// + Constructor + //////////////////////////////////////////////////////////////*/ + + constructor( + address _defaultAdmin, + string memory _name, + string memory _symbol, + address _royaltyRecipient, + uint128 _royaltyBps + ) ERC1155(_name, _symbol) { + _setupOwner(_defaultAdmin); + _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + } + + /*////////////////////////////////////////////////////////////// + Overriden metadata logic + //////////////////////////////////////////////////////////////*/ + + /// @notice Returns the metadata URI for the given tokenId. + /// @param _tokenId The tokenId of the token for which a URI should be returned. + /// @return The metadata URI for the given tokenId. + function uri(uint256 _tokenId) public view virtual override returns (string memory) { + string memory uriForToken = _uri[_tokenId]; + if (bytes(uriForToken).length > 0) { + return uriForToken; + } + + string memory batchUri = _getBaseURI(_tokenId); + return string(abi.encodePacked(batchUri, _tokenId.toString())); + } + + /*////////////////////////////////////////////////////////////// + Mint / burn logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Lets an authorized address mint NFTs to a recipient. + * @dev - The logic in the `_canMint` function determines whether the caller is authorized to mint NFTs. + * - If `_tokenId == type(uint256).max` a new NFT at tokenId `nextTokenIdToMint` is minted. If the given + * `tokenId < nextTokenIdToMint`, then additional supply of an existing NFT is being minted. + * + * @param _to The recipient of the NFTs to mint. + * @param _tokenId The tokenId of the NFT to mint. + * @param _tokenURI The full metadata URI for the NFTs minted (if a new NFT is being minted). + * @param _amount The amount of the same NFT to mint. + */ + function mintTo(address _to, uint256 _tokenId, string memory _tokenURI, uint256 _amount) public virtual { + require(_canMint(), "Not authorized to mint."); + + uint256 tokenIdToMint; + uint256 nextIdToMint = nextTokenIdToMint(); + + if (_tokenId == type(uint256).max) { + tokenIdToMint = nextIdToMint; + nextTokenIdToMint_ += 1; + _setTokenURI(nextIdToMint, _tokenURI); + } else { + require(_tokenId < nextIdToMint, "invalid id"); + tokenIdToMint = _tokenId; + } + + _mint(_to, tokenIdToMint, _amount, ""); + } + + /** + * @notice Lets an authorized address mint multiple NEW NFTs at once to a recipient. + * @dev The logic in the `_canMint` function determines whether the caller is authorized to mint NFTs. + * If `_tokenIds[i] == type(uint256).max` a new NFT at tokenId `nextTokenIdToMint` is minted. If the given + * `tokenIds[i] < nextTokenIdToMint`, then additional supply of an existing NFT is minted. + * The metadata for each new NFT is stored at `baseURI/{tokenID of NFT}` + * + * @param _to The recipient of the NFT to mint. + * @param _tokenIds The tokenIds of the NFTs to mint. + * @param _amounts The amounts of each NFT to mint. + * @param _baseURI The baseURI for the `n` number of NFTs minted. The metadata for each NFT is `baseURI/tokenId` + */ + function batchMintTo( + address _to, + uint256[] memory _tokenIds, + uint256[] memory _amounts, + string memory _baseURI + ) public virtual { + require(_canMint(), "Not authorized to mint."); + require(_amounts.length > 0, "Minting zero tokens."); + require(_tokenIds.length == _amounts.length, "Length mismatch."); + + uint256 nextIdToMint = nextTokenIdToMint(); + uint256 startNextIdToMint = nextIdToMint; + + uint256 numOfNewNFTs; + + for (uint256 i = 0; i < _tokenIds.length; i += 1) { + if (_tokenIds[i] == type(uint256).max) { + _tokenIds[i] = nextIdToMint; + + nextIdToMint += 1; + numOfNewNFTs += 1; + } else { + require(_tokenIds[i] < nextIdToMint, "invalid id"); + } + } + + if (numOfNewNFTs > 0) { + _batchMintMetadata(startNextIdToMint, numOfNewNFTs, _baseURI); + } + + nextTokenIdToMint_ = nextIdToMint; + _mintBatch(_to, _tokenIds, _amounts, ""); + } + + /** + * @notice Lets an owner or approved operator burn NFTs of the given tokenId. + * + * @param _owner The owner of the NFT to burn. + * @param _tokenId The tokenId of the NFT to burn. + * @param _amount The amount of the NFT to burn. + */ + function burn(address _owner, uint256 _tokenId, uint256 _amount) external virtual { + address caller = msg.sender; + + require(caller == _owner || isApprovedForAll[_owner][caller], "Unapproved caller"); + require(balanceOf[_owner][_tokenId] >= _amount, "Not enough tokens owned"); + + _burn(_owner, _tokenId, _amount); + } + + /** + * @notice Lets an owner or approved operator burn NFTs of the given tokenIds. + * + * @param _owner The owner of the NFTs to burn. + * @param _tokenIds The tokenIds of the NFTs to burn. + * @param _amounts The amounts of the NFTs to burn. + */ + function burnBatch(address _owner, uint256[] memory _tokenIds, uint256[] memory _amounts) external virtual { + address caller = msg.sender; + + require(caller == _owner || isApprovedForAll[_owner][caller], "Unapproved caller"); + require(_tokenIds.length == _amounts.length, "Length mismatch"); + + for (uint256 i = 0; i < _tokenIds.length; i += 1) { + require(balanceOf[_owner][_tokenIds[i]] >= _amounts[i], "Not enough tokens owned"); + } + + _burnBatch(_owner, _tokenIds, _amounts); + } + + /*////////////////////////////////////////////////////////////// + ERC165 Logic + //////////////////////////////////////////////////////////////*/ + + /** + * @dev See ERC165: https://eips.ethereum.org/EIPS/eip-165 + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC1155, IERC165) returns (bool) { + return + interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 + interfaceId == 0xd9b67a26 || // ERC165 Interface ID for ERC1155 + interfaceId == 0x0e89341c || // ERC165 Interface ID for ERC1155MetadataURI + interfaceId == type(IERC2981).interfaceId; // ERC165 ID for ERC2981 + } + + /*////////////////////////////////////////////////////////////// + View functions + //////////////////////////////////////////////////////////////*/ + + /// @notice The tokenId assigned to the next new NFT to be minted. + function nextTokenIdToMint() public view virtual returns (uint256) { + return nextTokenIdToMint_; + } + + /*////////////////////////////////////////////////////////////// + Internal (overrideable) functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns whether contract metadata can be set in the given execution context. + /// @return Whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether a token can be minted in the given execution context. + /// @return Whether a token can be minted in the given execution context. + function _canMint() internal view virtual returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether owner can be set in the given execution context. + /// @return Whether owner can be set in the given execution context. + function _canSetOwner() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether royalty info can be set in the given execution context. + /// @return Whether royalty info can be set in the given execution context. + function _canSetRoyaltyInfo() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Runs before every token transfer / mint / burn. + /// @param operator The address of the caller. + /// @param from The address of the sender. + /// @param to The address of the recipient. + /// @param ids The tokenIds of the tokens being transferred. + /// @param amounts The amounts of the tokens being transferred. + /// @param data Additional data with no specified format. + function _beforeTokenTransfer( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal virtual override { + super._beforeTokenTransfer(operator, from, to, ids, amounts, data); + + if (from == address(0)) { + for (uint256 i = 0; i < ids.length; ++i) { + totalSupply[ids[i]] += amounts[i]; + } + } + + if (to == address(0)) { + for (uint256 i = 0; i < ids.length; ++i) { + totalSupply[ids[i]] -= amounts[i]; + } + } + } +} diff --git a/contracts/base/ERC1155DelayedReveal.sol b/contracts/base/ERC1155DelayedReveal.sol new file mode 100644 index 000000000..3b06ef207 --- /dev/null +++ b/contracts/base/ERC1155DelayedReveal.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./ERC1155LazyMint.sol"; +import "../extension/DelayedReveal.sol"; + +/** + * BASE: ERC1155LazyMint + * EXTENSION: DelayedReveal + * + * The `ERC1155DelayedReveal` contract uses the `DelayedReveal` extension. + * + * 'Lazy minting' means defining the metadata of NFTs without minting it to an address. Regular 'minting' + * of NFTs means actually assigning an owner to an NFT. + * + * As a contract admin, this lets you prepare the metadata for NFTs that will be minted by an external party, + * without paying the gas cost for actually minting the NFTs. + * + * 'Delayed reveal' is a mechanism by which you can distribute NFTs to your audience and reveal the metadata of the distributed + * NFTs, after the fact. + * + * You can read more about how the `DelayedReveal` extension works, here: https://blog.thirdweb.com/delayed-reveal-nfts + */ + +contract ERC1155DelayedReveal is ERC1155LazyMint, DelayedReveal { + using Strings for uint256; + + /*////////////////////////////////////////////////////////////// + Constructor + //////////////////////////////////////////////////////////////*/ + + constructor( + address _defaultAdmin, + string memory _name, + string memory _symbol, + address _royaltyRecipient, + uint128 _royaltyBps + ) ERC1155LazyMint(_defaultAdmin, _name, _symbol, _royaltyRecipient, _royaltyBps) {} + + /*////////////////////////////////////////////////////////////// + Overriden Metadata logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the metadata URI for an NFT. + * @dev See `BatchMintMetadata` for handling of metadata in this contract. + * + * @param _tokenId The tokenId of an NFT. + */ + function uri(uint256 _tokenId) public view virtual override returns (string memory) { + (uint256 batchId, ) = _getBatchId(_tokenId); + string memory batchUri = _getBaseURI(_tokenId); + + if (isEncryptedBatch(batchId)) { + return string(abi.encodePacked(batchUri, "0")); + } else { + return string(abi.encodePacked(batchUri, _tokenId.toString())); + } + } + + /*////////////////////////////////////////////////////////////// + Lazy minting logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Lets an authorized address lazy mint a given amount of NFTs. + * + * @param _amount The number of NFTs to lazy mint. + * @param _baseURIForTokens The placeholder base URI for the 'n' number of NFTs being lazy minted, where the + * metadata for each of those NFTs is `${baseURIForTokens}/${tokenId}`. + * @param _data The encrypted base URI + provenance hash for the batch of NFTs being lazy minted. + * @return batchId A unique integer identifier for the batch of NFTs lazy minted together. + */ + function lazyMint( + uint256 _amount, + string calldata _baseURIForTokens, + bytes calldata _data + ) public virtual override returns (uint256 batchId) { + if (_data.length > 0) { + (bytes memory encryptedURI, bytes32 provenanceHash) = abi.decode(_data, (bytes, bytes32)); + if (encryptedURI.length != 0 && provenanceHash != "") { + _setEncryptedData(nextTokenIdToLazyMint + _amount, _data); + } + } + + return super.lazyMint(_amount, _baseURIForTokens, _data); + } + + /*////////////////////////////////////////////////////////////// + Delayed reveal logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Lets an authorized address reveal a batch of delayed reveal NFTs. + * + * @param _index The ID for the batch of delayed-reveal NFTs to reveal. + * @param _key The key with which the base URI for the relevant batch of NFTs was encrypted. + */ + function reveal(uint256 _index, bytes calldata _key) external virtual override returns (string memory revealedURI) { + require(_canReveal(), "Not authorized"); + + uint256 batchId = getBatchIdAtIndex(_index); + revealedURI = getRevealURI(batchId, _key); + + _setEncryptedData(batchId, ""); + _setBaseURI(batchId, revealedURI); + + emit TokenURIRevealed(_index, revealedURI); + } + + /// @dev Checks whether NFTs can be revealed in the given execution context. + function _canReveal() internal view virtual returns (bool) { + return msg.sender == owner(); + } +} diff --git a/contracts/base/ERC1155Drop.sol b/contracts/base/ERC1155Drop.sol new file mode 100644 index 000000000..a85a1ba56 --- /dev/null +++ b/contracts/base/ERC1155Drop.sol @@ -0,0 +1,368 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import { ERC1155 } from "../eip/ERC1155.sol"; + +import "../extension/ContractMetadata.sol"; +import "../extension/Multicall.sol"; +import "../extension/Ownable.sol"; +import "../extension/Royalty.sol"; +import "../extension/BatchMintMetadata.sol"; +import "../extension/PrimarySale.sol"; +import "../extension/DropSinglePhase1155.sol"; +import "../extension/LazyMint.sol"; +import "../extension/DelayedReveal.sol"; + +import { CurrencyTransferLib } from "../lib/CurrencyTransferLib.sol"; +import "../lib/Strings.sol"; + +/** + * BASE: ERC1155Base + * EXTENSION: DropSinglePhase1155 + * + * The `ERC1155Base` smart contract implements the ERC1155 NFT standard. + * It includes the following additions to standard ERC1155 logic: + * + * - Contract metadata for royalty support on platforms such as OpenSea that use + * off-chain information to distribute roaylties. + * + * - Ownership of the contract, with the ability to restrict certain functions to + * only be called by the contract's owner. + * + * - Multicall capability to perform multiple actions atomically + * + * - EIP 2981 compliance for royalty support on NFT marketplaces. + * + * The `drop` mechanism in the `DropSinglePhase1155` extension is a distribution mechanism for lazy minted tokens. It lets + * you set restrictions such as a price to charge, an allowlist etc. when an address atttempts to mint lazy minted tokens. + * + * The `ERC721Drop` contract lets you lazy mint tokens, and distribute those lazy minted tokens via the drop mechanism. + */ + +contract ERC1155Drop is + ERC1155, + ContractMetadata, + Ownable, + Royalty, + Multicall, + BatchMintMetadata, + PrimarySale, + LazyMint, + DelayedReveal, + DropSinglePhase1155 +{ + using Strings for uint256; + + /*////////////////////////////////////////////////////////////// + Mappings + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the total supply of NFTs of a given tokenId + * @dev Mapping from tokenId => total circulating supply of NFTs of that tokenId. + */ + mapping(uint256 => uint256) public totalSupply; + + /*/////////////////////////////////////////////////////////////// + Constructor + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Initializes the contract with the given parameters. + * + * @param _defaultAdmin The default admin for the contract. + * @param _name The name of the contract. + * @param _symbol The symbol of the contract. + * @param _royaltyRecipient The address to which royalties should be sent. + * @param _royaltyBps The royalty basis points to be charged. Max = 10000 (10000 = 100%, 1000 = 10%) + * @param _primarySaleRecipient The address to which primary sale revenue should be sent. + */ + constructor( + address _defaultAdmin, + string memory _name, + string memory _symbol, + address _royaltyRecipient, + uint128 _royaltyBps, + address _primarySaleRecipient + ) ERC1155(_name, _symbol) { + _setupOwner(_defaultAdmin); + _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + _setupPrimarySaleRecipient(_primarySaleRecipient); + } + + /*////////////////////////////////////////////////////////////// + ERC165 Logic + //////////////////////////////////////////////////////////////*/ + + /** + * @dev See ERC165: https://eips.ethereum.org/EIPS/eip-165 + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC1155, IERC165) returns (bool) { + return + interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 + interfaceId == 0xd9b67a26 || // ERC165 Interface ID for ERC1155 + interfaceId == 0x0e89341c || // ERC165 Interface ID for ERC1155MetadataURI + interfaceId == type(IERC2981).interfaceId; // ERC165 ID for ERC2981 + } + + /*////////////////////////////////////////////////////////////// + Minting/burning logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Lets an owner or approved operator burn NFTs of the given tokenId. + * + * @param _owner The owner of the NFT to burn. + * @param _tokenId The tokenId of the NFT to burn. + * @param _amount The amount of the NFT to burn. + */ + function burn(address _owner, uint256 _tokenId, uint256 _amount) external virtual { + address caller = msg.sender; + + require(caller == _owner || isApprovedForAll[_owner][caller], "Unapproved caller"); + require(balanceOf[_owner][_tokenId] >= _amount, "Not enough tokens owned"); + + _burn(_owner, _tokenId, _amount); + } + + /** + * @notice Lets an owner or approved operator burn NFTs of the given tokenIds. + * + * @param _owner The owner of the NFTs to burn. + * @param _tokenIds The tokenIds of the NFTs to burn. + * @param _amounts The amounts of the NFTs to burn. + */ + function burnBatch(address _owner, uint256[] memory _tokenIds, uint256[] memory _amounts) external virtual { + address caller = msg.sender; + + require(caller == _owner || isApprovedForAll[_owner][caller], "Unapproved caller"); + require(_tokenIds.length == _amounts.length, "Length mismatch"); + + for (uint256 i = 0; i < _tokenIds.length; i += 1) { + require(balanceOf[_owner][_tokenIds[i]] >= _amounts[i], "Not enough tokens owned"); + } + + _burnBatch(_owner, _tokenIds, _amounts); + } + + /*/////////////////////////////////////////////////////////////// + Overriden metadata logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the metadata URI for an NFT. + * @dev See `BatchMintMetadata` for handling of metadata in this contract. + * + * @param _tokenId The tokenId of an NFT. + * @return The metadata URI for the given NFT. + */ + function uri(uint256 _tokenId) public view virtual override returns (string memory) { + (uint256 batchId, ) = _getBatchId(_tokenId); + string memory batchUri = _getBaseURI(_tokenId); + + if (isEncryptedBatch(batchId)) { + return string(abi.encodePacked(batchUri, "0")); + } else { + return string(abi.encodePacked(batchUri, _tokenId.toString())); + } + } + + /*/////////////////////////////////////////////////////////////// + Delayed reveal logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Lets an authorized address reveal a batch of delayed reveal NFTs. + * + * @param _index The ID for the batch of delayed-reveal NFTs to reveal. + * @param _key The key with which the base URI for the relevant batch of NFTs was encrypted. + * @return revealedURI The revealed URI for the batch of NFTs. + */ + function reveal(uint256 _index, bytes calldata _key) public virtual override returns (string memory revealedURI) { + require(_canReveal(), "Not authorized"); + + uint256 batchId = getBatchIdAtIndex(_index); + revealedURI = getRevealURI(batchId, _key); + + _setEncryptedData(batchId, ""); + _setBaseURI(batchId, revealedURI); + + emit TokenURIRevealed(_index, revealedURI); + } + + /*/////////////////////////////////////////////////////////////// + Overriden lazy minting logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Lets an authorized address lazy mint a given amount of NFTs. + * + * @param _amount The number of NFTs to lazy mint. + * @param _baseURIForTokens The placeholder base URI for the 'n' number of NFTs being lazy minted, where the + * metadata for each of those NFTs is `${baseURIForTokens}/${tokenId}`. + * @param _data The encrypted base URI + provenance hash for the batch of NFTs being lazy minted. + * @return batchId A unique integer identifier for the batch of NFTs lazy minted together. + */ + function lazyMint( + uint256 _amount, + string calldata _baseURIForTokens, + bytes calldata _data + ) public virtual override returns (uint256 batchId) { + if (_data.length > 0) { + (bytes memory encryptedURI, bytes32 provenanceHash) = abi.decode(_data, (bytes, bytes32)); + if (encryptedURI.length != 0 && provenanceHash != "") { + _setEncryptedData(nextTokenIdToLazyMint + _amount, _data); + } + } + + return LazyMint.lazyMint(_amount, _baseURIForTokens, _data); + } + + /// @notice The tokenId assigned to the next new NFT to be lazy minted. + function nextTokenIdToMint() public view virtual returns (uint256) { + return nextTokenIdToLazyMint; + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Runs before every `claim` function call. + * + * @param _tokenId The tokenId of the NFT being claimed. + */ + function _beforeClaim( + uint256 _tokenId, + address, + uint256, + address, + uint256, + AllowlistProof calldata, + bytes memory + ) internal view virtual override { + if (_tokenId >= nextTokenIdToLazyMint) { + revert("Not enough minted tokens"); + } + } + + /** + * @dev Collects and distributes the primary sale value of NFTs being claimed. + * + * @param _primarySaleRecipient The address to which primary sale revenue should be sent. + * @param _quantityToClaim The quantity of NFTs being claimed. + * @param _currency The currency in which the NFTs are being sold. + * @param _pricePerToken The price per NFT being claimed. + */ + + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal virtual override { + if (_pricePerToken == 0) { + require(msg.value == 0, "!Value"); + return; + } + + uint256 totalPrice = _quantityToClaim * _pricePerToken; + + bool validMsgValue; + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { + validMsgValue = msg.value == totalPrice; + } else { + validMsgValue = msg.value == 0; + } + require(validMsgValue, "Invalid msg value"); + + address saleRecipient = _primarySaleRecipient == address(0) ? primarySaleRecipient() : _primarySaleRecipient; + CurrencyTransferLib.transferCurrency(_currency, msg.sender, saleRecipient, totalPrice); + } + + /** + * @dev Transfers the NFTs being claimed. + * + * @param _to The address to which the NFTs are being transferred. + * @param _tokenId The tokenId of the NFTs being claimed. + * @param _quantityBeingClaimed The quantity of NFTs being claimed. + */ + function _transferTokensOnClaim( + address _to, + uint256 _tokenId, + uint256 _quantityBeingClaimed + ) internal virtual override { + _mint(_to, _tokenId, _quantityBeingClaimed, ""); + } + + /** + * @dev Runs before every token transfer / mint / burn. + * + * @param operator The address performing the token transfer. + * @param from The address from which the token is being transferred. + * @param to The address to which the token is being transferred. + * @param ids The tokenIds of the tokens being transferred. + * @param amounts The amounts of the tokens being transferred. + * @param data Any additional data being passed in the token transfer. + */ + function _beforeTokenTransfer( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal virtual override { + super._beforeTokenTransfer(operator, from, to, ids, amounts, data); + + if (from == address(0)) { + for (uint256 i = 0; i < ids.length; ++i) { + totalSupply[ids[i]] += amounts[i]; + } + } + + if (to == address(0)) { + for (uint256 i = 0; i < ids.length; ++i) { + totalSupply[ids[i]] -= amounts[i]; + } + } + } + + /// @dev Checks whether primary sale recipient can be set in the given execution context. + function _canSetPrimarySaleRecipient() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Checks whether owner can be set in the given execution context. + function _canSetOwner() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Checks whether royalty info can be set in the given execution context. + function _canSetRoyaltyInfo() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Checks whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function _canSetClaimConditions() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether lazy minting can be done in the given execution context. + function _canLazyMint() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Checks whether NFTs can be revealed in the given execution context. + function _canReveal() internal view virtual returns (bool) { + return msg.sender == owner(); + } +} diff --git a/contracts/base/ERC1155LazyMint.sol b/contracts/base/ERC1155LazyMint.sol new file mode 100644 index 000000000..93fb0c2e2 --- /dev/null +++ b/contracts/base/ERC1155LazyMint.sol @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import { ERC1155 } from "../eip/ERC1155.sol"; + +import "../extension/ContractMetadata.sol"; +import "../extension/Multicall.sol"; +import "../extension/Ownable.sol"; +import "../extension/Royalty.sol"; +import "../extension/BatchMintMetadata.sol"; +import "../extension/LazyMint.sol"; +import "../extension/interface/IClaimableERC1155.sol"; + +import "../lib/Strings.sol"; +import "../external-deps/openzeppelin/security/ReentrancyGuard.sol"; + +/** + * BASE: ERC1155Base + * EXTENSION: LazyMint + * + * The `ERC1155LazyMint` smart contract implements the ERC1155 NFT standard. + * It includes the following additions to standard ERC1155 logic: + * + * - Lazy minting + * + * - Ability to mint NFTs via the provided `mintTo` and `batchMintTo` functions. + * + * - Contract metadata for royalty support on platforms such as OpenSea that use + * off-chain information to distribute roaylties. + * + * - Ownership of the contract, with the ability to restrict certain functions to + * only be called by the contract's owner. + * + * - Multicall capability to perform multiple actions atomically + * + * - EIP 2981 compliance for royalty support on NFT marketplaces. + * + * + * The `ERC1155LazyMint` contract uses the `LazyMint` extension. + * + * 'Lazy minting' means defining the metadata of NFTs without minting it to an address. Regular 'minting' + * of NFTs means actually assigning an owner to an NFT. + * + * As a contract admin, this lets you prepare the metadata for NFTs that will be minted by an external party, + * without paying the gas cost for actually minting the NFTs. + * + */ + +contract ERC1155LazyMint is + ERC1155, + ContractMetadata, + Ownable, + Royalty, + Multicall, + BatchMintMetadata, + LazyMint, + IClaimableERC1155, + ReentrancyGuard +{ + using Strings for uint256; + + /*////////////////////////////////////////////////////////////// + Mappings + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the total supply of NFTs of a given tokenId + * @dev Mapping from tokenId => total circulating supply of NFTs of that tokenId. + */ + mapping(uint256 => uint256) public totalSupply; + + /*////////////////////////////////////////////////////////////// + Constructor + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Initializes the contract during construction. + * + * @param _defaultAdmin The default admin of the contract. + * @param _name The name of the contract. + * @param _symbol The symbol of the contract. + * @param _royaltyRecipient The address to receive royalties. + * @param _royaltyBps The royalty basis points to be charged. Max = 10000 (10000 = 100%, 1000 = 10%) + */ + constructor( + address _defaultAdmin, + string memory _name, + string memory _symbol, + address _royaltyRecipient, + uint128 _royaltyBps + ) ERC1155(_name, _symbol) { + _setupOwner(_defaultAdmin); + _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + } + + /*////////////////////////////////////////////////////////////// + Overriden metadata logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the metadata URI for the given tokenId. + * + * @param _tokenId The tokenId of the NFT. + * @return The metadata URI for the given tokenId. + */ + function uri(uint256 _tokenId) public view virtual override returns (string memory) { + string memory batchUri = _getBaseURI(_tokenId); + return string(abi.encodePacked(batchUri, _tokenId.toString())); + } + + /*////////////////////////////////////////////////////////////// + CLAIM LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Lets an address claim multiple lazy minted NFTs at once to a recipient. + * This function prevents any reentrant calls, and is not allowed to be overridden. + * + * Contract creators should override `verifyClaim` and `transferTokensOnClaim` + * functions to create custom logic for verification and claiming, + * for e.g. price collection, allowlist, max quantity, etc. + * + * @dev The logic in `verifyClaim` determines whether the caller is authorized to mint NFTs. + * The logic in `transferTokensOnClaim` does actual minting of tokens, + * can also be used to apply other state changes. + * + * @param _receiver The recipient of the tokens to mint. + * @param _tokenId The tokenId of the lazy minted NFT to mint. + * @param _quantity The number of tokens to mint. + */ + function claim(address _receiver, uint256 _tokenId, uint256 _quantity) public payable virtual nonReentrant { + require(_tokenId < nextTokenIdToMint(), "invalid id"); + verifyClaim(msg.sender, _tokenId, _quantity); // Add your claim verification logic by overriding this function. + + _transferTokensOnClaim(_receiver, _tokenId, _quantity); // Mints tokens. Apply any state updates by overriding this function. + emit TokensClaimed(msg.sender, _receiver, _tokenId, _quantity); + } + + /** + * @notice Override this function to add logic for claim verification, based on conditions + * such as allowlist, price, max quantity etc. + * + * @dev Checks a request to claim NFTs against a custom condition. + * + * @param _claimer Caller of the claim function. + * @param _tokenId The tokenId of the lazy minted NFT to mint. + * @param _quantity The number of NFTs being claimed. + */ + function verifyClaim(address _claimer, uint256 _tokenId, uint256 _quantity) public view virtual {} + + /** + * @notice Lets an owner or approved operator burn NFTs of the given tokenId. + * + * @param _owner The owner of the NFT to burn. + * @param _tokenId The tokenId of the NFT to burn. + * @param _amount The amount of the NFT to burn. + */ + function burn(address _owner, uint256 _tokenId, uint256 _amount) external virtual { + address caller = msg.sender; + + require(caller == _owner || isApprovedForAll[_owner][caller], "Unapproved caller"); + require(balanceOf[_owner][_tokenId] >= _amount, "Not enough tokens owned"); + + _burn(_owner, _tokenId, _amount); + } + + /** + * @notice Lets an owner or approved operator burn NFTs of the given tokenIds. + * + * @param _owner The owner of the NFTs to burn. + * @param _tokenIds The tokenIds of the NFTs to burn. + * @param _amounts The amounts of the NFTs to burn. + */ + function burnBatch(address _owner, uint256[] memory _tokenIds, uint256[] memory _amounts) external virtual { + address caller = msg.sender; + + require(caller == _owner || isApprovedForAll[_owner][caller], "Unapproved caller"); + require(_tokenIds.length == _amounts.length, "Length mismatch"); + + for (uint256 i = 0; i < _tokenIds.length; i += 1) { + require(balanceOf[_owner][_tokenIds[i]] >= _amounts[i], "Not enough tokens owned"); + } + + _burnBatch(_owner, _tokenIds, _amounts); + } + + /*////////////////////////////////////////////////////////////// + ERC165 Logic + //////////////////////////////////////////////////////////////*/ + + /** + * @dev See ERC165: https://eips.ethereum.org/EIPS/eip-165 + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC1155, IERC165) returns (bool) { + return + interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 + interfaceId == 0xd9b67a26 || // ERC165 Interface ID for ERC1155 + interfaceId == 0x0e89341c || // ERC165 Interface ID for ERC1155MetadataURI + interfaceId == type(IERC2981).interfaceId; // ERC165 ID for ERC2981 + } + + /*////////////////////////////////////////////////////////////// + View functions + //////////////////////////////////////////////////////////////*/ + + /// @notice The tokenId assigned to the next new NFT to be lazy minted. + function nextTokenIdToMint() public view virtual returns (uint256) { + return nextTokenIdToLazyMint; + } + + /*////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Mints tokens to receiver on claim. + * Any state changes related to `claim` must be applied + * here by overriding this function. + * + * @dev Override this function to add logic for state updation. + * When overriding, apply any state changes before `_mint`. + * + * @param _receiver The receiver of the tokens to mint. + * @param _tokenId The tokenId of the lazy minted NFT to mint. + * @param _quantity The number of tokens to mint. + */ + function _transferTokensOnClaim(address _receiver, uint256 _tokenId, uint256 _quantity) internal virtual { + _mint(_receiver, _tokenId, _quantity, ""); + } + + /// @dev Returns whether lazy minting can be done in the given execution context. + function _canLazyMint() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether owner can be set in the given execution context. + function _canSetOwner() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether royalty info can be set in the given execution context. + function _canSetRoyaltyInfo() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /** + * @dev Runs before every token transfer / mint / burn. + * + * @param operator The address performing the token transfer. + * @param from The address from which the token is being transferred. + * @param to The address to which the token is being transferred. + * @param ids The tokenIds of the tokens being transferred. + * @param amounts The amounts of the tokens being transferred. + * @param data Any additional data being passed in the token transfer. + */ + function _beforeTokenTransfer( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal virtual override { + super._beforeTokenTransfer(operator, from, to, ids, amounts, data); + + if (from == address(0)) { + for (uint256 i = 0; i < ids.length; ++i) { + totalSupply[ids[i]] += amounts[i]; + } + } + + if (to == address(0)) { + for (uint256 i = 0; i < ids.length; ++i) { + totalSupply[ids[i]] -= amounts[i]; + } + } + } +} diff --git a/contracts/base/ERC1155SignatureMint.sol b/contracts/base/ERC1155SignatureMint.sol new file mode 100644 index 000000000..b7a74af24 --- /dev/null +++ b/contracts/base/ERC1155SignatureMint.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./ERC1155Base.sol"; + +import "../extension/PrimarySale.sol"; +import "../extension/SignatureMintERC1155.sol"; +import { ReentrancyGuard } from "../extension/upgradeable/ReentrancyGuard.sol"; +import { CurrencyTransferLib } from "../lib/CurrencyTransferLib.sol"; + +/** + * BASE: ERC1155Base + * EXTENSION: SignatureMintERC1155 + * + * The `ERC1155SignatureMint` contract uses the `ERC1155Base` contract, along with the `SignatureMintERC1155` extension. + * + * The 'signature minting' mechanism in the `SignatureMintERC1155` extension uses EIP 712, and is a way for a contract + * admin to authorize an external party's request to mint tokens on the admin's contract. At a high level, this means + * you can authorize some external party to mint tokens on your contract, and specify what exactly will be minted by + * that external party. + * + */ + +contract ERC1155SignatureMint is ERC1155Base, PrimarySale, SignatureMintERC1155, ReentrancyGuard { + /*////////////////////////////////////////////////////////////// + Constructor + //////////////////////////////////////////////////////////////*/ + + constructor( + address _defaultAdmin, + string memory _name, + string memory _symbol, + address _royaltyRecipient, + uint128 _royaltyBps, + address _primarySaleRecipient + ) ERC1155Base(_defaultAdmin, _name, _symbol, _royaltyRecipient, _royaltyBps) { + _setupPrimarySaleRecipient(_primarySaleRecipient); + } + + /*////////////////////////////////////////////////////////////// + Signature minting logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Mints tokens according to the provided mint request. + * + * @param _req The payload / mint request. + * @param _signature The signature produced by an account signing the mint request. + */ + function mintWithSignature( + MintRequest calldata _req, + bytes calldata _signature + ) external payable virtual override nonReentrant returns (address signer) { + require(_req.quantity > 0, "Minting zero tokens."); + + uint256 tokenIdToMint; + uint256 nextIdToMint = nextTokenIdToMint(); + + if (_req.tokenId == type(uint256).max) { + tokenIdToMint = nextIdToMint; + nextTokenIdToMint_ += 1; + } else { + require(_req.tokenId < nextIdToMint, "invalid id"); + tokenIdToMint = _req.tokenId; + } + + // Verify and process payload. + signer = _processRequest(_req, _signature); + + address receiver = _req.to; + + // Collect price + _collectPriceOnClaim(_req.primarySaleRecipient, _req.quantity, _req.currency, _req.pricePerToken); + + // Set royalties, if applicable. + if (_req.royaltyRecipient != address(0)) { + _setupRoyaltyInfoForToken(tokenIdToMint, _req.royaltyRecipient, _req.royaltyBps); + } + + // Set URI + if (_req.tokenId == type(uint256).max) { + _setTokenURI(tokenIdToMint, _req.uri); + } + + // Mint tokens. + _mint(receiver, tokenIdToMint, _req.quantity, ""); + + emit TokensMintedWithSignature(signer, receiver, tokenIdToMint, _req); + } + + /*////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns whether a given address is authorized to sign mint requests. + function _canSignMintRequest(address _signer) internal view virtual override returns (bool) { + return _signer == owner(); + } + + /// @dev Returns whether primary sale recipient can be set in the given execution context. + function _canSetPrimarySaleRecipient() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Collects and distributes the primary sale value of NFTs being claimed. + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal virtual { + if (_pricePerToken == 0) { + require(msg.value == 0, "!Value"); + return; + } + + uint256 totalPrice = _quantityToClaim * _pricePerToken; + + bool validMsgValue; + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { + validMsgValue = msg.value == totalPrice; + } else { + validMsgValue = msg.value == 0; + } + require(validMsgValue, "Invalid msg value"); + + address saleRecipient = _primarySaleRecipient == address(0) ? primarySaleRecipient() : _primarySaleRecipient; + CurrencyTransferLib.transferCurrency(_currency, msg.sender, saleRecipient, totalPrice); + } +} diff --git a/contracts/base/ERC20Base.sol b/contracts/base/ERC20Base.sol new file mode 100644 index 000000000..31d05a54d --- /dev/null +++ b/contracts/base/ERC20Base.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../external-deps/openzeppelin/token/ERC20/extensions/ERC20Permit.sol"; + +import "../extension/ContractMetadata.sol"; +import "../extension/Multicall.sol"; +import "../extension/Ownable.sol"; +import "../extension/interface/IMintableERC20.sol"; +import "../extension/interface/IBurnableERC20.sol"; + +/** + * The `ERC20Base` smart contract implements the ERC20 standard. + * It includes the following additions to standard ERC20 logic: + * + * - Ability to mint & burn tokens via the provided `mint` & `burn` functions. + * + * - Ownership of the contract, with the ability to restrict certain functions to + * only be called by the contract's owner. + * + * - Multicall capability to perform multiple actions atomically + * + * - EIP 2612 compliance: See {ERC20-permit} method, which can be used to change an account's ERC20 allowance by + * presenting a message signed by the account. + */ + +contract ERC20Base is ContractMetadata, Multicall, Ownable, ERC20Permit, IMintableERC20, IBurnableERC20 { + /*////////////////////////////////////////////////////////////// + Constructor + //////////////////////////////////////////////////////////////*/ + + constructor(address _defaultAdmin, string memory _name, string memory _symbol) ERC20Permit(_name, _symbol) { + _setupOwner(_defaultAdmin); + } + + /*////////////////////////////////////////////////////////////// + Minting logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Lets an authorized address mint tokens to a recipient. + * @dev The logic in the `_canMint` function determines whether the caller is authorized to mint tokens. + * + * @param _to The recipient of the tokens to mint. + * @param _amount Quantity of tokens to mint. + */ + function mintTo(address _to, uint256 _amount) public virtual { + require(_canMint(), "Not authorized to mint."); + require(_amount != 0, "Minting zero tokens."); + + _mint(_to, _amount); + } + + /** + * @notice Lets an owner a given amount of their tokens. + * @dev Caller should own the `_amount` of tokens. + * + * @param _amount The number of tokens to burn. + */ + function burn(uint256 _amount) external virtual { + require(balanceOf(msg.sender) >= _amount, "not enough balance"); + _burn(msg.sender, _amount); + } + + /** + * @notice Lets an owner burn a given amount of an account's tokens. + * @dev `_account` should own the `_amount` of tokens. + * + * @param _account The account to burn tokens from. + * @param _amount The number of tokens to burn. + */ + function burnFrom(address _account, uint256 _amount) external virtual override { + require(_canBurn(), "Not authorized to burn."); + require(balanceOf(_account) >= _amount, "not enough balance"); + uint256 decreasedAllowance = allowance(_account, msg.sender) - _amount; + _approve(_account, msg.sender, 0); + _approve(_account, msg.sender, decreasedAllowance); + _burn(_account, _amount); + } + + /*////////////////////////////////////////////////////////////// + Internal (overrideable) functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether tokens can be minted in the given execution context. + function _canMint() internal view virtual returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether tokens can be burned in the given execution context. + function _canBurn() internal view virtual returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether owner can be set in the given execution context. + function _canSetOwner() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @notice Returns the sender in the given execution context. + function _msgSender() internal view override(Multicall, Context) returns (address) { + return msg.sender; + } +} diff --git a/contracts/base/ERC20Drop.sol b/contracts/base/ERC20Drop.sol new file mode 100644 index 000000000..60c61925a --- /dev/null +++ b/contracts/base/ERC20Drop.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../external-deps/openzeppelin/token/ERC20/extensions/ERC20Permit.sol"; + +import "../extension/ContractMetadata.sol"; +import "../extension/Multicall.sol"; +import "../extension/Ownable.sol"; +import "../extension/PrimarySale.sol"; +import "../extension/DropSinglePhase.sol"; +import "../extension/interface/IBurnableERC20.sol"; + +import { CurrencyTransferLib } from "../lib/CurrencyTransferLib.sol"; + +/** + * BASE: ERC20 + * EXTENSION: DropSinglePhase + * + * The `ERC20Drop` smart contract implements the ERC20 standard. + * It includes the following additions to standard ERC20 logic: + * + * - Ownership of the contract, with the ability to restrict certain functions to + * only be called by the contract's owner. + * + * - Multicall capability to perform multiple actions atomically + * + * - EIP 2612 compliance: See {ERC20-permit} method, which can be used to change an account's ERC20 allowance by + * presenting a message signed by the account. + * + * The `drop` mechanism in the `DropSinglePhase` extension is a distribution mechanism for tokens. It lets + * you set restrictions such as a price to charge, an allowlist etc. when an address atttempts to mint tokens. + * + */ + +contract ERC20Drop is ContractMetadata, Multicall, Ownable, ERC20Permit, PrimarySale, DropSinglePhase, IBurnableERC20 { + /*////////////////////////////////////////////////////////////// + Constructor + //////////////////////////////////////////////////////////////*/ + + constructor( + address _defaultAdmin, + string memory _name, + string memory _symbol, + address _primarySaleRecipient + ) ERC20Permit(_name, _symbol) { + _setupOwner(_defaultAdmin); + _setupPrimarySaleRecipient(_primarySaleRecipient); + } + + /*////////////////////////////////////////////////////////////// + ERC20 logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Lets an owner a given amount of their tokens. + * @dev Caller should own the `_amount` of tokens. + * + * @param _amount The number of tokens to burn. + */ + function burn(uint256 _amount) external virtual { + require(balanceOf(msg.sender) >= _amount, "not enough balance"); + _burn(msg.sender, _amount); + } + + /** + * @notice Lets an owner burn a given amount of an account's tokens. + * @dev `_account` should own the `_amount` of tokens. + * + * @param _account The account to burn tokens from. + * @param _amount The number of tokens to burn. + */ + function burnFrom(address _account, uint256 _amount) external virtual override { + require(_canBurn(), "Not authorized to burn."); + require(balanceOf(_account) >= _amount, "not enough balance"); + uint256 decreasedAllowance = allowance(_account, msg.sender) - _amount; + _approve(_account, msg.sender, 0); + _approve(_account, msg.sender, decreasedAllowance); + _burn(_account, _amount); + } + + /*////////////////////////////////////////////////////////////// + Internal (overrideable) functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Collects and distributes the primary sale value of tokens being claimed. + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal virtual override { + if (_pricePerToken == 0) { + require(msg.value == 0, "!Value"); + return; + } + + uint256 totalPrice = (_quantityToClaim * _pricePerToken) / 1 ether; + require(totalPrice > 0, "quantity too low"); + + bool validMsgValue; + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { + validMsgValue = msg.value == totalPrice; + } else { + validMsgValue = msg.value == 0; + } + require(validMsgValue, "Invalid msg value"); + + address saleRecipient = _primarySaleRecipient == address(0) ? primarySaleRecipient() : _primarySaleRecipient; + CurrencyTransferLib.transferCurrency(_currency, msg.sender, saleRecipient, totalPrice); + } + + /// @dev Transfers the tokens being claimed. + function _transferTokensOnClaim( + address _to, + uint256 _quantityBeingClaimed + ) internal virtual override returns (uint256) { + _mint(_to, _quantityBeingClaimed); + return 0; + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function _canSetClaimConditions() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether tokens can be minted in the given execution context. + function _canMint() internal view virtual returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether tokens can be burned in the given execution context. + function _canBurn() internal view virtual returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether owner can be set in the given execution context. + function _canSetOwner() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether primary sale recipient can be set in the given execution context. + function _canSetPrimarySaleRecipient() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @notice Returns the sender in the given execution context. + function _msgSender() internal view override(Multicall, Context) returns (address) { + return msg.sender; + } +} diff --git a/contracts/base/ERC20DropVote.sol b/contracts/base/ERC20DropVote.sol new file mode 100644 index 000000000..4150ac0e1 --- /dev/null +++ b/contracts/base/ERC20DropVote.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../external-deps/openzeppelin/token/ERC20/extensions/ERC20Votes.sol"; + +import "../extension/ContractMetadata.sol"; +import "../extension/Multicall.sol"; +import "../extension/Ownable.sol"; +import "../extension/PrimarySale.sol"; +import "../extension/DropSinglePhase.sol"; + +import { CurrencyTransferLib } from "../lib/CurrencyTransferLib.sol"; + +/** + * BASE: ERC20Votes + * EXTENSION: DropSinglePhase + * + * The `ERC20Drop` contract uses the `DropSinglePhase` extensions, along with `ERC20Votes`. + * It implements the ERC20 standard, along with the following additions to standard ERC20 logic: + * + * - Ownership of the contract, with the ability to restrict certain functions to + * only be called by the contract's owner. + * + * - Multicall capability to perform multiple actions atomically + * + * - EIP 2612 compliance: See {ERC20-permit} method, which can be used to change an account's ERC20 allowance by + * presenting a message signed by the account. + * + * The `drop` mechanism in the `DropSinglePhase` extension is a distribution mechanism tokens. It lets + * you set restrictions such as a price to charge, an allowlist etc. when an address atttempts to mint tokens. + * + */ + +contract ERC20DropVote is ContractMetadata, Multicall, Ownable, ERC20Votes, PrimarySale, DropSinglePhase { + /*////////////////////////////////////////////////////////////// + Constructor + //////////////////////////////////////////////////////////////*/ + + constructor( + address _defaultAdmin, + string memory _name, + string memory _symbol, + address _primarySaleRecipient + ) ERC20Permit(_name, _symbol) { + _setupOwner(_defaultAdmin); + _setupPrimarySaleRecipient(_primarySaleRecipient); + } + + /*////////////////////////////////////////////////////////////// + ERC20 logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Lets an owner a given amount of their tokens. + * @dev Caller should own the `_amount` of tokens. + * + * @param _amount The number of tokens to burn. + */ + function burn(uint256 _amount) external virtual { + require(balanceOf(msg.sender) >= _amount, "not enough balance"); + _burn(msg.sender, _amount); + } + + /*////////////////////////////////////////////////////////////// + Internal (overrideable) functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Collects and distributes the primary sale value of tokens being claimed. + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal virtual override { + if (_pricePerToken == 0) { + require(msg.value == 0, "!Value"); + return; + } + + uint256 totalPrice = (_quantityToClaim * _pricePerToken) / 1 ether; + require(totalPrice > 0, "quantity too low"); + + bool validMsgValue; + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { + validMsgValue = msg.value == totalPrice; + } else { + validMsgValue = msg.value == 0; + } + require(validMsgValue, "Invalid msg value"); + + address saleRecipient = _primarySaleRecipient == address(0) ? primarySaleRecipient() : _primarySaleRecipient; + CurrencyTransferLib.transferCurrency(_currency, msg.sender, saleRecipient, totalPrice); + } + + /// @dev Transfers the tokens being claimed. + function _transferTokensOnClaim( + address _to, + uint256 _quantityBeingClaimed + ) internal virtual override returns (uint256) { + _mint(_to, _quantityBeingClaimed); + return 0; + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function _canSetClaimConditions() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether tokens can be minted in the given execution context. + function _canMint() internal view virtual returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether owner can be set in the given execution context. + function _canSetOwner() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether primary sale recipient can be set in the given execution context. + function _canSetPrimarySaleRecipient() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @notice Returns the sender in the given execution context. + function _msgSender() internal view override(Multicall, Context) returns (address) { + return msg.sender; + } +} diff --git a/contracts/base/ERC20SignatureMint.sol b/contracts/base/ERC20SignatureMint.sol new file mode 100644 index 000000000..6090aabd1 --- /dev/null +++ b/contracts/base/ERC20SignatureMint.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./ERC20Base.sol"; + +import "../extension/PrimarySale.sol"; +import { SignatureMintERC20 } from "../extension/SignatureMintERC20.sol"; +import { ReentrancyGuard } from "../extension/upgradeable/ReentrancyGuard.sol"; +import { CurrencyTransferLib } from "../lib/CurrencyTransferLib.sol"; + +/** + * BASE: ERC20 + * EXTENSION: SignatureMintERC20 + * + * The `ERC20SignatureMint` contract uses the `ERC20Base` contract, along with the `SignatureMintERC20` extension. + * + * The 'signature minting' mechanism in the `SignatureMintERC20` extension uses EIP 712, and is a way for a contract + * admin to authorize an external party's request to mint tokens on the admin's contract. At a high level, this means + * you can authorize some external party to mint tokens on your contract, and specify what exactly will be minted by + * that external party. + * + */ + +contract ERC20SignatureMint is ERC20Base, PrimarySale, SignatureMintERC20, ReentrancyGuard { + /*////////////////////////////////////////////////////////////// + Constructor + //////////////////////////////////////////////////////////////*/ + + constructor( + address _defaultAdmin, + string memory _name, + string memory _symbol, + address _primarySaleRecipient + ) ERC20Base(_defaultAdmin, _name, _symbol) { + _setupPrimarySaleRecipient(_primarySaleRecipient); + } + + /*////////////////////////////////////////////////////////////// + Signature minting logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Mints tokens according to the provided mint request. + * + * @param _req The payload / mint request. + * @param _signature The signature produced by an account signing the mint request. + */ + function mintWithSignature( + MintRequest calldata _req, + bytes calldata _signature + ) external payable virtual nonReentrant returns (address signer) { + require(_req.quantity > 0, "Minting zero tokens."); + + // Verify and process payload. + signer = _processRequest(_req, _signature); + + address receiver = _req.to; + + // Collect price + _collectPriceOnClaim(_req.primarySaleRecipient, _req.currency, _req.price); + + // Mint tokens. + _mint(receiver, _req.quantity); + + emit TokensMintedWithSignature(signer, receiver, _req); + } + + /*////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns whether a given address is authorized to sign mint requests. + function _canSignMintRequest(address _signer) internal view virtual override returns (bool) { + return _signer == owner(); + } + + /// @dev Returns whether primary sale recipient can be set in the given execution context. + function _canSetPrimarySaleRecipient() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Collects and distributes the primary sale value of tokens being claimed. + function _collectPriceOnClaim(address _primarySaleRecipient, address _currency, uint256 _price) internal virtual { + if (_price == 0) { + require(msg.value == 0, "!Value"); + return; + } + + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { + require(msg.value == _price, "Must send total price."); + } else { + require(msg.value == 0, "msg value not zero"); + } + + address saleRecipient = _primarySaleRecipient == address(0) ? primarySaleRecipient() : _primarySaleRecipient; + CurrencyTransferLib.transferCurrency(_currency, msg.sender, saleRecipient, _price); + } +} diff --git a/contracts/base/ERC20SignatureMintVote.sol b/contracts/base/ERC20SignatureMintVote.sol new file mode 100644 index 000000000..f0d03542e --- /dev/null +++ b/contracts/base/ERC20SignatureMintVote.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./ERC20Vote.sol"; + +import "../extension/PrimarySale.sol"; +import { SignatureMintERC20 } from "../extension/SignatureMintERC20.sol"; +import { ReentrancyGuard } from "../extension/upgradeable/ReentrancyGuard.sol"; +import { CurrencyTransferLib } from "../lib/CurrencyTransferLib.sol"; + +/** + * BASE: ERC20Vote + * EXTENSION: SignatureMintERC20 + * + * The `ERC20SignatureMint` contract uses the `ERC20Vote` contract, along with the `SignatureMintERC20` extension. + * + * The 'signature minting' mechanism in the `SignatureMintERC20` extension uses EIP 712, and is a way for a contract + * admin to authorize an external party's request to mint tokens on the admin's contract. At a high level, this means + * you can authorize some external party to mint tokens on your contract, and specify what exactly will be minted by + * that external party. + * + */ + +contract ERC20SignatureMintVote is ERC20Vote, PrimarySale, SignatureMintERC20, ReentrancyGuard { + /*////////////////////////////////////////////////////////////// + Constructor + //////////////////////////////////////////////////////////////*/ + + constructor( + address _defaultAdmin, + string memory _name, + string memory _symbol, + address _primarySaleRecipient + ) ERC20Vote(_defaultAdmin, _name, _symbol) { + _setupPrimarySaleRecipient(_primarySaleRecipient); + } + + /*////////////////////////////////////////////////////////////// + Signature minting logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Mints tokens according to the provided mint request. + * + * @param _req The payload / mint request. + * @param _signature The signature produced by an account signing the mint request. + */ + function mintWithSignature( + MintRequest calldata _req, + bytes calldata _signature + ) external payable virtual nonReentrant returns (address signer) { + require(_req.quantity > 0, "Minting zero tokens."); + + // Verify and process payload. + signer = _processRequest(_req, _signature); + + /** + * Get receiver of tokens. + * + * Note: If `_req.to == address(0)`, a `mintWithSignature` transaction sitting in the + * mempool can be frontrun by copying the input data, since the minted tokens + * will be sent to the `_msgSender()` in this case. + */ + address receiver = _req.to == address(0) ? msg.sender : _req.to; + + // Collect price + _collectPriceOnClaim(_req.primarySaleRecipient, _req.currency, _req.price); + + // Mint tokens. + _mint(receiver, _req.quantity); + + emit TokensMintedWithSignature(signer, receiver, _req); + } + + /*////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns whether a given address is authorized to sign mint requests. + function _canSignMintRequest(address _signer) internal view virtual override returns (bool) { + return _signer == owner(); + } + + /// @dev Returns whether primary sale recipient can be set in the given execution context. + function _canSetPrimarySaleRecipient() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Collects and distributes the primary sale value of tokens being claimed. + function _collectPriceOnClaim(address _primarySaleRecipient, address _currency, uint256 _price) internal virtual { + if (_price == 0) { + require(msg.value == 0, "!Value"); + return; + } + + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { + require(msg.value == _price, "Must send total price."); + } else { + require(msg.value == 0, "msg value not zero"); + } + + address saleRecipient = _primarySaleRecipient == address(0) ? primarySaleRecipient() : _primarySaleRecipient; + CurrencyTransferLib.transferCurrency(_currency, msg.sender, saleRecipient, _price); + } +} diff --git a/contracts/base/ERC20Vote.sol b/contracts/base/ERC20Vote.sol new file mode 100644 index 000000000..79626a830 --- /dev/null +++ b/contracts/base/ERC20Vote.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../external-deps/openzeppelin/token/ERC20/extensions/ERC20Votes.sol"; + +import "./ERC20Base.sol"; +import "../extension/interface/IMintableERC20.sol"; +import "../extension/interface/IBurnableERC20.sol"; + +/** + * The `ERC20Vote` smart contract implements the ERC20 standard and ERC20Votes. + * It includes the following additions to standard ERC20 logic: + * + * - Ability to mint & burn tokens via the provided `mint` & `burn` functions. + * + * - Ownership of the contract, with the ability to restrict certain functions to + * only be called by the contract's owner. + * + * - Multicall capability to perform multiple actions atomically + * + * - Extension of ERC20 to support voting and delegation. + * + * - EIP 2612 compliance: See {ERC20-permit} method, which can be used to change an account's ERC20 allowance by + * presenting a message signed by the account. + */ + +contract ERC20Vote is ContractMetadata, Multicall, Ownable, ERC20Votes, IMintableERC20, IBurnableERC20 { + /*////////////////////////////////////////////////////////////// + Constructor + //////////////////////////////////////////////////////////////*/ + + constructor(address _defaultAdmin, string memory _name, string memory _symbol) ERC20Permit(_name, _symbol) { + _setupOwner(_defaultAdmin); + } + + /*////////////////////////////////////////////////////////////// + Minting logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Lets an authorized address mint tokens to a recipient. + * @dev The logic in the `_canMint` function determines whether the caller is authorized to mint tokens. + * + * @param _to The recipient of the tokens to mint. + * @param _amount Quantity of tokens to mint. + */ + function mintTo(address _to, uint256 _amount) public virtual { + require(_canMint(), "Not authorized to mint."); + require(_amount != 0, "Minting zero tokens."); + + _mint(_to, _amount); + } + + /** + * @notice Lets an owner a given amount of their tokens. + * @dev Caller should own the `_amount` of tokens. + * + * @param _amount The number of tokens to burn. + */ + function burn(uint256 _amount) external virtual { + require(balanceOf(_msgSender()) >= _amount, "not enough balance"); + _burn(msg.sender, _amount); + } + + /** + * @notice Lets an owner burn a given amount of an account's tokens. + * @dev `_account` should own the `_amount` of tokens. + * + * @param _account The account to burn tokens from. + * @param _amount The number of tokens to burn. + */ + function burnFrom(address _account, uint256 _amount) external virtual override { + require(_canBurn(), "Not authorized to burn."); + require(balanceOf(_account) >= _amount, "not enough balance"); + uint256 decreasedAllowance = allowance(_account, msg.sender) - _amount; + _approve(_account, msg.sender, 0); + _approve(_account, msg.sender, decreasedAllowance); + _burn(_account, _amount); + } + + /*////////////////////////////////////////////////////////////// + Internal (overrideable) functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether tokens can be minted in the given execution context. + function _canMint() internal view virtual returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether tokens can be burned in the given execution context. + function _canBurn() internal view virtual returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether owner can be set in the given execution context. + function _canSetOwner() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @notice Returns the sender in the given execution context. + function _msgSender() internal view override(Multicall, Context) returns (address) { + return msg.sender; + } +} diff --git a/contracts/base/ERC721Base.sol b/contracts/base/ERC721Base.sol index 102e804b0..3d256e3f6 100644 --- a/contracts/base/ERC721Base.sol +++ b/contracts/base/ERC721Base.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; -import { ERC721A } from "../eip/ERC721A.sol"; +/// @author thirdweb + +import "../eip/queryable/ERC721AQueryable.sol"; import "../extension/ContractMetadata.sol"; import "../extension/Multicall.sol"; @@ -9,7 +11,7 @@ import "../extension/Ownable.sol"; import "../extension/Royalty.sol"; import "../extension/BatchMintMetadata.sol"; -import "../lib/TWStrings.sol"; +import "../lib/Strings.sol"; /** * The `ERC721Base` smart contract implements the ERC721 NFT standard, along with the ERC721A optimization to the standard. @@ -28,8 +30,8 @@ import "../lib/TWStrings.sol"; * - EIP 2981 compliance for royalty support on NFT marketplaces. */ -contract ERC721Base is ERC721A, ContractMetadata, Multicall, Ownable, Royalty, BatchMintMetadata { - using TWStrings for uint256; +contract ERC721Base is ERC721AQueryable, ContractMetadata, Multicall, Ownable, Royalty, BatchMintMetadata { + using Strings for uint256; /*////////////////////////////////////////////////////////////// Mappings @@ -41,13 +43,23 @@ contract ERC721Base is ERC721A, ContractMetadata, Multicall, Ownable, Royalty, B Constructor //////////////////////////////////////////////////////////////*/ + /** + * @notice Initializes the contract during construction. + * + * @param _defaultAdmin The default admin of the contract. + * @param _name The name of the contract. + * @param _symbol The symbol of the contract. + * @param _royaltyRecipient The address to receive royalties. + * @param _royaltyBps The royalty basis points to be charged. Max = 10000 (10000 = 100%, 1000 = 10%) + */ constructor( + address _defaultAdmin, string memory _name, string memory _symbol, address _royaltyRecipient, uint128 _royaltyBps ) ERC721A(_name, _symbol) { - _setupOwner(msg.sender); + _setupOwner(_defaultAdmin); _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); } @@ -55,7 +67,10 @@ contract ERC721Base is ERC721A, ContractMetadata, Multicall, Ownable, Royalty, B ERC165 Logic //////////////////////////////////////////////////////////////*/ - /// @dev See ERC165: https://eips.ethereum.org/EIPS/eip-165 + /** + * @dev See ERC165: https://eips.ethereum.org/EIPS/eip-165 + * @inheritdoc IERC165 + */ function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721A, IERC165) returns (bool) { return interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 @@ -74,13 +89,13 @@ contract ERC721Base is ERC721A, ContractMetadata, Multicall, Ownable, Royalty, B * * @param _tokenId The tokenId of an NFT. */ - function tokenURI(uint256 _tokenId) public view virtual override returns (string memory) { + function tokenURI(uint256 _tokenId) public view virtual override(ERC721A, IERC721Metadata) returns (string memory) { string memory fullUriForToken = fullURI[_tokenId]; if (bytes(fullUriForToken).length > 0) { return fullUriForToken; } - string memory batchUri = getBaseURI(_tokenId); + string memory batchUri = _getBaseURI(_tokenId); return string(abi.encodePacked(batchUri, _tokenId.toString())); } @@ -97,7 +112,7 @@ contract ERC721Base is ERC721A, ContractMetadata, Multicall, Ownable, Royalty, B */ function mintTo(address _to, string memory _tokenURI) public virtual { require(_canMint(), "Not authorized to mint."); - fullURI[nextTokenIdToMint()] = _tokenURI; + _setTokenURI(nextTokenIdToMint(), _tokenURI); _safeMint(_to, 1, ""); } @@ -110,12 +125,7 @@ contract ERC721Base is ERC721A, ContractMetadata, Multicall, Ownable, Royalty, B * @param _baseURI The baseURI for the `n` number of NFTs minted. The metadata for each NFT is `baseURI/tokenId` * @param _data Additional data to pass along during the minting of the NFT. */ - function batchMintTo( - address _to, - uint256 _quantity, - string memory _baseURI, - bytes memory _data - ) public virtual { + function batchMintTo(address _to, uint256 _quantity, string memory _baseURI, bytes memory _data) public virtual { require(_canMint(), "Not authorized to mint."); _batchMintMetadata(nextTokenIdToMint(), _quantity, _baseURI); _safeMint(_to, _quantity, _data); @@ -140,10 +150,39 @@ contract ERC721Base is ERC721A, ContractMetadata, Multicall, Ownable, Royalty, B return _currentIndex; } + /** + * @notice Returns whether a given address is the owner, or approved to transfer an NFT. + * + * @param _operator The address to check. + * @param _tokenId The tokenId of the NFT to check. + * + * @return isApprovedOrOwnerOf Whether the given address is approved to transfer the given NFT. + */ + function isApprovedOrOwner( + address _operator, + uint256 _tokenId + ) public view virtual returns (bool isApprovedOrOwnerOf) { + address owner = ownerOf(_tokenId); + isApprovedOrOwnerOf = (_operator == owner || + isApprovedForAll(owner, _operator) || + getApproved(_tokenId) == _operator); + } + /*////////////////////////////////////////////////////////////// Internal (overrideable) functions //////////////////////////////////////////////////////////////*/ + /** + * @notice Sets the metadata URI for a given tokenId. + * + * @param _tokenId The tokenId of the NFT to set the URI for. + * @param _tokenURI The URI to set for the given tokenId. + */ + function _setTokenURI(uint256 _tokenId, string memory _tokenURI) internal virtual { + require(bytes(fullURI[_tokenId]).length == 0, "URI already set"); + fullURI[_tokenId] = _tokenURI; + } + /// @dev Returns whether contract metadata can be set in the given execution context. function _canSetContractURI() internal view virtual override returns (bool) { return msg.sender == owner(); @@ -163,4 +202,9 @@ contract ERC721Base is ERC721A, ContractMetadata, Multicall, Ownable, Royalty, B function _canSetRoyaltyInfo() internal view virtual override returns (bool) { return msg.sender == owner(); } + + /// @notice Returns the sender in the given execution context. + function _msgSender() internal view override(Multicall, Context) returns (address) { + return msg.sender; + } } diff --git a/contracts/base/ERC721DelayedReveal.sol b/contracts/base/ERC721DelayedReveal.sol index ed9f69121..624ec4ecc 100644 --- a/contracts/base/ERC721DelayedReveal.sol +++ b/contracts/base/ERC721DelayedReveal.sol @@ -1,15 +1,17 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + import "./ERC721LazyMint.sol"; import "../extension/DelayedReveal.sol"; /** - * BASE: ERC721A - * EXTENSION: LazyMint, DelayedReveal + * BASE: ERC721LazyMint + * EXTENSION: DelayedReveal * - * The `ERC721DelayedReveal` contract uses the `ERC721Base` contract, along with the `LazyMint` and `DelayedReveal` extension. + * The `ERC721DelayedReveal` contract uses the `ERC721LazyMint` contract, along with `DelayedReveal` extension. * * 'Lazy minting' means defining the metadata of NFTs without minting it to an address. Regular 'minting' * of NFTs means actually assigning an owner to an NFT. @@ -24,18 +26,19 @@ import "../extension/DelayedReveal.sol"; */ contract ERC721DelayedReveal is ERC721LazyMint, DelayedReveal { - using TWStrings for uint256; + using Strings for uint256; /*////////////////////////////////////////////////////////////// Constructor //////////////////////////////////////////////////////////////*/ constructor( + address _defaultAdmin, string memory _name, string memory _symbol, address _royaltyRecipient, uint128 _royaltyBps - ) ERC721LazyMint(_name, _symbol, _royaltyRecipient, _royaltyBps) {} + ) ERC721LazyMint(_defaultAdmin, _name, _symbol, _royaltyRecipient, _royaltyBps) {} /*////////////////////////////////////////////////////////////// Overriden ERC721 logic @@ -47,9 +50,9 @@ contract ERC721DelayedReveal is ERC721LazyMint, DelayedReveal { * * @param _tokenId The tokenId of an NFT. */ - function tokenURI(uint256 _tokenId) public view override returns (string memory) { - uint256 batchId = getBatchId(_tokenId); - string memory batchUri = getBaseURI(_tokenId); + function tokenURI(uint256 _tokenId) public view virtual override returns (string memory) { + (uint256 batchId, ) = _getBatchId(_tokenId); + string memory batchUri = _getBaseURI(_tokenId); if (isEncryptedBatch(batchId)) { return string(abi.encodePacked(batchUri, "0")); @@ -68,19 +71,22 @@ contract ERC721DelayedReveal is ERC721LazyMint, DelayedReveal { * @param _amount The number of NFTs to lazy mint. * @param _baseURIForTokens The placeholder base URI for the 'n' number of NFTs being lazy minted, where the * metadata for each of those NFTs is `${baseURIForTokens}/${tokenId}`. - * @param _encryptedBaseURI The encrypted base URI for the batch of NFTs being lazy minted. + * @param _data The encrypted base URI + provenance hash for the batch of NFTs being lazy minted. * @return batchId A unique integer identifier for the batch of NFTs lazy minted together. */ function lazyMint( uint256 _amount, string calldata _baseURIForTokens, - bytes calldata _encryptedBaseURI + bytes calldata _data ) public virtual override returns (uint256 batchId) { - if (_encryptedBaseURI.length != 0) { - _setEncryptedBaseURI(nextTokenIdToLazyMint + _amount, _encryptedBaseURI); + if (_data.length > 0) { + (bytes memory encryptedURI, bytes32 provenanceHash) = abi.decode(_data, (bytes, bytes32)); + if (encryptedURI.length != 0 && provenanceHash != "") { + _setEncryptedData(nextTokenIdToLazyMint + _amount, _data); + } } - return super.lazyMint(_amount, _baseURIForTokens, _encryptedBaseURI); + return super.lazyMint(_amount, _baseURIForTokens, _data); } /*////////////////////////////////////////////////////////////// @@ -99,7 +105,7 @@ contract ERC721DelayedReveal is ERC721LazyMint, DelayedReveal { uint256 batchId = getBatchIdAtIndex(_index); revealedURI = getRevealURI(batchId, _key); - _setEncryptedBaseURI(batchId, ""); + _setEncryptedData(batchId, ""); _setBaseURI(batchId, revealedURI); emit TokenURIRevealed(_index, revealedURI); diff --git a/contracts/base/ERC721Drop.sol b/contracts/base/ERC721Drop.sol index 9b6b52b45..42912647a 100644 --- a/contracts/base/ERC721Drop.sol +++ b/contracts/base/ERC721Drop.sol @@ -1,45 +1,102 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; -import "./ERC721SignatureMint.sol"; +/// @author thirdweb +import { ERC721A, Context } from "../eip/ERC721AVirtualApprove.sol"; + +import "../extension/ContractMetadata.sol"; +import "../extension/Multicall.sol"; +import "../extension/Ownable.sol"; +import "../extension/Royalty.sol"; +import "../extension/BatchMintMetadata.sol"; +import "../extension/PrimarySale.sol"; import "../extension/DropSinglePhase.sol"; import "../extension/LazyMint.sol"; import "../extension/DelayedReveal.sol"; -import "../lib/TWStrings.sol"; +import "../lib/Strings.sol"; +import { CurrencyTransferLib } from "../lib/CurrencyTransferLib.sol"; /** * BASE: ERC721A - * EXTENSION: SignatureMintERC721, DropSinglePhase + * EXTENSION: DropSinglePhase + * + * The `ERC721Drop` contract implements the ERC721 NFT standard, along with the ERC721A optimization to the standard. + * It includes the following additions to standard ERC721 logic: + * + * - Contract metadata for royalty support on platforms such as OpenSea that use + * off-chain information to distribute roaylties. + * + * - Ownership of the contract, with the ability to restrict certain functions to + * only be called by the contract's owner. * - * The `ERC721Drop` contract uses the `ERC721Base` contract, along with the `SignatureMintERC721` and `DropSinglePhase` extension. + * - Multicall capability to perform multiple actions atomically * - * The 'signature minting' mechanism in the `SignatureMintERC721` extension is a way for a contract admin to authorize - * an external party's request to mint tokens on the admin's contract. At a high level, this means you can authorize - * some external party to mint tokens on your contract, and specify what exactly will be minted by that external party. + * - EIP 2981 compliance for royalty support on NFT marketplaces. * * The `drop` mechanism in the `DropSinglePhase` extension is a distribution mechanism for lazy minted tokens. It lets * you set restrictions such as a price to charge, an allowlist etc. when an address atttempts to mint lazy minted tokens. * - * The `ERC721Drop` contract lets you lazy mint tokens, and distribute those lazy minted tokens via signature minting, or - * via the drop mechanism. + * The `ERC721Drop` contract lets you lazy mint tokens, and distribute those lazy minted tokens via the drop mechanism. */ -contract ERC721Drop is ERC721SignatureMint, LazyMint, DelayedReveal, DropSinglePhase { - using TWStrings for uint256; +contract ERC721Drop is + ERC721A, + ContractMetadata, + Multicall, + Ownable, + Royalty, + BatchMintMetadata, + PrimarySale, + LazyMint, + DelayedReveal, + DropSinglePhase +{ + using Strings for uint256; /*/////////////////////////////////////////////////////////////// Constructor //////////////////////////////////////////////////////////////*/ + /** + * @notice Initializes the contract during construction. + * + * @param _defaultAdmin The default admin of the contract. + * @param _name The name of the contract. + * @param _symbol The symbol of the contract. + * @param _royaltyRecipient The address to receive royalties. + * @param _royaltyBps The royalty basis points to be charged. Max = 10000 (10000 = 100%, 1000 = 10%) + * @param _primarySaleRecipient The address to receive primary sale value. + */ constructor( + address _defaultAdmin, string memory _name, string memory _symbol, address _royaltyRecipient, uint128 _royaltyBps, address _primarySaleRecipient - ) ERC721SignatureMint(_name, _symbol, _royaltyRecipient, _royaltyBps, _primarySaleRecipient) {} + ) ERC721A(_name, _symbol) { + _setupOwner(_defaultAdmin); + _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + _setupPrimarySaleRecipient(_primarySaleRecipient); + } + + /*////////////////////////////////////////////////////////////// + ERC165 Logic + //////////////////////////////////////////////////////////////*/ + + /** + * @dev See ERC165: https://eips.ethereum.org/EIPS/eip-165 + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721A, IERC165) returns (bool) { + return + interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 + interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721 + interfaceId == 0x5b5e139f || // ERC165 Interface ID for ERC721Metadata + interfaceId == type(IERC2981).interfaceId; // ERC165 ID for ERC2981 + } /*/////////////////////////////////////////////////////////////// Overriden ERC 721 logic @@ -51,9 +108,9 @@ contract ERC721Drop is ERC721SignatureMint, LazyMint, DelayedReveal, DropSingleP * * @param _tokenId The tokenId of an NFT. */ - function tokenURI(uint256 _tokenId) public view override returns (string memory) { - uint256 batchId = getBatchId(_tokenId); - string memory batchUri = getBaseURI(_tokenId); + function tokenURI(uint256 _tokenId) public view virtual override returns (string memory) { + (uint256 batchId, ) = _getBatchId(_tokenId); + string memory batchUri = _getBaseURI(_tokenId); if (isEncryptedBatch(batchId)) { return string(abi.encodePacked(batchUri, "0")); @@ -62,49 +119,6 @@ contract ERC721Drop is ERC721SignatureMint, LazyMint, DelayedReveal, DropSingleP } } - /*/////////////////////////////////////////////////////////////// - Overriden signature minting logic - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Mints tokens according to the provided mint request. - * - * @param _req The payload / mint request. - * @param _signature The signature produced by an account signing the mint request. - */ - function mintWithSignature(MintRequest calldata _req, bytes calldata _signature) - external - payable - virtual - override - returns (address signer) - { - require(_req.quantity > 0, "Minting zero tokens."); - - uint256 tokenIdToMint = _currentIndex; - require(tokenIdToMint + _req.quantity <= nextTokenIdToLazyMint, "Not enough lazy minted tokens."); - - // Verify and process payload. - signer = _processRequest(_req, _signature); - - /** - * Get receiver of tokens. - * - * Note: If `_req.to == address(0)`, a `mintWithSignature` transaction sitting in the - * mempool can be frontrun by copying the input data, since the minted tokens - * will be sent to the `_msgSender()` in this case. - */ - address receiver = _req.to == address(0) ? msg.sender : _req.to; - - // Collect price - collectPriceOnClaim(_req.quantity, _req.currency, _req.pricePerToken); - - // Mint tokens. - _safeMint(receiver, _req.quantity); - - emit TokensMintedWithSignature(signer, receiver, tokenIdToMint, _req); - } - /*/////////////////////////////////////////////////////////////// Overriden lazy minting logic //////////////////////////////////////////////////////////////*/ @@ -115,26 +129,34 @@ contract ERC721Drop is ERC721SignatureMint, LazyMint, DelayedReveal, DropSingleP * @param _amount The number of NFTs to lazy mint. * @param _baseURIForTokens The placeholder base URI for the 'n' number of NFTs being lazy minted, where the * metadata for each of those NFTs is `${baseURIForTokens}/${tokenId}`. - * @param _encryptedBaseURI The encrypted base URI for the batch of NFTs being lazy minted. + * @param _data The encrypted base URI + provenance hash for the batch of NFTs being lazy minted. * @return batchId A unique integer identifier for the batch of NFTs lazy minted together. */ function lazyMint( uint256 _amount, string calldata _baseURIForTokens, - bytes calldata _encryptedBaseURI + bytes calldata _data ) public virtual override returns (uint256 batchId) { - if (_encryptedBaseURI.length != 0) { - _setEncryptedBaseURI(nextTokenIdToLazyMint + _amount, _encryptedBaseURI); + if (_data.length > 0) { + (bytes memory encryptedURI, bytes32 provenanceHash) = abi.decode(_data, (bytes, bytes32)); + if (encryptedURI.length != 0 && provenanceHash != "") { + _setEncryptedData(nextTokenIdToLazyMint + _amount, _data); + } } - return LazyMint.lazyMint(_amount, _baseURIForTokens, _encryptedBaseURI); + return LazyMint.lazyMint(_amount, _baseURIForTokens, _data); } /// @notice The tokenId assigned to the next new NFT to be lazy minted. - function nextTokenIdToMint() public view virtual override returns (uint256) { + function nextTokenIdToMint() public view virtual returns (uint256) { return nextTokenIdToLazyMint; } + /// @notice The tokenId assigned to the next new NFT to be claimed. + function nextTokenIdToClaim() public view virtual returns (uint256) { + return _currentIndex; + } + /*/////////////////////////////////////////////////////////////// Delayed reveal logic //////////////////////////////////////////////////////////////*/ @@ -145,23 +167,41 @@ contract ERC721Drop is ERC721SignatureMint, LazyMint, DelayedReveal, DropSingleP * @param _index The ID for the batch of delayed-reveal NFTs to reveal. * @param _key The key with which the base URI for the relevant batch of NFTs was encrypted. */ - function reveal(uint256 _index, bytes calldata _key) external override returns (string memory revealedURI) { + function reveal(uint256 _index, bytes calldata _key) public virtual override returns (string memory revealedURI) { require(_canReveal(), "Not authorized"); uint256 batchId = getBatchIdAtIndex(_index); revealedURI = getRevealURI(batchId, _key); - _setEncryptedBaseURI(batchId, ""); + _setEncryptedData(batchId, ""); _setBaseURI(batchId, revealedURI); emit TokenURIRevealed(_index, revealedURI); } + /*////////////////////////////////////////////////////////////// + Minting/burning logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Lets an owner or approved operator burn the NFT of the given tokenId. + * @dev ERC721A's `_burn(uint256,bool)` internally checks for token approvals. + * + * @param _tokenId The tokenId of the NFT to burn. + */ + function burn(uint256 _tokenId) external virtual { + _burn(_tokenId, true); + } + /*/////////////////////////////////////////////////////////////// Internal functions //////////////////////////////////////////////////////////////*/ - /// @dev Runs before every `claim` function call. + /** + * @dev Runs before every `claim` function call. + * + * @param _quantity The quantity of NFTs being claimed. + */ function _beforeClaim( address, uint256 _quantity, @@ -169,66 +209,81 @@ contract ERC721Drop is ERC721SignatureMint, LazyMint, DelayedReveal, DropSingleP uint256, AllowlistProof calldata, bytes memory - ) internal view override { - require(msg.sender == tx.origin, "BOT"); + ) internal view virtual override { if (_currentIndex + _quantity > nextTokenIdToLazyMint) { revert("Not enough minted tokens"); } } - /// @dev Collects and distributes the primary sale value of NFTs being claimed. - function collectPriceOnClaim( + /** + * @dev Collects and distributes the primary sale value of NFTs being claimed. + * + * @param _primarySaleRecipient The address to receive the primary sale value. + * @param _quantityToClaim The quantity of NFTs being claimed. + * @param _currency The currency in which the NFTs are being claimed. + * @param _pricePerToken The price per token in the given currency. + */ + function _collectPriceOnClaim( + address _primarySaleRecipient, uint256 _quantityToClaim, address _currency, uint256 _pricePerToken - ) internal override(DropSinglePhase, ERC721SignatureMint) { + ) internal virtual override { if (_pricePerToken == 0) { + require(msg.value == 0, "!Value"); return; } uint256 totalPrice = _quantityToClaim * _pricePerToken; + bool validMsgValue; if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { - if (msg.value != totalPrice) { - revert("Must send total price"); - } + validMsgValue = msg.value == totalPrice; + } else { + validMsgValue = msg.value == 0; } + require(validMsgValue, "Invalid msg value"); - CurrencyTransferLib.transferCurrency(_currency, msg.sender, primarySaleRecipient(), totalPrice); + address saleRecipient = _primarySaleRecipient == address(0) ? primarySaleRecipient() : _primarySaleRecipient; + CurrencyTransferLib.transferCurrency(_currency, msg.sender, saleRecipient, totalPrice); } - /// @dev Transfers the NFTs being claimed. - function transferTokensOnClaim(address _to, uint256 _quantityBeingClaimed) - internal - override - returns (uint256 startTokenId) - { + /** + * @dev Transfers the NFTs being claimed. + * + * @param _to The address to which the NFTs are being transferred. + * @param _quantityBeingClaimed The quantity of NFTs being claimed. + */ + function _transferTokensOnClaim( + address _to, + uint256 _quantityBeingClaimed + ) internal virtual override returns (uint256 startTokenId) { startTokenId = _currentIndex; _safeMint(_to, _quantityBeingClaimed); } /// @dev Checks whether primary sale recipient can be set in the given execution context. - function _canSetPrimarySaleRecipient() internal view override returns (bool) { + function _canSetPrimarySaleRecipient() internal view virtual override returns (bool) { return msg.sender == owner(); } /// @dev Checks whether owner can be set in the given execution context. - function _canSetOwner() internal view override returns (bool) { + function _canSetOwner() internal view virtual override returns (bool) { return msg.sender == owner(); } /// @dev Checks whether royalty info can be set in the given execution context. - function _canSetRoyaltyInfo() internal view override returns (bool) { + function _canSetRoyaltyInfo() internal view virtual override returns (bool) { return msg.sender == owner(); } /// @dev Checks whether contract metadata can be set in the given execution context. - function _canSetContractURI() internal view override returns (bool) { + function _canSetContractURI() internal view virtual override returns (bool) { return msg.sender == owner(); } /// @dev Checks whether platform fee info can be set in the given execution context. - function _canSetClaimConditions() internal view override returns (bool) { + function _canSetClaimConditions() internal view virtual override returns (bool) { return msg.sender == owner(); } @@ -250,16 +305,8 @@ contract ERC721Drop is ERC721SignatureMint, LazyMint, DelayedReveal, DropSingleP return msg.sender; } - function mintTo(address, string memory) public virtual override { - revert("Not implemented. Use lazymint instead."); - } - - function batchMintTo( - address, - uint256, - string memory, - bytes memory - ) public virtual override { - revert("Not implemented. Use lazymint instead."); + /// @notice Returns the sender in the given execution context. + function _msgSender() internal view override(Multicall, Context) returns (address) { + return msg.sender; } } diff --git a/contracts/base/ERC721LazyMint.sol b/contracts/base/ERC721LazyMint.sol index a688a1c5d..71929b22d 100644 --- a/contracts/base/ERC721LazyMint.sol +++ b/contracts/base/ERC721LazyMint.sol @@ -1,88 +1,219 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; -import "./ERC721Base.sol"; +/// @author thirdweb +import { ERC721A, Context } from "../eip/ERC721AVirtualApprove.sol"; + +import "../extension/ContractMetadata.sol"; +import "../extension/Multicall.sol"; +import "../extension/Ownable.sol"; +import "../extension/Royalty.sol"; +import "../extension/BatchMintMetadata.sol"; import "../extension/LazyMint.sol"; +import "../extension/interface/IClaimableERC721.sol"; -import "../lib/TWStrings.sol"; +import "../lib/Strings.sol"; +import "../external-deps/openzeppelin/security/ReentrancyGuard.sol"; /** - * BASE: ERC721Base + * BASE: ERC721A * EXTENSION: LazyMint * - * The `ERC721LazyMint` contract uses the `ERC721Base` contract, along with the `LazyMint` extension. + * The `ERC721LazyMint` smart contract implements the ERC721 NFT standard, along with the ERC721A optimization to the standard. + * It includes the following additions to standard ERC721 logic: + * + * - Lazy minting + * + * - Contract metadata for royalty support on platforms such as OpenSea that use + * off-chain information to distribute roaylties. + * + * - Ownership of the contract, with the ability to restrict certain functions to + * only be called by the contract's owner. + * + * - Multicall capability to perform multiple actions atomically + * + * - EIP 2981 compliance for royalty support on NFT marketplaces. * * 'Lazy minting' means defining the metadata of NFTs without minting it to an address. Regular 'minting' * of NFTs means actually assigning an owner to an NFT. * * As a contract admin, this lets you prepare the metadata for NFTs that will be minted by an external party, * without paying the gas cost for actually minting the NFTs. - * */ -contract ERC721LazyMint is ERC721Base, LazyMint { - using TWStrings for uint256; +contract ERC721LazyMint is + ERC721A, + ContractMetadata, + Multicall, + Ownable, + Royalty, + BatchMintMetadata, + LazyMint, + IClaimableERC721, + ReentrancyGuard +{ + using Strings for uint256; /*////////////////////////////////////////////////////////////// Constructor //////////////////////////////////////////////////////////////*/ + /** + * @notice Initializes the contract during construction. + * + * @param _defaultAdmin The default admin of the contract. + * @param _name The name of the contract. + * @param _symbol The symbol of the contract. + * @param _royaltyRecipient The address to receive royalties. + * @param _royaltyBps The royalty basis points to be charged. Max = 10000 (10000 = 100%, 1000 = 10%) + */ constructor( + address _defaultAdmin, string memory _name, string memory _symbol, address _royaltyRecipient, uint128 _royaltyBps - ) ERC721Base(_name, _symbol, _royaltyRecipient, _royaltyBps) {} + ) ERC721A(_name, _symbol) { + _setupOwner(_defaultAdmin); + _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + } /*////////////////////////////////////////////////////////////// - Minting logic + ERC165 Logic //////////////////////////////////////////////////////////////*/ /** - * @notice Lets an authorized address mint a lazy minted NFT to a recipient. - * @dev The logic in the `_canMint` function determines whether the caller is authorized to mint NFTs. - * - * @param _to The recipient of the NFT to mint. + * @dev See ERC165: https://eips.ethereum.org/EIPS/eip-165 + * @inheritdoc IERC165 */ - function mintTo(address _to, string memory) public virtual override { - require(_canMint(), "Not authorized to mint."); - require(_currentIndex + 1 <= nextTokenIdToLazyMint, "Not enough lazy minted tokens."); + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721A, IERC165) returns (bool) { + return + interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 + interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721 + interfaceId == 0x5b5e139f || // ERC165 Interface ID for ERC721Metadata + interfaceId == type(IERC2981).interfaceId; // ERC165 ID for ERC2981 + } - _safeMint(_to, 1, ""); + /*////////////////////////////////////////////////////////////// + Overriden ERC721 logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the metadata URI for an NFT. + * @dev See `BatchMintMetadata` for handling of metadata in this contract. + * + * @param _tokenId The tokenId of an NFT. + */ + function tokenURI(uint256 _tokenId) public view virtual override returns (string memory) { + string memory batchUri = _getBaseURI(_tokenId); + return string(abi.encodePacked(batchUri, _tokenId.toString())); } + /*////////////////////////////////////////////////////////////// + Claiming logic + //////////////////////////////////////////////////////////////*/ + /** - * @notice Lets an authorized address mint multiple lazy minted NFTs at once to a recipient. - * @dev The logic in the `_canMint` function determines whether the caller is authorized to mint NFTs. + * @notice Lets an address claim multiple lazy minted NFTs at once to a recipient. + * This function prevents any reentrant calls, and is not allowed to be overridden. + * + * @dev Contract creators should override `verifyClaim` and `transferTokensOnClaim` + * functions to create custom logic for verification and claiming, + * for e.g. price collection, allowlist, max quantity, etc. + * The logic in `verifyClaim` determines whether the caller is authorized to mint NFTs. + * The logic in `transferTokensOnClaim` does actual minting of tokens, + * can also be used to apply other state changes. * - * @param _to The recipient of the NFT to mint. - * @param _quantity The number of NFTs to mint. - * @param _data Additional data to pass along during the minting of the NFT. + * @param _receiver The recipient of the NFT to mint. + * @param _quantity The number of NFTs to mint. */ - function batchMintTo( - address _to, - uint256 _quantity, - string memory, - bytes memory _data - ) public virtual override { - require(_canMint(), "Not authorized to mint."); + function claim(address _receiver, uint256 _quantity) public payable virtual nonReentrant { require(_currentIndex + _quantity <= nextTokenIdToLazyMint, "Not enough lazy minted tokens."); + verifyClaim(msg.sender, _quantity); // Add your claim verification logic by overriding this function. - _safeMint(_to, _quantity, _data); + uint256 startTokenId = _transferTokensOnClaim(_receiver, _quantity); // Mints tokens. Apply any state updates by overriding this function. + emit TokensClaimed(msg.sender, _receiver, startTokenId, _quantity); + } + + /** + * @notice Checks a request to claim NFTs against a custom condition. + * + * @dev Override this function to add logic for claim verification, based on conditions + * such as allowlist, price, max quantity etc. + * + * @param _claimer Caller of the claim function. + * @param _quantity The number of NFTs being claimed. + */ + function verifyClaim(address _claimer, uint256 _quantity) public view virtual {} + + /** + * @notice Lets an owner or approved operator burn the NFT of the given tokenId. + * @dev ERC721A's `_burn(uint256,bool)` internally checks for token approvals. + * + * @param _tokenId The tokenId of the NFT to burn. + */ + function burn(uint256 _tokenId) external virtual { + _burn(_tokenId, true); } /// @notice The tokenId assigned to the next new NFT to be lazy minted. - function nextTokenIdToMint() public view virtual override returns (uint256) { + function nextTokenIdToMint() public view virtual returns (uint256) { return nextTokenIdToLazyMint; } + /// @notice The tokenId assigned to the next new NFT to be claimed. + function nextTokenIdToClaim() public view virtual returns (uint256) { + return _currentIndex; + } + /*////////////////////////////////////////////////////////////// Internal functions //////////////////////////////////////////////////////////////*/ + /** + * @notice Mints tokens to receiver on claim. + * Any state changes related to `claim` must be applied + * here by overriding this function. + * + * @dev Override this function to add logic for state updation. + * When overriding, apply any state changes before `_safeMint`. + * + * @param _receiver The recipient of the NFT to mint. + * @param _quantity The number of NFTs to mint. + * + * @return startTokenId The tokenId of the first NFT minted. + */ + function _transferTokensOnClaim( + address _receiver, + uint256 _quantity + ) internal virtual returns (uint256 startTokenId) { + startTokenId = _currentIndex; + _safeMint(_receiver, _quantity); + } + /// @dev Returns whether lazy minting can be done in the given execution context. function _canLazyMint() internal view virtual override returns (bool) { return msg.sender == owner(); } + + /// @dev Returns whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether owner can be set in the given execution context. + function _canSetOwner() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether royalty info can be set in the given execution context. + function _canSetRoyaltyInfo() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @notice Returns the sender in the given execution context. + function _msgSender() internal view override(Multicall, Context) returns (address) { + return msg.sender; + } } diff --git a/contracts/base/ERC721Multiwrap.sol b/contracts/base/ERC721Multiwrap.sol new file mode 100644 index 000000000..6125b059f --- /dev/null +++ b/contracts/base/ERC721Multiwrap.sol @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import { ERC721A, Context } from "../eip/ERC721AVirtualApprove.sol"; + +import "../extension/ContractMetadata.sol"; +import "../extension/Ownable.sol"; +import "../extension/Royalty.sol"; +import "../extension/SoulboundERC721A.sol"; +import "../extension/TokenStore.sol"; +import "../extension/Multicall.sol"; +import { ReentrancyGuard } from "../extension/upgradeable/ReentrancyGuard.sol"; + +/** + * BASE: ERC721Base + * EXTENSION: TokenStore, SoulboundERC721A + * + * The `ERC721Multiwrap` contract uses the `ERC721Base` contract, along with the `TokenStore` and + * `SoulboundERC721A` extension. + * + * The `ERC721Multiwrap` contract lets you wrap arbitrary ERC20, ERC721 and ERC1155 tokens you own + * into a single wrapped token / NFT. + * + * The `SoulboundERC721A` extension lets you make your NFTs 'soulbound' i.e. non-transferrable. + * + */ + +contract ERC721Multiwrap is + Multicall, + TokenStore, + SoulboundERC721A, + ERC721A, + ContractMetadata, + Ownable, + Royalty, + ReentrancyGuard +{ + /*////////////////////////////////////////////////////////////// + Permission control roles + //////////////////////////////////////////////////////////////*/ + + /// @dev Only MINTER_ROLE holders can wrap tokens, when wrapping is restricted. + bytes32 private constant MINTER_ROLE = keccak256("MINTER_ROLE"); + /// @dev Only UNWRAP_ROLE holders can unwrap tokens, when unwrapping is restricted. + bytes32 private constant UNWRAP_ROLE = keccak256("UNWRAP_ROLE"); + /// @dev Only assets with ASSET_ROLE can be wrapped, when wrapping is restricted to particular assets. + bytes32 private constant ASSET_ROLE = keccak256("ASSET_ROLE"); + + /*////////////////////////////////////////////////////////////// + Events + //////////////////////////////////////////////////////////////*/ + + /// @dev Emitted when tokens are wrapped. + event TokensWrapped( + address indexed wrapper, + address indexed recipientOfWrappedToken, + uint256 indexed tokenIdOfWrappedToken, + Token[] wrappedContents + ); + + /// @dev Emitted when tokens are unwrapped. + event TokensUnwrapped( + address indexed unwrapper, + address indexed recipientOfWrappedContents, + uint256 indexed tokenIdOfWrappedToken + ); + + /*////////////////////////////////////////////////////////////// + Events + //////////////////////////////////////////////////////////////*/ + + /// @notice Checks whether the caller holds `role`, when restrictions for `role` are switched on. + modifier onlyRoleWithSwitch(bytes32 role) { + _checkRoleWithSwitch(role, msg.sender); + _; + } + + /*////////////////////////////////////////////////////////////// + Constructor + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Initializes the contract during construction. + * + * @param _defaultAdmin The default admin of the contract. + * @param _name The name of the contract. + * @param _symbol The symbol of the contract. + * @param _royaltyRecipient The address to receive royalties. + * @param _royaltyBps The royalty basis points to be charged. Max = 10000 (10000 = 100%, 1000 = 10%) + * @param _nativeTokenWrapper The address of the ERC20 wrapper for the native token. + */ + constructor( + address _defaultAdmin, + string memory _name, + string memory _symbol, + address _royaltyRecipient, + uint128 _royaltyBps, + address _nativeTokenWrapper + ) ERC721A(_name, _symbol) TokenStore(_nativeTokenWrapper) { + _setupOwner(_defaultAdmin); + _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + _setupRole(MINTER_ROLE, _defaultAdmin); + _setupRole(TRANSFER_ROLE, _defaultAdmin); + + _setupRole(ASSET_ROLE, address(0)); + _setupRole(UNWRAP_ROLE, address(0)); + + restrictTransfers(false); + } + + /*/////////////////////////////////////////////////////////////// + Public gette functions + //////////////////////////////////////////////////////////////*/ + + /** + * @dev See ERC165: https://eips.ethereum.org/EIPS/eip-165 + * @inheritdoc IERC165 + */ + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC1155Receiver, ERC721A, IERC165) returns (bool) { + return + super.supportsInterface(interfaceId) || + interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 + interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721 + interfaceId == 0x5b5e139f || // ERC165 Interface ID for ERC721Metadata + interfaceId == type(IERC2981).interfaceId || // ERC165 ID for ERC2981 + interfaceId == type(IERC1155Receiver).interfaceId; + } + + /*////////////////////////////////////////////////////////////// + Overriden ERC721 logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the URI for a given tokenId. + function tokenURI(uint256 _tokenId) public view virtual override returns (string memory) { + return getUriOfBundle(_tokenId); + } + + /*/////////////////////////////////////////////////////////////// + Wrapping / Unwrapping logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Wrap multiple ERC1155, ERC721, ERC20 tokens into a single wrapped NFT. + * + * @param _tokensToWrap The tokens to wrap. + * @param _uriForWrappedToken The metadata URI for the wrapped NFT. + * @param _recipient The recipient of the wrapped NFT. + * + * @return tokenId The tokenId of the wrapped NFT minted. + */ + function wrap( + Token[] calldata _tokensToWrap, + string calldata _uriForWrappedToken, + address _recipient + ) public payable virtual onlyRoleWithSwitch(MINTER_ROLE) nonReentrant returns (uint256 tokenId) { + if (!hasRole(ASSET_ROLE, address(0))) { + for (uint256 i = 0; i < _tokensToWrap.length; i += 1) { + _checkRole(ASSET_ROLE, _tokensToWrap[i].assetContract); + } + } + + tokenId = nextTokenIdToMint(); + + _storeTokens(msg.sender, _tokensToWrap, _uriForWrappedToken, tokenId); + + _safeMint(_recipient, 1); + + emit TokensWrapped(msg.sender, _recipient, tokenId, _tokensToWrap); + } + + /** + * @notice Unwrap a wrapped NFT to retrieve underlying ERC1155, ERC721, ERC20 tokens. + * + * @param _tokenId The token Id of the wrapped NFT to unwrap. + * @param _recipient The recipient of the underlying ERC1155, ERC721, ERC20 tokens of the wrapped NFT. + */ + function unwrap(uint256 _tokenId, address _recipient) public virtual onlyRoleWithSwitch(UNWRAP_ROLE) nonReentrant { + require(_tokenId < nextTokenIdToMint(), "wrapped NFT DNE."); + require(isApprovedOrOwner(msg.sender, _tokenId), "caller not approved for unwrapping."); + + _burn(_tokenId); + _releaseTokens(_recipient, _tokenId); + + emit TokensUnwrapped(msg.sender, _recipient, _tokenId); + } + + /*////////////////////////////////////////////////////////////// + Public getters + //////////////////////////////////////////////////////////////*/ + + /// @notice The tokenId assigned to the next new NFT to be minted. + function nextTokenIdToMint() public view virtual returns (uint256) { + return _currentIndex; + } + + /** + * @notice Returns whether a given address is the owner, or approved to transfer an NFT. + * + * @param _operator The address to check. + * @param _tokenId The tokenId to check. + * + * @return isApprovedOrOwnerOf Whether `_operator` is approved to transfer `_tokenId`. + */ + function isApprovedOrOwner( + address _operator, + uint256 _tokenId + ) public view virtual returns (bool isApprovedOrOwnerOf) { + address owner = ownerOf(_tokenId); + isApprovedOrOwnerOf = (_operator == owner || + isApprovedForAll(owner, _operator) || + getApproved(_tokenId) == _operator); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /** + * @dev See {ERC721-_beforeTokenTransfer}. + * @inheritdoc ERC721A + */ + function _beforeTokenTransfers( + address from, + address to, + uint256 startTokenId, + uint256 quantity + ) internal virtual override(ERC721A, SoulboundERC721A) { + super._beforeTokenTransfers(from, to, startTokenId, quantity); + SoulboundERC721A._beforeTokenTransfers(from, to, startTokenId, quantity); + } + + /// @dev Returns whether transfers can be restricted in a given execution context. + function _canRestrictTransfers() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether owner can be set in the given execution context. + function _canSetOwner() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether royalty info can be set in the given execution context. + function _canSetRoyaltyInfo() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @notice Returns the sender in the given execution context. + function _msgSender() internal view override(Multicall, Context) returns (address) { + return msg.sender; + } +} diff --git a/contracts/base/ERC721SignatureMint.sol b/contracts/base/ERC721SignatureMint.sol index 8eb86819e..a3b389b00 100644 --- a/contracts/base/ERC721SignatureMint.sol +++ b/contracts/base/ERC721SignatureMint.sol @@ -1,13 +1,15 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + import "./ERC721Base.sol"; import "../extension/PrimarySale.sol"; import "../extension/PermissionsEnumerable.sol"; import "../extension/SignatureMintERC721.sol"; - -import "../lib/CurrencyTransferLib.sol"; +import { ReentrancyGuard } from "../extension/upgradeable/ReentrancyGuard.sol"; +import { CurrencyTransferLib } from "../lib/CurrencyTransferLib.sol"; /** * BASE: ERC721A @@ -22,18 +24,19 @@ import "../lib/CurrencyTransferLib.sol"; * */ -contract ERC721SignatureMint is ERC721Base, PrimarySale, SignatureMintERC721 { +contract ERC721SignatureMint is ERC721Base, PrimarySale, SignatureMintERC721, ReentrancyGuard { /*////////////////////////////////////////////////////////////// Constructor //////////////////////////////////////////////////////////////*/ constructor( + address _defaultAdmin, string memory _name, string memory _symbol, address _royaltyRecipient, uint128 _royaltyBps, address _primarySaleRecipient - ) ERC721Base(_name, _symbol, _royaltyRecipient, _royaltyBps) { + ) ERC721Base(_defaultAdmin, _name, _symbol, _royaltyRecipient, _royaltyBps) { _setupPrimarySaleRecipient(_primarySaleRecipient); } @@ -47,34 +50,29 @@ contract ERC721SignatureMint is ERC721Base, PrimarySale, SignatureMintERC721 { * @param _req The payload / mint request. * @param _signature The signature produced by an account signing the mint request. */ - function mintWithSignature(MintRequest calldata _req, bytes calldata _signature) - external - payable - virtual - override - returns (address signer) - { - require(_req.quantity > 0, "Minting zero tokens."); + function mintWithSignature( + MintRequest calldata _req, + bytes calldata _signature + ) external payable virtual override nonReentrant returns (address signer) { + require(_req.quantity == 1, "quantiy must be 1"); uint256 tokenIdToMint = nextTokenIdToMint(); // Verify and process payload. signer = _processRequest(_req, _signature); - /** - * Get receiver of tokens. - * - * Note: If `_req.to == address(0)`, a `mintWithSignature` transaction sitting in the - * mempool can be frontrun by copying the input data, since the minted tokens - * will be sent to the `_msgSender()` in this case. - */ - address receiver = _req.to == address(0) ? msg.sender : _req.to; + address receiver = _req.to; // Collect price - collectPriceOnClaim(_req.quantity, _req.currency, _req.pricePerToken); + _collectPriceOnClaim(_req.primarySaleRecipient, _req.quantity, _req.currency, _req.pricePerToken); + + // Set royalties, if applicable. + if (_req.royaltyRecipient != address(0) && _req.royaltyBps != 0) { + _setupRoyaltyInfoForToken(tokenIdToMint, _req.royaltyRecipient, _req.royaltyBps); + } // Mint tokens. - _batchMintMetadata(nextTokenIdToMint(), _req.quantity, _req.uri); + _setTokenURI(tokenIdToMint, _req.uri); _safeMint(receiver, _req.quantity); emit TokensMintedWithSignature(signer, receiver, tokenIdToMint, _req); @@ -95,21 +93,28 @@ contract ERC721SignatureMint is ERC721Base, PrimarySale, SignatureMintERC721 { } /// @dev Collects and distributes the primary sale value of NFTs being claimed. - function collectPriceOnClaim( + function _collectPriceOnClaim( + address _primarySaleRecipient, uint256 _quantityToClaim, address _currency, uint256 _pricePerToken ) internal virtual { if (_pricePerToken == 0) { + require(msg.value == 0, "!Value"); return; } uint256 totalPrice = _quantityToClaim * _pricePerToken; + bool validMsgValue; if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { - require(msg.value == totalPrice, "Must send total price."); + validMsgValue = msg.value == totalPrice; + } else { + validMsgValue = msg.value == 0; } + require(validMsgValue, "Invalid msg value"); - CurrencyTransferLib.transferCurrency(_currency, msg.sender, primarySaleRecipient(), totalPrice); + address saleRecipient = _primarySaleRecipient == address(0) ? primarySaleRecipient() : _primarySaleRecipient; + CurrencyTransferLib.transferCurrency(_currency, msg.sender, saleRecipient, totalPrice); } } diff --git a/contracts/base/Staking1155Base.sol b/contracts/base/Staking1155Base.sol new file mode 100644 index 000000000..a557d06a1 --- /dev/null +++ b/contracts/base/Staking1155Base.sol @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../extension/ContractMetadata.sol"; +import "../extension/Multicall.sol"; +import "../extension/Ownable.sol"; +import "../extension/Staking1155.sol"; + +import "../eip/ERC165.sol"; +import "../eip/interface/IERC20.sol"; +import "../eip/interface/IERC1155Receiver.sol"; + +import { CurrencyTransferLib } from "../lib/CurrencyTransferLib.sol"; + +/** + * + * EXTENSION: Staking1155 + * + * The `Staking1155Base` smart contract implements NFT staking mechanism. + * Allows users to stake their ERC-1155 NFTs and earn rewards in form of ERC-20 tokens. + * + * Following features and implementation setup must be noted: + * + * - ERC-1155 NFTs from only one collection can be staked. + * + * - Contract admin can choose to give out rewards by either transferring or minting the rewardToken, + * which is an ERC20 token. See {_mintRewards}. + * + * - To implement custom logic for staking, reward calculation, etc. corresponding functions can be + * overridden from the extension `Staking1155`. + * + * - Ownership of the contract, with the ability to restrict certain functions to + * only be called by the contract's owner. + * + * - Multicall capability to perform multiple actions atomically. + * + */ + +/// note: This contract is provided as a base contract. +// This is to support a variety of use-cases that can be build on top of this base. +// +// Additional functionality such as deposit functions, reward-minting, etc. +// must be implemented by the deployer of this contract, as needed for their use-case. + +contract Staking1155Base is ContractMetadata, Multicall, Ownable, Staking1155, ERC165, IERC1155Receiver { + /// @dev ERC20 Reward Token address. See {_mintRewards} below. + address public immutable rewardToken; + + /// @dev The address of the native token wrapper contract. + address internal immutable nativeTokenWrapper; + + /// @dev Total amount of reward tokens in the contract. + uint256 private rewardTokenBalance; + + constructor( + uint80 _defaultTimeUnit, + address _defaultAdmin, + uint256 _defaultRewardsPerUnitTime, + address _stakingToken, + address _rewardToken, + address _nativeTokenWrapper + ) Staking1155(_stakingToken) { + _setupOwner(_defaultAdmin); + _setDefaultStakingCondition(_defaultTimeUnit, _defaultRewardsPerUnitTime); + + rewardToken = _rewardToken; + nativeTokenWrapper = _nativeTokenWrapper; + } + + /// @dev Lets the contract receive ether to unwrap native tokens. + receive() external payable virtual { + require(msg.sender == nativeTokenWrapper, "caller not native token wrapper."); + } + + /// @dev Admin deposits reward tokens. + function depositRewardTokens(uint256 _amount) external payable virtual nonReentrant { + _depositRewardTokens(_amount); // override this for custom logic. + } + + /// @dev Admin can withdraw excess reward tokens. + function withdrawRewardTokens(uint256 _amount) external virtual nonReentrant { + _withdrawRewardTokens(_amount); // override this for custom logic. + } + + /// @notice View total rewards available in the staking contract. + function getRewardTokenBalance() external view virtual override returns (uint256) { + return rewardTokenBalance; + } + + /*/////////////////////////////////////////////////////////////// + ERC 165 / 721 logic + //////////////////////////////////////////////////////////////*/ + + function onERC1155Received(address, address, uint256, uint256, bytes calldata) external view returns (bytes4) { + require(isStaking == 2, "Direct transfer"); + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external virtual returns (bytes4) {} + + function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) { + return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); + } + + /*////////////////////////////////////////////////////////////// + Minting logic + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Mint ERC20 rewards to the staker. Override for custom logic. + * + * @param _staker Address for which to calculated rewards. + * @param _rewards Amount of tokens to be given out as reward. + * + */ + function _mintRewards(address _staker, uint256 _rewards) internal virtual override { + require(_rewards <= rewardTokenBalance, "Not enough reward tokens"); + rewardTokenBalance -= _rewards; + CurrencyTransferLib.transferCurrencyWithWrapper( + rewardToken, + address(this), + _staker, + _rewards, + nativeTokenWrapper + ); + } + + /*////////////////////////////////////////////////////////////// + Other Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Admin deposits reward tokens -- override for custom logic. + function _depositRewardTokens(uint256 _amount) internal virtual { + require(msg.sender == owner(), "Not authorized"); + + address _rewardToken = rewardToken == CurrencyTransferLib.NATIVE_TOKEN ? nativeTokenWrapper : rewardToken; + + uint256 balanceBefore = IERC20(_rewardToken).balanceOf(address(this)); + CurrencyTransferLib.transferCurrencyWithWrapper( + rewardToken, + msg.sender, + address(this), + _amount, + nativeTokenWrapper + ); + uint256 actualAmount = IERC20(_rewardToken).balanceOf(address(this)) - balanceBefore; + + rewardTokenBalance += actualAmount; + } + + /// @dev Admin can withdraw excess reward tokens -- override for custom logic. + function _withdrawRewardTokens(uint256 _amount) internal virtual { + require(msg.sender == owner(), "Not authorized"); + + // to prevent locking of direct-transferred tokens + rewardTokenBalance = _amount > rewardTokenBalance ? 0 : rewardTokenBalance - _amount; + + CurrencyTransferLib.transferCurrencyWithWrapper( + rewardToken, + address(this), + msg.sender, + _amount, + nativeTokenWrapper + ); + } + + /// @dev Returns whether staking restrictions can be set in given execution context. + function _canSetStakeConditions() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether owner can be set in the given execution context. + function _canSetOwner() internal view virtual override returns (bool) { + return msg.sender == owner(); + } +} diff --git a/contracts/base/Staking20Base.sol b/contracts/base/Staking20Base.sol new file mode 100644 index 000000000..6e6cbe05b --- /dev/null +++ b/contracts/base/Staking20Base.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../extension/ContractMetadata.sol"; +import "../extension/Multicall.sol"; +import "../extension/Ownable.sol"; +import "../extension/Staking20.sol"; + +import "../eip/interface/IERC20.sol"; +import "../eip/interface/IERC20Metadata.sol"; + +import { CurrencyTransferLib } from "../lib/CurrencyTransferLib.sol"; + +/** + * + * EXTENSION: Staking20 + * + * The `Staking20Base` smart contract implements Token staking mechanism. + * Allows users to stake their ERC-20 Tokens and earn rewards in form of another ERC-20 tokens. + * + * Following features and implementation setup must be noted: + * + * - ERC-20 Tokens from only one contract can be staked. + * + * - Contract admin can choose to give out rewards by either transferring or minting the rewardToken, + * which is ideally a different ERC20 token. See {_mintRewards}. + * + * - To implement custom logic for staking, reward calculation, etc. corresponding functions can be + * overridden from the extension `Staking20`. + * + * - Ownership of the contract, with the ability to restrict certain functions to + * only be called by the contract's owner. + * + * - Multicall capability to perform multiple actions atomically. + * + */ + +/// note: This contract is provided as a base contract. +// This is to support a variety of use-cases that can be build on top of this base. +// +// Additional functionality such as deposit functions, reward-minting, etc. +// must be implemented by the deployer of this contract, as needed for their use-case. + +contract Staking20Base is ContractMetadata, Multicall, Ownable, Staking20 { + /// @dev ERC20 Reward Token address. See {_mintRewards} below. + address public immutable rewardToken; + + /// @dev Total amount of reward tokens in the contract. + uint256 private rewardTokenBalance; + + constructor( + uint80 _timeUnit, + address _defaultAdmin, + uint256 _rewardRatioNumerator, + uint256 _rewardRatioDenominator, + address _stakingToken, + address _rewardToken, + address _nativeTokenWrapper + ) + Staking20( + _nativeTokenWrapper, + _stakingToken, + IERC20Metadata(_stakingToken).decimals(), + IERC20Metadata(_rewardToken).decimals() + ) + { + _setupOwner(_defaultAdmin); + _setStakingCondition(_timeUnit, _rewardRatioNumerator, _rewardRatioDenominator); + + require(_rewardToken != _stakingToken, "Reward Token and Staking Token can't be same."); + rewardToken = _rewardToken; + } + + /// @dev Lets the contract receive ether to unwrap native tokens. + receive() external payable virtual { + require(msg.sender == nativeTokenWrapper, "caller not native token wrapper."); + } + + /// @dev Admin deposits reward tokens. + function depositRewardTokens(uint256 _amount) external payable virtual nonReentrant { + _depositRewardTokens(_amount); // override this for custom logic. + } + + /// @dev Admin can withdraw excess reward tokens. + function withdrawRewardTokens(uint256 _amount) external virtual nonReentrant { + _withdrawRewardTokens(_amount); // override this for custom logic. + } + + /// @notice View total rewards available in the staking contract. + function getRewardTokenBalance() external view virtual override returns (uint256) { + return rewardTokenBalance; + } + + /*////////////////////////////////////////////////////////////// + Minting logic + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Mint ERC20 rewards to the staker. Override for custom logic. + * + * @param _staker Address for which to calculated rewards. + * @param _rewards Amount of tokens to be given out as reward. + * + */ + function _mintRewards(address _staker, uint256 _rewards) internal virtual override { + require(_rewards <= rewardTokenBalance, "Not enough reward tokens"); + rewardTokenBalance -= _rewards; + CurrencyTransferLib.transferCurrencyWithWrapper( + rewardToken, + address(this), + _staker, + _rewards, + nativeTokenWrapper + ); + } + + /*////////////////////////////////////////////////////////////// + Other Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Admin deposits reward tokens -- override for custom logic. + function _depositRewardTokens(uint256 _amount) internal virtual { + require(msg.sender == owner(), "Not authorized"); + + address _rewardToken = rewardToken == CurrencyTransferLib.NATIVE_TOKEN ? nativeTokenWrapper : rewardToken; + + uint256 balanceBefore = IERC20(_rewardToken).balanceOf(address(this)); + CurrencyTransferLib.transferCurrencyWithWrapper( + rewardToken, + msg.sender, + address(this), + _amount, + nativeTokenWrapper + ); + uint256 actualAmount = IERC20(_rewardToken).balanceOf(address(this)) - balanceBefore; + + rewardTokenBalance += actualAmount; + } + + /// @dev Admin can withdraw excess reward tokens -- override for custom logic. + function _withdrawRewardTokens(uint256 _amount) internal virtual { + require(msg.sender == owner(), "Not authorized"); + + // to prevent locking of direct-transferred tokens + rewardTokenBalance = _amount > rewardTokenBalance ? 0 : rewardTokenBalance - _amount; + + CurrencyTransferLib.transferCurrencyWithWrapper( + rewardToken, + address(this), + msg.sender, + _amount, + nativeTokenWrapper + ); + + // The withdrawal shouldn't reduce staking token balance. `>=` accounts for any accidental transfers. + address _stakingToken = stakingToken == CurrencyTransferLib.NATIVE_TOKEN ? nativeTokenWrapper : stakingToken; + require( + IERC20(_stakingToken).balanceOf(address(this)) >= stakingTokenBalance, + "Staking token balance reduced." + ); + } + + /// @dev Returns whether staking restrictions can be set in given execution context. + function _canSetStakeConditions() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether owner can be set in the given execution context. + function _canSetOwner() internal view virtual override returns (bool) { + return msg.sender == owner(); + } +} diff --git a/contracts/base/Staking721Base.sol b/contracts/base/Staking721Base.sol new file mode 100644 index 000000000..ad1ebb19d --- /dev/null +++ b/contracts/base/Staking721Base.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../extension/ContractMetadata.sol"; +import "../extension/Multicall.sol"; +import "../extension/Ownable.sol"; +import "../extension/Staking721.sol"; + +import "../eip/ERC165.sol"; +import "../eip/interface/IERC20.sol"; +import "../eip/interface/IERC721Receiver.sol"; + +import { CurrencyTransferLib } from "../lib/CurrencyTransferLib.sol"; + +/** + * + * EXTENSION: Staking721 + * + * The `Staking721Base` smart contract implements NFT staking mechanism. + * Allows users to stake their ERC-721 NFTs and earn rewards in form of ERC-20 tokens. + * + * Following features and implementation setup must be noted: + * + * - ERC-721 NFTs from only one NFT collection can be staked. + * + * - Contract admin can choose to give out rewards by either transferring or minting the rewardToken, + * which is an ERC20 token. See {_mintRewards}. + * + * - To implement custom logic for staking, reward calculation, etc. corresponding functions can be + * overridden from the extension `Staking721`. + * + * - Ownership of the contract, with the ability to restrict certain functions to + * only be called by the contract's owner. + * + * - Multicall capability to perform multiple actions atomically. + * + */ + +/// note: This contract is provided as a base contract. +// This is to support a variety of use-cases that can be build on top of this base. +// +// Additional functionality such as deposit functions, reward-minting, etc. +// must be implemented by the deployer of this contract, as needed for their use-case. + +contract Staking721Base is ContractMetadata, Multicall, Ownable, Staking721, ERC165, IERC721Receiver { + /// @dev ERC20 Reward Token address. See {_mintRewards} below. + address public immutable rewardToken; + + /// @dev The address of the native token wrapper contract. + address public immutable nativeTokenWrapper; + + /// @dev Total amount of reward tokens in the contract. + uint256 private rewardTokenBalance; + + constructor( + address _defaultAdmin, + uint256 _timeUnit, + uint256 _rewardsPerUnitTime, + address _stakingToken, + address _rewardToken, + address _nativeTokenWrapper + ) Staking721(_stakingToken) { + _setupOwner(_defaultAdmin); + _setStakingCondition(_timeUnit, _rewardsPerUnitTime); + + rewardToken = _rewardToken; + nativeTokenWrapper = _nativeTokenWrapper; + } + + /// @dev Lets the contract receive ether to unwrap native tokens. + receive() external payable virtual { + require(msg.sender == nativeTokenWrapper, "caller not native token wrapper."); + } + + /// @dev Admin deposits reward tokens. + function depositRewardTokens(uint256 _amount) external payable virtual nonReentrant { + _depositRewardTokens(_amount); // override this for custom logic. + } + + /// @dev Admin can withdraw excess reward tokens. + function withdrawRewardTokens(uint256 _amount) external virtual nonReentrant { + _withdrawRewardTokens(_amount); // override this for custom logic. + } + + /// @notice View total rewards available in the staking contract. + function getRewardTokenBalance() external view virtual override returns (uint256) { + return rewardTokenBalance; + } + + /*/////////////////////////////////////////////////////////////// + ERC 165 / 721 logic + //////////////////////////////////////////////////////////////*/ + + function onERC721Received( + address, + address, + uint256, + bytes calldata + ) external view virtual override returns (bytes4) { + require(isStaking == 2, "Direct transfer"); + return this.onERC721Received.selector; + } + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == type(IERC721Receiver).interfaceId || super.supportsInterface(interfaceId); + } + + /*////////////////////////////////////////////////////////////// + Minting logic + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Mint ERC20 rewards to the staker. Override for custom logic. + * + * @param _staker Address for which to calculated rewards. + * @param _rewards Amount of tokens to be given out as reward. + * + */ + function _mintRewards(address _staker, uint256 _rewards) internal virtual override { + require(_rewards <= rewardTokenBalance, "Not enough reward tokens"); + rewardTokenBalance -= _rewards; + CurrencyTransferLib.transferCurrencyWithWrapper( + rewardToken, + address(this), + _staker, + _rewards, + nativeTokenWrapper + ); + } + + /*////////////////////////////////////////////////////////////// + Other Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Admin deposits reward tokens -- override for custom logic. + function _depositRewardTokens(uint256 _amount) internal virtual { + require(msg.sender == owner(), "Not authorized"); + + address _rewardToken = rewardToken == CurrencyTransferLib.NATIVE_TOKEN ? nativeTokenWrapper : rewardToken; + + uint256 balanceBefore = IERC20(_rewardToken).balanceOf(address(this)); + CurrencyTransferLib.transferCurrencyWithWrapper( + rewardToken, + msg.sender, + address(this), + _amount, + nativeTokenWrapper + ); + uint256 actualAmount = IERC20(_rewardToken).balanceOf(address(this)) - balanceBefore; + + rewardTokenBalance += actualAmount; + } + + /// @dev Admin can withdraw excess reward tokens -- override for custom logic. + function _withdrawRewardTokens(uint256 _amount) internal virtual { + require(msg.sender == owner(), "Not authorized"); + + // to prevent locking of direct-transferred tokens + rewardTokenBalance = _amount > rewardTokenBalance ? 0 : rewardTokenBalance - _amount; + + CurrencyTransferLib.transferCurrencyWithWrapper( + rewardToken, + address(this), + msg.sender, + _amount, + nativeTokenWrapper + ); + } + + /// @dev Returns whether staking restrictions can be set in given execution context. + function _canSetStakeConditions() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether owner can be set in the given execution context. + function _canSetOwner() internal view virtual override returns (bool) { + return msg.sender == owner(); + } +} diff --git a/contracts/drop/DropERC1155.sol b/contracts/drop/DropERC1155.sol deleted file mode 100644 index 04535a3c7..000000000 --- a/contracts/drop/DropERC1155.sol +++ /dev/null @@ -1,739 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -// ========== External imports ========== - -import "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; - -import "@openzeppelin/contracts-upgradeable/utils/structs/BitMapsUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; - -import "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; - -// ========== Internal imports ========== - -import "../interfaces/IThirdwebContract.sol"; - -// ========== Features ========== - -import "../extension/interface/IPlatformFee.sol"; -import "../extension/interface/IPrimarySale.sol"; -import "../extension/interface/IRoyalty.sol"; -import "../extension/interface/IOwnable.sol"; - -import { IDropERC1155 } from "../interfaces/drop/IDropERC1155.sol"; -import { ITWFee } from "../interfaces/ITWFee.sol"; - -import "../openzeppelin-presets/metatx/ERC2771ContextUpgradeable.sol"; - -import "../lib/CurrencyTransferLib.sol"; -import "../lib/FeeType.sol"; -import "../lib/MerkleProof.sol"; - -contract DropERC1155 is - Initializable, - IThirdwebContract, - IOwnable, - IRoyalty, - IPrimarySale, - IPlatformFee, - ReentrancyGuardUpgradeable, - ERC2771ContextUpgradeable, - MulticallUpgradeable, - AccessControlEnumerableUpgradeable, - ERC1155Upgradeable, - IDropERC1155 -{ - using BitMapsUpgradeable for BitMapsUpgradeable.BitMap; - using StringsUpgradeable for uint256; - - /*/////////////////////////////////////////////////////////////// - State variables - //////////////////////////////////////////////////////////////*/ - - bytes32 private constant MODULE_TYPE = bytes32("DropERC1155"); - uint256 private constant VERSION = 2; - - // Token name - string public name; - - // Token symbol - string public symbol; - - /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. - bytes32 private constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE"); - /// @dev Only MINTER_ROLE holders can lazy mint NFTs. - bytes32 private constant MINTER_ROLE = keccak256("MINTER_ROLE"); - - /// @dev Max bps in the thirdweb system - uint256 private constant MAX_BPS = 10_000; - - /// @dev Owner of the contract (purpose: OpenSea compatibility) - address private _owner; - - // @dev The next token ID of the NFT to "lazy mint". - uint256 public nextTokenIdToMint; - - /// @dev The address that receives all primary sales value. - address public primarySaleRecipient; - - /// @dev The address that receives all platform fees from all sales. - address private platformFeeRecipient; - - /// @dev The % of primary sales collected as platform fees. - uint16 private platformFeeBps; - - /// @dev The recipient of who gets the royalty. - address private royaltyRecipient; - - /// @dev The (default) address that receives all royalty value. - uint16 private royaltyBps; - - /// @dev Contract level metadata. - string public contractURI; - - /// @dev Largest tokenId of each batch of tokens with the same baseURI - uint256[] private baseURIIndices; - - /*/////////////////////////////////////////////////////////////// - Mappings - //////////////////////////////////////////////////////////////*/ - - /** - * @dev Mapping from 'Largest tokenId of a batch of tokens with the same baseURI' - * to base URI for the respective batch of tokens. - **/ - mapping(uint256 => string) private baseURI; - - /// @dev Mapping from token ID => total circulating supply of tokens with that ID. - mapping(uint256 => uint256) public totalSupply; - - /// @dev Mapping from token ID => maximum possible total circulating supply of tokens with that ID. - mapping(uint256 => uint256) public maxTotalSupply; - - /// @dev Mapping from token ID => the set of all claim conditions, at any given moment, for tokens of the token ID. - mapping(uint256 => ClaimConditionList) public claimCondition; - - /// @dev Mapping from token ID => the address of the recipient of primary sales. - mapping(uint256 => address) public saleRecipient; - - /// @dev Mapping from token ID => royalty recipient and bps for tokens of the token ID. - mapping(uint256 => RoyaltyInfo) private royaltyInfoForToken; - - /// @dev Mapping from token ID => claimer wallet address => total number of NFTs of the token ID a wallet has claimed. - mapping(uint256 => mapping(address => uint256)) public walletClaimCount; - - /// @dev Mapping from token ID => the max number of NFTs of the token ID a wallet can claim. - mapping(uint256 => uint256) public maxWalletClaimCount; - - /*/////////////////////////////////////////////////////////////// - Constructor + initializer logic - //////////////////////////////////////////////////////////////*/ - - constructor() initializer {} - - /// @dev Initiliazes the contract, like a constructor. - function initialize( - address _defaultAdmin, - string memory _name, - string memory _symbol, - string memory _contractURI, - address[] memory _trustedForwarders, - address _saleRecipient, - address _royaltyRecipient, - uint128 _royaltyBps, - uint128 _platformFeeBps, - address _platformFeeRecipient - ) external initializer { - // Initialize inherited contracts, most base-like -> most derived. - __ReentrancyGuard_init(); - __ERC2771Context_init_unchained(_trustedForwarders); - __ERC1155_init_unchained(""); - - // Initialize this contract's state. - name = _name; - symbol = _symbol; - royaltyRecipient = _royaltyRecipient; - royaltyBps = uint16(_royaltyBps); - platformFeeRecipient = _platformFeeRecipient; - primarySaleRecipient = _saleRecipient; - contractURI = _contractURI; - platformFeeBps = uint16(_platformFeeBps); - _owner = _defaultAdmin; - - _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); - _setupRole(MINTER_ROLE, _defaultAdmin); - _setupRole(TRANSFER_ROLE, _defaultAdmin); - _setupRole(TRANSFER_ROLE, address(0)); - } - - /*/////////////////////////////////////////////////////////////// - Generic contract logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns the type of the contract. - function contractType() external pure returns (bytes32) { - return MODULE_TYPE; - } - - /// @dev Returns the version of the contract. - function contractVersion() external pure returns (uint8) { - return uint8(VERSION); - } - - /** - * @dev Returns the address of the current owner. - */ - function owner() public view returns (address) { - return hasRole(DEFAULT_ADMIN_ROLE, _owner) ? _owner : address(0); - } - - /*/////////////////////////////////////////////////////////////// - ERC 165 / 1155 / 2981 logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns the URI for a given tokenId. - function uri(uint256 _tokenId) public view override returns (string memory _tokenURI) { - for (uint256 i = 0; i < baseURIIndices.length; i += 1) { - if (_tokenId < baseURIIndices[i]) { - return string(abi.encodePacked(baseURI[baseURIIndices[i]], _tokenId.toString())); - } - } - - return ""; - } - - /// @dev See ERC 165 - function supportsInterface(bytes4 interfaceId) - public - view - virtual - override(ERC1155Upgradeable, AccessControlEnumerableUpgradeable, IERC165Upgradeable, IERC165) - returns (bool) - { - return super.supportsInterface(interfaceId) || type(IERC2981Upgradeable).interfaceId == interfaceId; - } - - /// @dev Returns the royalty recipient and amount, given a tokenId and sale price. - function royaltyInfo(uint256 tokenId, uint256 salePrice) - external - view - virtual - returns (address receiver, uint256 royaltyAmount) - { - (address recipient, uint256 bps) = getRoyaltyInfoForToken(tokenId); - receiver = recipient; - royaltyAmount = (salePrice * bps) / MAX_BPS; - } - - /*/////////////////////////////////////////////////////////////// - Minting logic - //////////////////////////////////////////////////////////////*/ - - /** - * @dev Lets an account with `MINTER_ROLE` lazy mint 'n' NFTs. - * The URIs for each token is the provided `_baseURIForTokens` + `{tokenId}`. - */ - function lazyMint(uint256 _amount, string calldata _baseURIForTokens) external onlyRole(MINTER_ROLE) { - uint256 startId = nextTokenIdToMint; - uint256 baseURIIndex = startId + _amount; - - nextTokenIdToMint = baseURIIndex; - baseURI[baseURIIndex] = _baseURIForTokens; - baseURIIndices.push(baseURIIndex); - - emit TokensLazyMinted(startId, startId + _amount - 1, _baseURIForTokens); - } - - /*/////////////////////////////////////////////////////////////// - Claim logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Lets an account claim a given quantity of NFTs, of a single tokenId. - function claim( - address _receiver, - uint256 _tokenId, - uint256 _quantity, - address _currency, - uint256 _pricePerToken, - bytes32[] calldata _proofs, - uint256 _proofMaxQuantityPerTransaction - ) external payable nonReentrant { - require(isTrustedForwarder(msg.sender) || _msgSender() == tx.origin, "BOT"); - - // Get the active claim condition index. - uint256 activeConditionId = getActiveClaimConditionId(_tokenId); - - /** - * We make allowlist checks (i.e. verifyClaimMerkleProof) before verifying the claim's general - * validity (i.e. verifyClaim) because we give precedence to the check of allow list quantity - * restriction over the check of the general claim condition's quantityLimitPerTransaction - * restriction. - */ - - // Verify inclusion in allowlist. - (bool validMerkleProof, uint256 merkleProofIndex) = verifyClaimMerkleProof( - activeConditionId, - _msgSender(), - _tokenId, - _quantity, - _proofs, - _proofMaxQuantityPerTransaction - ); - - // Verify claim validity. If not valid, revert. - // when there's allowlist present --> verifyClaimMerkleProof will verify the _proofMaxQuantityPerTransaction value with hashed leaf in the allowlist - // when there's no allowlist, this check is true --> verifyClaim will check for _quantity being less/equal than the limit - bool toVerifyMaxQuantityPerTransaction = _proofMaxQuantityPerTransaction == 0 || - claimCondition[_tokenId].phases[activeConditionId].merkleRoot == bytes32(0); - verifyClaim( - activeConditionId, - _msgSender(), - _tokenId, - _quantity, - _currency, - _pricePerToken, - toVerifyMaxQuantityPerTransaction - ); - - if (validMerkleProof && _proofMaxQuantityPerTransaction > 0) { - /** - * Mark the claimer's use of their position in the allowlist. A spot in an allowlist - * can be used only once. - */ - claimCondition[_tokenId].limitMerkleProofClaim[activeConditionId].set(merkleProofIndex); - } - - // If there's a price, collect price. - collectClaimPrice(_quantity, _currency, _pricePerToken, _tokenId); - - // Mint the relevant tokens to claimer. - transferClaimedTokens(_receiver, activeConditionId, _tokenId, _quantity); - - emit TokensClaimed(activeConditionId, _tokenId, _msgSender(), _receiver, _quantity); - } - - /// @dev Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions, for a tokenId. - function setClaimConditions( - uint256 _tokenId, - ClaimCondition[] calldata _phases, - bool _resetClaimEligibility - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - ClaimConditionList storage condition = claimCondition[_tokenId]; - uint256 existingStartIndex = condition.currentStartId; - uint256 existingPhaseCount = condition.count; - - /** - * `limitLastClaimTimestamp` and `limitMerkleProofClaim` are mappings that use a - * claim condition's UID as a key. - * - * If `_resetClaimEligibility == true`, we assign completely new UIDs to the claim - * conditions in `_phases`, effectively resetting the restrictions on claims expressed - * by `limitLastClaimTimestamp` and `limitMerkleProofClaim`. - */ - uint256 newStartIndex = existingStartIndex; - if (_resetClaimEligibility) { - newStartIndex = existingStartIndex + existingPhaseCount; - } - - condition.count = _phases.length; - condition.currentStartId = newStartIndex; - - uint256 lastConditionStartTimestamp; - for (uint256 i = 0; i < _phases.length; i++) { - require( - i == 0 || lastConditionStartTimestamp < _phases[i].startTimestamp, - "startTimestamp must be in ascending order." - ); - - uint256 supplyClaimedAlready = condition.phases[newStartIndex + i].supplyClaimed; - require(supplyClaimedAlready <= _phases[i].maxClaimableSupply, "max supply claimed already"); - - condition.phases[newStartIndex + i] = _phases[i]; - condition.phases[newStartIndex + i].supplyClaimed = supplyClaimedAlready; - - lastConditionStartTimestamp = _phases[i].startTimestamp; - } - - /** - * Gas refunds (as much as possible) - * - * If `_resetClaimEligibility == true`, we assign completely new UIDs to the claim - * conditions in `_phases`. So, we delete claim conditions with UID < `newStartIndex`. - * - * If `_resetClaimEligibility == false`, and there are more existing claim conditions - * than in `_phases`, we delete the existing claim conditions that don't get replaced - * by the conditions in `_phases`. - */ - if (_resetClaimEligibility) { - for (uint256 i = existingStartIndex; i < newStartIndex; i++) { - delete condition.phases[i]; - delete condition.limitMerkleProofClaim[i]; - } - } else { - if (existingPhaseCount > _phases.length) { - for (uint256 i = _phases.length; i < existingPhaseCount; i++) { - delete condition.phases[newStartIndex + i]; - delete condition.limitMerkleProofClaim[newStartIndex + i]; - } - } - } - - emit ClaimConditionsUpdated(_tokenId, _phases); - } - - /// @dev Collects and distributes the primary sale value of NFTs being claimed. - function collectClaimPrice( - uint256 _quantityToClaim, - address _currency, - uint256 _pricePerToken, - uint256 _tokenId - ) internal { - if (_pricePerToken == 0) { - return; - } - - uint256 totalPrice = _quantityToClaim * _pricePerToken; - uint256 platformFees = (totalPrice * platformFeeBps) / MAX_BPS; - - if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { - require(msg.value == totalPrice, "must send total price."); - } - - address recipient = saleRecipient[_tokenId] == address(0) ? primarySaleRecipient : saleRecipient[_tokenId]; - CurrencyTransferLib.transferCurrency(_currency, _msgSender(), platformFeeRecipient, platformFees); - CurrencyTransferLib.transferCurrency(_currency, _msgSender(), recipient, totalPrice - platformFees); - } - - /// @dev Transfers the NFTs being claimed. - function transferClaimedTokens( - address _to, - uint256 _conditionId, - uint256 _tokenId, - uint256 _quantityBeingClaimed - ) internal { - // Update the supply minted under mint condition. - claimCondition[_tokenId].phases[_conditionId].supplyClaimed += _quantityBeingClaimed; - - // if transfer claimed tokens is called when to != msg.sender, it'd use msg.sender's limits. - // behavior would be similar to msg.sender mint for itself, then transfer to `to`. - claimCondition[_tokenId].limitLastClaimTimestamp[_conditionId][_msgSender()] = block.timestamp; - - walletClaimCount[_tokenId][_msgSender()] += _quantityBeingClaimed; - - _mint(_to, _tokenId, _quantityBeingClaimed, ""); - } - - /// @dev Checks a request to claim NFTs against the active claim condition's criteria. - function verifyClaim( - uint256 _conditionId, - address _claimer, - uint256 _tokenId, - uint256 _quantity, - address _currency, - uint256 _pricePerToken, - bool verifyMaxQuantityPerTransaction - ) public view { - ClaimCondition memory currentClaimPhase = claimCondition[_tokenId].phases[_conditionId]; - - require( - _currency == currentClaimPhase.currency && _pricePerToken == currentClaimPhase.pricePerToken, - "invalid currency or price specified." - ); - // If we're checking for an allowlist quantity restriction, ignore the general quantity restriction. - require( - _quantity > 0 && - (!verifyMaxQuantityPerTransaction || _quantity <= currentClaimPhase.quantityLimitPerTransaction), - "invalid quantity claimed." - ); - require( - currentClaimPhase.supplyClaimed + _quantity <= currentClaimPhase.maxClaimableSupply, - "exceed max mint supply." - ); - require( - maxTotalSupply[_tokenId] == 0 || totalSupply[_tokenId] + _quantity <= maxTotalSupply[_tokenId], - "exceed max total supply" - ); - require( - maxWalletClaimCount[_tokenId] == 0 || - walletClaimCount[_tokenId][_claimer] + _quantity <= maxWalletClaimCount[_tokenId], - "exceed claim limit for wallet" - ); - - (uint256 lastClaimTimestamp, uint256 nextValidClaimTimestamp) = getClaimTimestamp( - _tokenId, - _conditionId, - _claimer - ); - require(lastClaimTimestamp == 0 || block.timestamp >= nextValidClaimTimestamp, "cannot claim yet."); - } - - /// @dev Checks whether a claimer meets the claim condition's allowlist criteria. - function verifyClaimMerkleProof( - uint256 _conditionId, - address _claimer, - uint256 _tokenId, - uint256 _quantity, - bytes32[] calldata _proofs, - uint256 _proofMaxQuantityPerTransaction - ) public view returns (bool validMerkleProof, uint256 merkleProofIndex) { - ClaimCondition memory currentClaimPhase = claimCondition[_tokenId].phases[_conditionId]; - - if (currentClaimPhase.merkleRoot != bytes32(0)) { - (validMerkleProof, merkleProofIndex) = MerkleProof.verify( - _proofs, - currentClaimPhase.merkleRoot, - keccak256(abi.encodePacked(_claimer, _proofMaxQuantityPerTransaction)) - ); - require(validMerkleProof, "not in whitelist."); - require( - !claimCondition[_tokenId].limitMerkleProofClaim[_conditionId].get(merkleProofIndex), - "proof claimed." - ); - require( - _proofMaxQuantityPerTransaction == 0 || _quantity <= _proofMaxQuantityPerTransaction, - "invalid quantity proof." - ); - } - } - - /*/////////////////////////////////////////////////////////////// - Getter functions - //////////////////////////////////////////////////////////////*/ - - /// @dev At any given moment, returns the uid for the active claim condition, for a given tokenId. - function getActiveClaimConditionId(uint256 _tokenId) public view returns (uint256) { - ClaimConditionList storage conditionList = claimCondition[_tokenId]; - for (uint256 i = conditionList.currentStartId + conditionList.count; i > conditionList.currentStartId; i--) { - if (block.timestamp >= conditionList.phases[i - 1].startTimestamp) { - return i - 1; - } - } - - revert("no active mint condition."); - } - - /// @dev Returns the platform fee recipient and bps. - function getPlatformFeeInfo() external view returns (address, uint16) { - return (platformFeeRecipient, uint16(platformFeeBps)); - } - - /// @dev Returns the default royalty recipient and bps. - function getDefaultRoyaltyInfo() external view returns (address, uint16) { - return (royaltyRecipient, uint16(royaltyBps)); - } - - /// @dev Returns the royalty recipient and bps for a particular token Id. - function getRoyaltyInfoForToken(uint256 _tokenId) public view returns (address, uint16) { - RoyaltyInfo memory royaltyForToken = royaltyInfoForToken[_tokenId]; - - return - royaltyForToken.recipient == address(0) - ? (royaltyRecipient, uint16(royaltyBps)) - : (royaltyForToken.recipient, uint16(royaltyForToken.bps)); - } - - /// @dev Returns the timestamp for when a claimer is eligible for claiming NFTs again. - function getClaimTimestamp( - uint256 _tokenId, - uint256 _conditionId, - address _claimer - ) public view returns (uint256 lastClaimTimestamp, uint256 nextValidClaimTimestamp) { - lastClaimTimestamp = claimCondition[_tokenId].limitLastClaimTimestamp[_conditionId][_claimer]; - - unchecked { - nextValidClaimTimestamp = - lastClaimTimestamp + - claimCondition[_tokenId].phases[_conditionId].waitTimeInSecondsBetweenClaims; - - if (nextValidClaimTimestamp < lastClaimTimestamp) { - nextValidClaimTimestamp = type(uint256).max; - } - } - } - - /// @dev Returns the claim condition at the given uid. - function getClaimConditionById(uint256 _tokenId, uint256 _conditionId) - external - view - returns (ClaimCondition memory condition) - { - condition = claimCondition[_tokenId].phases[_conditionId]; - } - - /*/////////////////////////////////////////////////////////////// - Setter functions - //////////////////////////////////////////////////////////////*/ - - /// @dev Lets a contract admin set a claim count for a wallet. - function setWalletClaimCount( - uint256 _tokenId, - address _claimer, - uint256 _count - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - walletClaimCount[_tokenId][_claimer] = _count; - emit WalletClaimCountUpdated(_tokenId, _claimer, _count); - } - - /// @dev Lets a contract admin set a maximum number of NFTs of a tokenId that can be claimed by any wallet. - function setMaxWalletClaimCount(uint256 _tokenId, uint256 _count) external onlyRole(DEFAULT_ADMIN_ROLE) { - maxWalletClaimCount[_tokenId] = _count; - emit MaxWalletClaimCountUpdated(_tokenId, _count); - } - - /// @dev Lets a module admin set a max total supply for token. - function setMaxTotalSupply(uint256 _tokenId, uint256 _maxTotalSupply) external onlyRole(DEFAULT_ADMIN_ROLE) { - maxTotalSupply[_tokenId] = _maxTotalSupply; - emit MaxTotalSupplyUpdated(_tokenId, _maxTotalSupply); - } - - /// @dev Lets a contract admin set the recipient for all primary sales. - function setPrimarySaleRecipient(address _saleRecipient) external onlyRole(DEFAULT_ADMIN_ROLE) { - primarySaleRecipient = _saleRecipient; - emit PrimarySaleRecipientUpdated(_saleRecipient); - } - - /// @dev Lets a contract admin set the recipient for all primary sales. - function setSaleRecipientForToken(uint256 _tokenId, address _saleRecipient) external onlyRole(DEFAULT_ADMIN_ROLE) { - saleRecipient[_tokenId] = _saleRecipient; - emit SaleRecipientForTokenUpdated(_tokenId, _saleRecipient); - } - - /// @dev Lets a contract admin update the default royalty recipient and bps. - function setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) - external - onlyRole(DEFAULT_ADMIN_ROLE) - { - require(_royaltyBps <= MAX_BPS, "exceed royalty bps"); - - royaltyRecipient = _royaltyRecipient; - royaltyBps = uint16(_royaltyBps); - - emit DefaultRoyalty(_royaltyRecipient, _royaltyBps); - } - - /// @dev Lets a contract admin set the royalty recipient and bps for a particular token Id. - function setRoyaltyInfoForToken( - uint256 _tokenId, - address _recipient, - uint256 _bps - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(_bps <= MAX_BPS, "exceed royalty bps"); - - royaltyInfoForToken[_tokenId] = RoyaltyInfo({ recipient: _recipient, bps: _bps }); - - emit RoyaltyForToken(_tokenId, _recipient, _bps); - } - - /// @dev Lets a contract admin update the platform fee recipient and bps - function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) - external - onlyRole(DEFAULT_ADMIN_ROLE) - { - require(_platformFeeBps <= MAX_BPS, "bps <= 10000."); - - platformFeeBps = uint16(_platformFeeBps); - platformFeeRecipient = _platformFeeRecipient; - - emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); - } - - /// @dev Lets a contract admin set a new owner for the contract. The new owner must be a contract admin. - function setOwner(address _newOwner) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(hasRole(DEFAULT_ADMIN_ROLE, _newOwner), "new owner not module admin."); - emit OwnerUpdated(_owner, _newOwner); - _owner = _newOwner; - } - - /// @dev Lets a contract admin set the URI for contract-level metadata. - function setContractURI(string calldata _uri) external onlyRole(DEFAULT_ADMIN_ROLE) { - contractURI = _uri; - } - - /*/////////////////////////////////////////////////////////////// - Miscellaneous - //////////////////////////////////////////////////////////////*/ - - /// @dev Lets a token owner burn the tokens they own (i.e. destroy for good) - function burn( - address account, - uint256 id, - uint256 value - ) public virtual { - require( - account == _msgSender() || isApprovedForAll(account, _msgSender()), - "ERC1155: caller is not owner nor approved." - ); - - _burn(account, id, value); - } - - /// @dev Lets a token owner burn multiple tokens they own at once (i.e. destroy for good) - function burnBatch( - address account, - uint256[] memory ids, - uint256[] memory values - ) public virtual { - require( - account == _msgSender() || isApprovedForAll(account, _msgSender()), - "ERC1155: caller is not owner nor approved." - ); - - _burnBatch(account, ids, values); - } - - /** - * @dev See {ERC1155-_beforeTokenTransfer}. - */ - function _beforeTokenTransfer( - address operator, - address from, - address to, - uint256[] memory ids, - uint256[] memory amounts, - bytes memory data - ) internal virtual override { - super._beforeTokenTransfer(operator, from, to, ids, amounts, data); - - // if transfer is restricted on the contract, we still want to allow burning and minting - if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { - require(hasRole(TRANSFER_ROLE, from) || hasRole(TRANSFER_ROLE, to), "restricted to TRANSFER_ROLE holders."); - } - - if (from == address(0)) { - for (uint256 i = 0; i < ids.length; ++i) { - totalSupply[ids[i]] += amounts[i]; - } - } - - if (to == address(0)) { - for (uint256 i = 0; i < ids.length; ++i) { - totalSupply[ids[i]] -= amounts[i]; - } - } - } - - function _msgSender() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (address sender) - { - return ERC2771ContextUpgradeable._msgSender(); - } - - function _msgData() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (bytes calldata) - { - return ERC2771ContextUpgradeable._msgData(); - } -} diff --git a/contracts/drop/DropERC20.sol b/contracts/drop/DropERC20.sol deleted file mode 100644 index 7a69dd045..000000000 --- a/contracts/drop/DropERC20.sol +++ /dev/null @@ -1,525 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -// ========== External imports ========== - -import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20VotesUpgradeable.sol"; - -import "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; - -import "@openzeppelin/contracts-upgradeable/utils/structs/BitMapsUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; - -// ========== Internal imports ========== - -import "../interfaces/IThirdwebContract.sol"; - -// ========== Features ========== - -import "../extension/interface/IPlatformFee.sol"; -import "../extension/interface/IPrimarySale.sol"; - -import { IDropERC20 } from "../interfaces/drop/IDropERC20.sol"; -import { ITWFee } from "../interfaces/ITWFee.sol"; - -import "../openzeppelin-presets/metatx/ERC2771ContextUpgradeable.sol"; - -import "../lib/MerkleProof.sol"; -import "../lib/CurrencyTransferLib.sol"; -import "../lib/FeeType.sol"; - -contract DropERC20 is - Initializable, - IThirdwebContract, - IPrimarySale, - IPlatformFee, - ReentrancyGuardUpgradeable, - ERC2771ContextUpgradeable, - MulticallUpgradeable, - AccessControlEnumerableUpgradeable, - ERC20BurnableUpgradeable, - ERC20VotesUpgradeable, - IDropERC20 -{ - using BitMapsUpgradeable for BitMapsUpgradeable.BitMap; - - /*/////////////////////////////////////////////////////////////// - State variables - //////////////////////////////////////////////////////////////*/ - - bytes32 private constant MODULE_TYPE = bytes32("DropERC20"); - uint128 private constant VERSION = 1; - - /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. - bytes32 private constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE"); - - /// @dev Contract level metadata. - string public contractURI; - - /// @dev Max bps in the thirdweb system. - uint128 internal constant MAX_BPS = 10_000; - - /// @dev The % of primary sales collected as platform fees. - uint128 internal platformFeeBps; - - /// @dev The address that receives all platform fees from all sales. - address internal platformFeeRecipient; - - /// @dev The address that receives all primary sales value. - address public primarySaleRecipient; - - /// @dev The max number of tokens a wallet can claim. - uint256 public maxWalletClaimCount; - - /// @dev Global max total supply of tokens. - uint256 public maxTotalSupply; - - /// @dev The set of all claim conditions, at any given moment. - ClaimConditionList public claimCondition; - - /*/////////////////////////////////////////////////////////////// - Mappings - //////////////////////////////////////////////////////////////*/ - - /// @dev Mapping from address => number of tokens a wallet has claimed. - mapping(address => uint256) public walletClaimCount; - - /*/////////////////////////////////////////////////////////////// - Constructor + initializer logic - //////////////////////////////////////////////////////////////*/ - - constructor() initializer {} - - /// @dev Initiliazes the contract, like a constructor. - function initialize( - address _defaultAdmin, - string memory _name, - string memory _symbol, - string memory _contractURI, - address[] memory _trustedForwarders, - address _primarySaleRecipient, - address _platformFeeRecipient, - uint256 _platformFeeBps - ) external initializer { - // Initialize inherited contracts, most base-like -> most derived. - __ERC2771Context_init_unchained(_trustedForwarders); - __ERC20Permit_init(_name); - __ERC20_init_unchained(_name, _symbol); - - // Initialize this contract's state. - contractURI = _contractURI; - primarySaleRecipient = _primarySaleRecipient; - platformFeeRecipient = _platformFeeRecipient; - platformFeeBps = uint128(_platformFeeBps); - - _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); - _setupRole(TRANSFER_ROLE, _defaultAdmin); - _setupRole(TRANSFER_ROLE, address(0)); - } - - /*/////////////////////////////////////////////////////////////// - Generic contract logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns the type of the contract. - function contractType() external pure returns (bytes32) { - return MODULE_TYPE; - } - - /// @dev Returns the version of the contract. - function contractVersion() external pure returns (uint8) { - return uint8(VERSION); - } - - /*/////////////////////////////////////////////////////////////// - ERC 165 + ERC20 transfer hooks - //////////////////////////////////////////////////////////////*/ - - /// @dev See ERC 165 - function supportsInterface(bytes4 interfaceId) - public - view - virtual - override(AccessControlEnumerableUpgradeable) - returns (bool) - { - return super.supportsInterface(interfaceId); - } - - function _afterTokenTransfer( - address from, - address to, - uint256 amount - ) internal virtual override(ERC20Upgradeable, ERC20VotesUpgradeable) { - super._afterTokenTransfer(from, to, amount); - } - - /// @dev Runs on every transfer. - function _beforeTokenTransfer( - address from, - address to, - uint256 amount - ) internal override(ERC20Upgradeable) { - super._beforeTokenTransfer(from, to, amount); - - if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { - require(hasRole(TRANSFER_ROLE, from) || hasRole(TRANSFER_ROLE, to), "transfers restricted."); - } - } - - /*/////////////////////////////////////////////////////////////// - Claim logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Lets an account claim tokens. - function claim( - address _receiver, - uint256 _quantity, - address _currency, - uint256 _pricePerToken, - bytes32[] calldata _proofs, - uint256 _proofMaxQuantityPerTransaction - ) external payable nonReentrant { - require(isTrustedForwarder(msg.sender) || _msgSender() == tx.origin, "BOT"); - - // Get the claim conditions. - uint256 activeConditionId = getActiveClaimConditionId(); - - /** - * We make allowlist checks (i.e. verifyClaimMerkleProof) before verifying the claim's general - * validity (i.e. verifyClaim) because we give precedence to the check of allow list quantity - * restriction over the check of the general claim condition's quantityLimitPerTransaction - * restriction. - */ - - // Verify inclusion in allowlist. - (bool validMerkleProof, uint256 merkleProofIndex) = verifyClaimMerkleProof( - activeConditionId, - _msgSender(), - _quantity, - _proofs, - _proofMaxQuantityPerTransaction - ); - - // Verify claim validity. If not valid, revert. - // when there's allowlist present --> verifyClaimMerkleProof will verify the _proofMaxQuantityPerTransaction value with hashed leaf in the allowlist - // when there's no allowlist, this check is true --> verifyClaim will check for _quantity being less/equal than the limit - bool toVerifyMaxQuantityPerTransaction = _proofMaxQuantityPerTransaction == 0 || - claimCondition.phases[activeConditionId].merkleRoot == bytes32(0); - verifyClaim( - activeConditionId, - _msgSender(), - _quantity, - _currency, - _pricePerToken, - toVerifyMaxQuantityPerTransaction - ); - - if (validMerkleProof && _proofMaxQuantityPerTransaction > 0) { - /** - * Mark the claimer's use of their position in the allowlist. A spot in an allowlist - * can be used only once. - */ - claimCondition.limitMerkleProofClaim[activeConditionId].set(merkleProofIndex); - } - - // If there's a price, collect price. - collectClaimPrice(_quantity, _currency, _pricePerToken); - - // Mint the relevant NFTs to claimer. - transferClaimedTokens(_receiver, activeConditionId, _quantity); - - emit TokensClaimed(activeConditionId, _msgSender(), _receiver, _quantity); - } - - /// @dev Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. - function setClaimConditions(ClaimCondition[] calldata _phases, bool _resetClaimEligibility) - external - onlyRole(DEFAULT_ADMIN_ROLE) - { - uint256 existingStartIndex = claimCondition.currentStartId; - uint256 existingPhaseCount = claimCondition.count; - - /** - * `limitLastClaimTimestamp` and `limitMerkleProofClaim` are mappings that use a - * claim condition's UID as a key. - * - * If `_resetClaimEligibility == true`, we assign completely new UIDs to the claim - * conditions in `_phases`, effectively resetting the restrictions on claims expressed - * by `limitLastClaimTimestamp` and `limitMerkleProofClaim`. - */ - uint256 newStartIndex = existingStartIndex; - if (_resetClaimEligibility) { - newStartIndex = existingStartIndex + existingPhaseCount; - } - - claimCondition.count = _phases.length; - claimCondition.currentStartId = newStartIndex; - - uint256 lastConditionStartTimestamp; - for (uint256 i = 0; i < _phases.length; i++) { - require( - i == 0 || lastConditionStartTimestamp < _phases[i].startTimestamp, - "startTimestamp must be in ascending order." - ); - - uint256 supplyClaimedAlready = claimCondition.phases[newStartIndex + i].supplyClaimed; - require(supplyClaimedAlready <= _phases[i].maxClaimableSupply, "max supply claimed already"); - - claimCondition.phases[newStartIndex + i] = _phases[i]; - claimCondition.phases[newStartIndex + i].supplyClaimed = supplyClaimedAlready; - - lastConditionStartTimestamp = _phases[i].startTimestamp; - } - - /** - * Gas refunds (as much as possible) - * - * If `_resetClaimEligibility == true`, we assign completely new UIDs to the claim - * conditions in `_phases`. So, we delete claim conditions with UID < `newStartIndex`. - * - * If `_resetClaimEligibility == false`, and there are more existing claim conditions - * than in `_phases`, we delete the existing claim conditions that don't get replaced - * by the conditions in `_phases`. - */ - if (_resetClaimEligibility) { - for (uint256 i = existingStartIndex; i < newStartIndex; i++) { - delete claimCondition.phases[i]; - delete claimCondition.limitMerkleProofClaim[i]; - } - } else { - if (existingPhaseCount > _phases.length) { - for (uint256 i = _phases.length; i < existingPhaseCount; i++) { - delete claimCondition.phases[newStartIndex + i]; - delete claimCondition.limitMerkleProofClaim[newStartIndex + i]; - } - } - } - - emit ClaimConditionsUpdated(_phases); - } - - /// @dev Collects and distributes the primary sale value of tokens being claimed. - function collectClaimPrice( - uint256 _quantityToClaim, - address _currency, - uint256 _pricePerToken - ) internal { - if (_pricePerToken == 0) { - return; - } - - // `_pricePerToken` is interpreted as price per 1 ether unit of the ERC20 tokens. - uint256 totalPrice = (_quantityToClaim * _pricePerToken) / 1 ether; - uint256 platformFees = (totalPrice * platformFeeBps) / MAX_BPS; - - if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { - require(msg.value == totalPrice, "must send total price."); - } - - CurrencyTransferLib.transferCurrency(_currency, _msgSender(), platformFeeRecipient, platformFees); - CurrencyTransferLib.transferCurrency(_currency, _msgSender(), primarySaleRecipient, totalPrice - platformFees); - } - - /// @dev Transfers the tokens being claimed. - function transferClaimedTokens( - address _to, - uint256 _conditionId, - uint256 _quantityBeingClaimed - ) internal { - // Update the supply minted under mint condition. - claimCondition.phases[_conditionId].supplyClaimed += _quantityBeingClaimed; - - // if transfer claimed tokens is called when to != msg.sender, it'd use msg.sender's limits. - // behavior would be similar to msg.sender mint for itself, then transfer to `to`. - claimCondition.limitLastClaimTimestamp[_conditionId][_msgSender()] = block.timestamp; - walletClaimCount[_msgSender()] += _quantityBeingClaimed; - - _mint(_to, _quantityBeingClaimed); - } - - /// @dev Checks a request to claim tokens against the active claim condition's criteria. - function verifyClaim( - uint256 _conditionId, - address _claimer, - uint256 _quantity, - address _currency, - uint256 _pricePerToken, - bool verifyMaxQuantityPerTransaction - ) public view { - ClaimCondition memory currentClaimPhase = claimCondition.phases[_conditionId]; - - require( - _currency == currentClaimPhase.currency && _pricePerToken == currentClaimPhase.pricePerToken, - "invalid currency or price specified." - ); - // If we're checking for an allowlist quantity restriction, ignore the general quantity restriction. - require( - _quantity > 0 && - (!verifyMaxQuantityPerTransaction || _quantity <= currentClaimPhase.quantityLimitPerTransaction), - "invalid quantity claimed." - ); - require( - currentClaimPhase.supplyClaimed + _quantity <= currentClaimPhase.maxClaimableSupply, - "exceed max mint supply." - ); - require(maxTotalSupply == 0 || totalSupply() + _quantity <= maxTotalSupply, "exceed max total supply."); - require( - maxWalletClaimCount == 0 || walletClaimCount[_claimer] + _quantity <= maxWalletClaimCount, - "exceed claim limit for wallet" - ); - - (uint256 lastClaimTimestamp, uint256 nextValidClaimTimestamp) = getClaimTimestamp(_conditionId, _claimer); - require(lastClaimTimestamp == 0 || block.timestamp >= nextValidClaimTimestamp, "cannot claim yet."); - } - - /// @dev Checks whether a claimer meets the claim condition's allowlist criteria. - function verifyClaimMerkleProof( - uint256 _conditionId, - address _claimer, - uint256 _quantity, - bytes32[] calldata _proofs, - uint256 _proofMaxQuantityPerTransaction - ) public view returns (bool validMerkleProof, uint256 merkleProofIndex) { - ClaimCondition memory currentClaimPhase = claimCondition.phases[_conditionId]; - - if (currentClaimPhase.merkleRoot != bytes32(0)) { - (validMerkleProof, merkleProofIndex) = MerkleProof.verify( - _proofs, - currentClaimPhase.merkleRoot, - keccak256(abi.encodePacked(_claimer, _proofMaxQuantityPerTransaction)) - ); - require(validMerkleProof, "not in whitelist."); - require(!claimCondition.limitMerkleProofClaim[_conditionId].get(merkleProofIndex), "proof claimed."); - require( - _proofMaxQuantityPerTransaction == 0 || _quantity <= _proofMaxQuantityPerTransaction, - "invalid quantity proof." - ); - } - } - - /*/////////////////////////////////////////////////////////////// - Getter functions - //////////////////////////////////////////////////////////////*/ - - /// @dev At any given moment, returns the uid for the active claim condition. - function getActiveClaimConditionId() public view returns (uint256) { - for (uint256 i = claimCondition.currentStartId + claimCondition.count; i > claimCondition.currentStartId; i--) { - if (block.timestamp >= claimCondition.phases[i - 1].startTimestamp) { - return i - 1; - } - } - - revert("no active mint condition."); - } - - /// @dev Returns the timestamp for when a claimer is eligible for claiming tokens again. - function getClaimTimestamp(uint256 _conditionId, address _claimer) - public - view - returns (uint256 lastClaimTimestamp, uint256 nextValidClaimTimestamp) - { - lastClaimTimestamp = claimCondition.limitLastClaimTimestamp[_conditionId][_claimer]; - - unchecked { - nextValidClaimTimestamp = - lastClaimTimestamp + - claimCondition.phases[_conditionId].waitTimeInSecondsBetweenClaims; - - if (nextValidClaimTimestamp < lastClaimTimestamp) { - nextValidClaimTimestamp = type(uint256).max; - } - } - } - - /// @dev Returns the claim condition at the given uid. - function getClaimConditionById(uint256 _conditionId) external view returns (ClaimCondition memory condition) { - condition = claimCondition.phases[_conditionId]; - } - - /// @dev Returns the platform fee recipient and bps. - function getPlatformFeeInfo() external view returns (address, uint16) { - return (platformFeeRecipient, uint16(platformFeeBps)); - } - - /*/////////////////////////////////////////////////////////////// - Setter functions - //////////////////////////////////////////////////////////////*/ - - /// @dev Lets a contract admin set a claim count for a wallet. - function setWalletClaimCount(address _claimer, uint256 _count) external onlyRole(DEFAULT_ADMIN_ROLE) { - walletClaimCount[_claimer] = _count; - emit WalletClaimCountUpdated(_claimer, _count); - } - - /// @dev Lets a contract admin set a maximum number of tokens that can be claimed by any wallet. - function setMaxWalletClaimCount(uint256 _count) external onlyRole(DEFAULT_ADMIN_ROLE) { - maxWalletClaimCount = _count; - emit MaxWalletClaimCountUpdated(_count); - } - - /// @dev Lets a contract admin set the global maximum supply of tokens. - function setMaxTotalSupply(uint256 _maxTotalSupply) external onlyRole(DEFAULT_ADMIN_ROLE) { - maxTotalSupply = _maxTotalSupply; - emit MaxTotalSupplyUpdated(_maxTotalSupply); - } - - /// @dev Lets a contract admin set the recipient for all primary sales. - function setPrimarySaleRecipient(address _saleRecipient) external onlyRole(DEFAULT_ADMIN_ROLE) { - primarySaleRecipient = _saleRecipient; - emit PrimarySaleRecipientUpdated(_saleRecipient); - } - - /// @dev Lets a contract admin update the platform fee recipient and bps - function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) - external - onlyRole(DEFAULT_ADMIN_ROLE) - { - require(_platformFeeBps <= MAX_BPS, "bps <= 10000."); - - platformFeeBps = uint64(_platformFeeBps); - platformFeeRecipient = _platformFeeRecipient; - - emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); - } - - /// @dev Lets a contract admin set the URI for contract-level metadata. - function setContractURI(string calldata _uri) external onlyRole(DEFAULT_ADMIN_ROLE) { - contractURI = _uri; - } - - /*/////////////////////////////////////////////////////////////// - Miscellaneous - //////////////////////////////////////////////////////////////*/ - - function _mint(address account, uint256 amount) internal virtual override(ERC20Upgradeable, ERC20VotesUpgradeable) { - super._mint(account, amount); - } - - function _burn(address account, uint256 amount) internal virtual override(ERC20Upgradeable, ERC20VotesUpgradeable) { - super._burn(account, amount); - } - - function _msgSender() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (address sender) - { - return ERC2771ContextUpgradeable._msgSender(); - } - - function _msgData() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (bytes calldata) - { - return ERC2771ContextUpgradeable._msgData(); - } -} diff --git a/contracts/drop/DropERC721.sol b/contracts/drop/DropERC721.sol deleted file mode 100644 index 62c7ada69..000000000 --- a/contracts/drop/DropERC721.sol +++ /dev/null @@ -1,742 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -// ========== External imports ========== - -import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; - -import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; - -import "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/structs/BitMapsUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; - -// ========== Internal imports ========== - -import { IDropERC721 } from "../interfaces/drop/IDropERC721.sol"; -import { ITWFee } from "../interfaces/ITWFee.sol"; -import "../interfaces/IThirdwebContract.sol"; - -// ========== Features ========== - -import "../extension/interface/IPlatformFee.sol"; -import "../extension/interface/IPrimarySale.sol"; -import "../extension/interface/IRoyalty.sol"; -import "../extension/interface/IOwnable.sol"; - -import "../openzeppelin-presets/metatx/ERC2771ContextUpgradeable.sol"; - -import "../lib/CurrencyTransferLib.sol"; -import "../lib/FeeType.sol"; -import "../lib/MerkleProof.sol"; - -contract DropERC721 is - Initializable, - IThirdwebContract, - IOwnable, - IRoyalty, - IPrimarySale, - IPlatformFee, - ReentrancyGuardUpgradeable, - ERC2771ContextUpgradeable, - MulticallUpgradeable, - AccessControlEnumerableUpgradeable, - ERC721EnumerableUpgradeable, - IDropERC721 -{ - using BitMapsUpgradeable for BitMapsUpgradeable.BitMap; - using StringsUpgradeable for uint256; - - /*/////////////////////////////////////////////////////////////// - State variables - //////////////////////////////////////////////////////////////*/ - - bytes32 private constant MODULE_TYPE = bytes32("DropERC721"); - uint256 private constant VERSION = 2; - - /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. - bytes32 private constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE"); - /// @dev Only MINTER_ROLE holders can lazy mint NFTs. - bytes32 private constant MINTER_ROLE = keccak256("MINTER_ROLE"); - - /// @dev Max bps in the thirdweb system. - uint256 private constant MAX_BPS = 10_000; - - /// @dev Owner of the contract (purpose: OpenSea compatibility) - address private _owner; - - /// @dev The next token ID of the NFT to "lazy mint". - uint256 public nextTokenIdToMint; - - /// @dev The next token ID of the NFT that can be claimed. - uint256 public nextTokenIdToClaim; - - /// @dev The address that receives all primary sales value. - address public primarySaleRecipient; - - /// @dev The max number of NFTs a wallet can claim. - uint256 public maxWalletClaimCount; - - /// @dev Global max total supply of NFTs. - uint256 public maxTotalSupply; - - /// @dev The address that receives all platform fees from all sales. - address private platformFeeRecipient; - - /// @dev The % of primary sales collected as platform fees. - uint16 private platformFeeBps; - - /// @dev The (default) address that receives all royalty value. - address private royaltyRecipient; - - /// @dev The (default) % of a sale to take as royalty (in basis points). - uint16 private royaltyBps; - - /// @dev Contract level metadata. - string public contractURI; - - /// @dev Largest tokenId of each batch of tokens with the same baseURI - uint256[] public baseURIIndices; - - /// @dev The set of all claim conditions, at any given moment. - ClaimConditionList public claimCondition; - - /*/////////////////////////////////////////////////////////////// - Mappings - //////////////////////////////////////////////////////////////*/ - - /** - * @dev Mapping from 'Largest tokenId of a batch of tokens with the same baseURI' - * to base URI for the respective batch of tokens. - **/ - mapping(uint256 => string) private baseURI; - - /** - * @dev Mapping from 'Largest tokenId of a batch of 'delayed-reveal' tokens with - * the same baseURI' to encrypted base URI for the respective batch of tokens. - **/ - mapping(uint256 => bytes) public encryptedBaseURI; - - /// @dev Mapping from address => total number of NFTs a wallet has claimed. - mapping(address => uint256) public walletClaimCount; - - /// @dev Token ID => royalty recipient and bps for token - mapping(uint256 => RoyaltyInfo) private royaltyInfoForToken; - - /*/////////////////////////////////////////////////////////////// - Constructor + initializer logic - //////////////////////////////////////////////////////////////*/ - - constructor() initializer {} - - /// @dev Initiliazes the contract, like a constructor. - function initialize( - address _defaultAdmin, - string memory _name, - string memory _symbol, - string memory _contractURI, - address[] memory _trustedForwarders, - address _saleRecipient, - address _royaltyRecipient, - uint128 _royaltyBps, - uint128 _platformFeeBps, - address _platformFeeRecipient - ) external initializer { - // Initialize inherited contracts, most base-like -> most derived. - __ReentrancyGuard_init(); - __ERC2771Context_init(_trustedForwarders); - __ERC721_init(_name, _symbol); - - // Initialize this contract's state. - royaltyRecipient = _royaltyRecipient; - royaltyBps = uint16(_royaltyBps); - platformFeeRecipient = _platformFeeRecipient; - platformFeeBps = uint16(_platformFeeBps); - primarySaleRecipient = _saleRecipient; - contractURI = _contractURI; - _owner = _defaultAdmin; - - _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); - _setupRole(MINTER_ROLE, _defaultAdmin); - _setupRole(TRANSFER_ROLE, _defaultAdmin); - _setupRole(TRANSFER_ROLE, address(0)); - } - - /*/////////////////////////////////////////////////////////////// - Generic contract logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns the type of the contract. - function contractType() external pure returns (bytes32) { - return MODULE_TYPE; - } - - /// @dev Returns the version of the contract. - function contractVersion() external pure returns (uint8) { - return uint8(VERSION); - } - - /** - * @dev Returns the address of the current owner. - */ - function owner() public view returns (address) { - return hasRole(DEFAULT_ADMIN_ROLE, _owner) ? _owner : address(0); - } - - /*/////////////////////////////////////////////////////////////// - ERC 165 / 721 / 2981 logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns the URI for a given tokenId. - function tokenURI(uint256 _tokenId) public view override returns (string memory) { - for (uint256 i = 0; i < baseURIIndices.length; i += 1) { - if (_tokenId < baseURIIndices[i]) { - if (encryptedBaseURI[baseURIIndices[i]].length != 0) { - return string(abi.encodePacked(baseURI[baseURIIndices[i]], "0")); - } else { - return string(abi.encodePacked(baseURI[baseURIIndices[i]], _tokenId.toString())); - } - } - } - - return ""; - } - - /// @dev See ERC 165 - function supportsInterface(bytes4 interfaceId) - public - view - virtual - override(ERC721EnumerableUpgradeable, AccessControlEnumerableUpgradeable, IERC165Upgradeable, IERC165) - returns (bool) - { - return super.supportsInterface(interfaceId) || type(IERC2981Upgradeable).interfaceId == interfaceId; - } - - /// @dev Returns the royalty recipient and amount, given a tokenId and sale price. - function royaltyInfo(uint256 tokenId, uint256 salePrice) - external - view - virtual - returns (address receiver, uint256 royaltyAmount) - { - (address recipient, uint256 bps) = getRoyaltyInfoForToken(tokenId); - receiver = recipient; - royaltyAmount = (salePrice * bps) / MAX_BPS; - } - - /*/////////////////////////////////////////////////////////////// - Minting + delayed-reveal logic - //////////////////////////////////////////////////////////////*/ - - /** - * @dev Lets an account with `MINTER_ROLE` lazy mint 'n' NFTs. - * The URIs for each token is the provided `_baseURIForTokens` + `{tokenId}`. - */ - function lazyMint( - uint256 _amount, - string calldata _baseURIForTokens, - bytes calldata _encryptedBaseURI - ) external onlyRole(MINTER_ROLE) { - uint256 startId = nextTokenIdToMint; - uint256 baseURIIndex = startId + _amount; - - nextTokenIdToMint = baseURIIndex; - baseURI[baseURIIndex] = _baseURIForTokens; - baseURIIndices.push(baseURIIndex); - - if (_encryptedBaseURI.length != 0) { - encryptedBaseURI[baseURIIndex] = _encryptedBaseURI; - } - - emit TokensLazyMinted(startId, startId + _amount - 1, _baseURIForTokens, _encryptedBaseURI); - } - - /// @dev Lets an account with `MINTER_ROLE` reveal the URI for a batch of 'delayed-reveal' NFTs. - function reveal(uint256 index, bytes calldata _key) - external - onlyRole(MINTER_ROLE) - returns (string memory revealedURI) - { - require(index < baseURIIndices.length, "invalid index."); - - uint256 _index = baseURIIndices[index]; - bytes memory encryptedURI = encryptedBaseURI[_index]; - require(encryptedURI.length != 0, "nothing to reveal."); - - revealedURI = string(encryptDecrypt(encryptedURI, _key)); - - baseURI[_index] = revealedURI; - delete encryptedBaseURI[_index]; - - emit NFTRevealed(_index, revealedURI); - - return revealedURI; - } - - /// @dev See: https://ethereum.stackexchange.com/questions/69825/decrypt-message-on-chain - function encryptDecrypt(bytes memory data, bytes calldata key) public pure returns (bytes memory result) { - // Store data length on stack for later use - uint256 length = data.length; - - // solhint-disable-next-line no-inline-assembly - assembly { - // Set result to free memory pointer - result := mload(0x40) - // Increase free memory pointer by lenght + 32 - mstore(0x40, add(add(result, length), 32)) - // Set result length - mstore(result, length) - } - - // Iterate over the data stepping by 32 bytes - for (uint256 i = 0; i < length; i += 32) { - // Generate hash of the key and offset - bytes32 hash = keccak256(abi.encodePacked(key, i)); - - bytes32 chunk; - // solhint-disable-next-line no-inline-assembly - assembly { - // Read 32-bytes data chunk - chunk := mload(add(data, add(i, 32))) - } - // XOR the chunk with hash - chunk ^= hash; - // solhint-disable-next-line no-inline-assembly - assembly { - // Write 32-byte encrypted chunk - mstore(add(result, add(i, 32)), chunk) - } - } - } - - /*/////////////////////////////////////////////////////////////// - Claim logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Lets an account claim NFTs. - function claim( - address _receiver, - uint256 _quantity, - address _currency, - uint256 _pricePerToken, - bytes32[] calldata _proofs, - uint256 _proofMaxQuantityPerTransaction - ) external payable nonReentrant { - require(isTrustedForwarder(msg.sender) || _msgSender() == tx.origin, "BOT"); - - uint256 tokenIdToClaim = nextTokenIdToClaim; - - // Get the claim conditions. - uint256 activeConditionId = getActiveClaimConditionId(); - - /** - * We make allowlist checks (i.e. verifyClaimMerkleProof) before verifying the claim's general - * validity (i.e. verifyClaim) because we give precedence to the check of allow list quantity - * restriction over the check of the general claim condition's quantityLimitPerTransaction - * restriction. - */ - - // Verify inclusion in allowlist. - (bool validMerkleProof, uint256 merkleProofIndex) = verifyClaimMerkleProof( - activeConditionId, - _msgSender(), - _quantity, - _proofs, - _proofMaxQuantityPerTransaction - ); - - // Verify claim validity. If not valid, revert. - // when there's allowlist present --> verifyClaimMerkleProof will verify the _proofMaxQuantityPerTransaction value with hashed leaf in the allowlist - // when there's no allowlist, this check is true --> verifyClaim will check for _quantity being less/equal than the limit - bool toVerifyMaxQuantityPerTransaction = _proofMaxQuantityPerTransaction == 0 || - claimCondition.phases[activeConditionId].merkleRoot == bytes32(0); - verifyClaim( - activeConditionId, - _msgSender(), - _quantity, - _currency, - _pricePerToken, - toVerifyMaxQuantityPerTransaction - ); - - if (validMerkleProof && _proofMaxQuantityPerTransaction > 0) { - /** - * Mark the claimer's use of their position in the allowlist. A spot in an allowlist - * can be used only once. - */ - claimCondition.limitMerkleProofClaim[activeConditionId].set(merkleProofIndex); - } - - // If there's a price, collect price. - collectClaimPrice(_quantity, _currency, _pricePerToken); - - // Mint the relevant NFTs to claimer. - transferClaimedTokens(_receiver, activeConditionId, _quantity); - - emit TokensClaimed(activeConditionId, _msgSender(), _receiver, tokenIdToClaim, _quantity); - } - - /// @dev Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. - function setClaimConditions(ClaimCondition[] calldata _phases, bool _resetClaimEligibility) - external - onlyRole(DEFAULT_ADMIN_ROLE) - { - uint256 existingStartIndex = claimCondition.currentStartId; - uint256 existingPhaseCount = claimCondition.count; - - /** - * `limitLastClaimTimestamp` and `limitMerkleProofClaim` are mappings that use a - * claim condition's UID as a key. - * - * If `_resetClaimEligibility == true`, we assign completely new UIDs to the claim - * conditions in `_phases`, effectively resetting the restrictions on claims expressed - * by `limitLastClaimTimestamp` and `limitMerkleProofClaim`. - */ - uint256 newStartIndex = existingStartIndex; - if (_resetClaimEligibility) { - newStartIndex = existingStartIndex + existingPhaseCount; - } - - claimCondition.count = _phases.length; - claimCondition.currentStartId = newStartIndex; - - uint256 lastConditionStartTimestamp; - for (uint256 i = 0; i < _phases.length; i++) { - require(i == 0 || lastConditionStartTimestamp < _phases[i].startTimestamp, "ST"); - - uint256 supplyClaimedAlready = claimCondition.phases[newStartIndex + i].supplyClaimed; - require(supplyClaimedAlready <= _phases[i].maxClaimableSupply, "max supply claimed already"); - - claimCondition.phases[newStartIndex + i] = _phases[i]; - claimCondition.phases[newStartIndex + i].supplyClaimed = supplyClaimedAlready; - - lastConditionStartTimestamp = _phases[i].startTimestamp; - } - - /** - * Gas refunds (as much as possible) - * - * If `_resetClaimEligibility == true`, we assign completely new UIDs to the claim - * conditions in `_phases`. So, we delete claim conditions with UID < `newStartIndex`. - * - * If `_resetClaimEligibility == false`, and there are more existing claim conditions - * than in `_phases`, we delete the existing claim conditions that don't get replaced - * by the conditions in `_phases`. - */ - if (_resetClaimEligibility) { - for (uint256 i = existingStartIndex; i < newStartIndex; i++) { - delete claimCondition.phases[i]; - delete claimCondition.limitMerkleProofClaim[i]; - } - } else { - if (existingPhaseCount > _phases.length) { - for (uint256 i = _phases.length; i < existingPhaseCount; i++) { - delete claimCondition.phases[newStartIndex + i]; - delete claimCondition.limitMerkleProofClaim[newStartIndex + i]; - } - } - } - - emit ClaimConditionsUpdated(_phases); - } - - /// @dev Collects and distributes the primary sale value of NFTs being claimed. - function collectClaimPrice( - uint256 _quantityToClaim, - address _currency, - uint256 _pricePerToken - ) internal { - if (_pricePerToken == 0) { - return; - } - - uint256 totalPrice = _quantityToClaim * _pricePerToken; - uint256 platformFees = (totalPrice * platformFeeBps) / MAX_BPS; - - if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { - require(msg.value == totalPrice, "must send total price."); - } - - CurrencyTransferLib.transferCurrency(_currency, _msgSender(), platformFeeRecipient, platformFees); - CurrencyTransferLib.transferCurrency(_currency, _msgSender(), primarySaleRecipient, totalPrice - platformFees); - } - - /// @dev Transfers the NFTs being claimed. - function transferClaimedTokens( - address _to, - uint256 _conditionId, - uint256 _quantityBeingClaimed - ) internal { - // Update the supply minted under mint condition. - claimCondition.phases[_conditionId].supplyClaimed += _quantityBeingClaimed; - - // if transfer claimed tokens is called when `to != msg.sender`, it'd use msg.sender's limits. - // behavior would be similar to `msg.sender` mint for itself, then transfer to `_to`. - claimCondition.limitLastClaimTimestamp[_conditionId][_msgSender()] = block.timestamp; - walletClaimCount[_msgSender()] += _quantityBeingClaimed; - - uint256 tokenIdToClaim = nextTokenIdToClaim; - - for (uint256 i = 0; i < _quantityBeingClaimed; i += 1) { - _mint(_to, tokenIdToClaim); - tokenIdToClaim += 1; - } - - nextTokenIdToClaim = tokenIdToClaim; - } - - /// @dev Checks a request to claim NFTs against the active claim condition's criteria. - function verifyClaim( - uint256 _conditionId, - address _claimer, - uint256 _quantity, - address _currency, - uint256 _pricePerToken, - bool verifyMaxQuantityPerTransaction - ) public view { - ClaimCondition memory currentClaimPhase = claimCondition.phases[_conditionId]; - - require( - _currency == currentClaimPhase.currency && _pricePerToken == currentClaimPhase.pricePerToken, - "invalid currency or price." - ); - - // If we're checking for an allowlist quantity restriction, ignore the general quantity restriction. - require( - _quantity > 0 && - (!verifyMaxQuantityPerTransaction || _quantity <= currentClaimPhase.quantityLimitPerTransaction), - "invalid quantity." - ); - require( - currentClaimPhase.supplyClaimed + _quantity <= currentClaimPhase.maxClaimableSupply, - "exceed max claimable supply." - ); - require(nextTokenIdToClaim + _quantity <= nextTokenIdToMint, "not enough minted tokens."); - require(maxTotalSupply == 0 || nextTokenIdToClaim + _quantity <= maxTotalSupply, "exceed max total supply."); - require( - maxWalletClaimCount == 0 || walletClaimCount[_claimer] + _quantity <= maxWalletClaimCount, - "exceed claim limit" - ); - - (uint256 lastClaimTimestamp, uint256 nextValidClaimTimestamp) = getClaimTimestamp(_conditionId, _claimer); - require(lastClaimTimestamp == 0 || block.timestamp >= nextValidClaimTimestamp, "cannot claim."); - } - - /// @dev Checks whether a claimer meets the claim condition's allowlist criteria. - function verifyClaimMerkleProof( - uint256 _conditionId, - address _claimer, - uint256 _quantity, - bytes32[] calldata _proofs, - uint256 _proofMaxQuantityPerTransaction - ) public view returns (bool validMerkleProof, uint256 merkleProofIndex) { - ClaimCondition memory currentClaimPhase = claimCondition.phases[_conditionId]; - - if (currentClaimPhase.merkleRoot != bytes32(0)) { - (validMerkleProof, merkleProofIndex) = MerkleProof.verify( - _proofs, - currentClaimPhase.merkleRoot, - keccak256(abi.encodePacked(_claimer, _proofMaxQuantityPerTransaction)) - ); - require(validMerkleProof, "not in whitelist."); - require(!claimCondition.limitMerkleProofClaim[_conditionId].get(merkleProofIndex), "proof claimed."); - require( - _proofMaxQuantityPerTransaction == 0 || _quantity <= _proofMaxQuantityPerTransaction, - "invalid quantity proof." - ); - } - } - - /*/////////////////////////////////////////////////////////////// - Getter functions - //////////////////////////////////////////////////////////////*/ - - /// @dev At any given moment, returns the uid for the active claim condition. - function getActiveClaimConditionId() public view returns (uint256) { - for (uint256 i = claimCondition.currentStartId + claimCondition.count; i > claimCondition.currentStartId; i--) { - if (block.timestamp >= claimCondition.phases[i - 1].startTimestamp) { - return i - 1; - } - } - - revert("!CONDITION."); - } - - /// @dev Returns the royalty recipient and bps for a particular token Id. - function getRoyaltyInfoForToken(uint256 _tokenId) public view returns (address, uint16) { - RoyaltyInfo memory royaltyForToken = royaltyInfoForToken[_tokenId]; - - return - royaltyForToken.recipient == address(0) - ? (royaltyRecipient, uint16(royaltyBps)) - : (royaltyForToken.recipient, uint16(royaltyForToken.bps)); - } - - /// @dev Returns the platform fee recipient and bps. - function getPlatformFeeInfo() external view returns (address, uint16) { - return (platformFeeRecipient, uint16(platformFeeBps)); - } - - /// @dev Returns the default royalty recipient and bps. - function getDefaultRoyaltyInfo() external view returns (address, uint16) { - return (royaltyRecipient, uint16(royaltyBps)); - } - - /// @dev Returns the timestamp for when a claimer is eligible for claiming NFTs again. - function getClaimTimestamp(uint256 _conditionId, address _claimer) - public - view - returns (uint256 lastClaimTimestamp, uint256 nextValidClaimTimestamp) - { - lastClaimTimestamp = claimCondition.limitLastClaimTimestamp[_conditionId][_claimer]; - - unchecked { - nextValidClaimTimestamp = - lastClaimTimestamp + - claimCondition.phases[_conditionId].waitTimeInSecondsBetweenClaims; - - if (nextValidClaimTimestamp < lastClaimTimestamp) { - nextValidClaimTimestamp = type(uint256).max; - } - } - } - - /// @dev Returns the claim condition at the given uid. - function getClaimConditionById(uint256 _conditionId) external view returns (ClaimCondition memory condition) { - condition = claimCondition.phases[_conditionId]; - } - - /// @dev Returns the amount of stored baseURIs - function getBaseURICount() external view returns (uint256) { - return baseURIIndices.length; - } - - /*/////////////////////////////////////////////////////////////// - Setter functions - //////////////////////////////////////////////////////////////*/ - - /// @dev Lets a contract admin set a claim count for a wallet. - function setWalletClaimCount(address _claimer, uint256 _count) external onlyRole(DEFAULT_ADMIN_ROLE) { - walletClaimCount[_claimer] = _count; - emit WalletClaimCountUpdated(_claimer, _count); - } - - /// @dev Lets a contract admin set a maximum number of NFTs that can be claimed by any wallet. - function setMaxWalletClaimCount(uint256 _count) external onlyRole(DEFAULT_ADMIN_ROLE) { - maxWalletClaimCount = _count; - emit MaxWalletClaimCountUpdated(_count); - } - - /// @dev Lets a contract admin set the global maximum supply for collection's NFTs. - function setMaxTotalSupply(uint256 _maxTotalSupply) external onlyRole(DEFAULT_ADMIN_ROLE) { - maxTotalSupply = _maxTotalSupply; - emit MaxTotalSupplyUpdated(_maxTotalSupply); - } - - /// @dev Lets a contract admin set the recipient for all primary sales. - function setPrimarySaleRecipient(address _saleRecipient) external onlyRole(DEFAULT_ADMIN_ROLE) { - primarySaleRecipient = _saleRecipient; - emit PrimarySaleRecipientUpdated(_saleRecipient); - } - - /// @dev Lets a contract admin update the default royalty recipient and bps. - function setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) - external - onlyRole(DEFAULT_ADMIN_ROLE) - { - require(_royaltyBps <= MAX_BPS, "> MAX_BPS"); - - royaltyRecipient = _royaltyRecipient; - royaltyBps = uint16(_royaltyBps); - - emit DefaultRoyalty(_royaltyRecipient, _royaltyBps); - } - - /// @dev Lets a contract admin set the royalty recipient and bps for a particular token Id. - function setRoyaltyInfoForToken( - uint256 _tokenId, - address _recipient, - uint256 _bps - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(_bps <= MAX_BPS, "> MAX_BPS"); - - royaltyInfoForToken[_tokenId] = RoyaltyInfo({ recipient: _recipient, bps: _bps }); - - emit RoyaltyForToken(_tokenId, _recipient, _bps); - } - - /// @dev Lets a contract admin update the platform fee recipient and bps - function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) - external - onlyRole(DEFAULT_ADMIN_ROLE) - { - require(_platformFeeBps <= MAX_BPS, "> MAX_BPS."); - - platformFeeBps = uint16(_platformFeeBps); - platformFeeRecipient = _platformFeeRecipient; - - emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); - } - - /// @dev Lets a contract admin set a new owner for the contract. The new owner must be a contract admin. - function setOwner(address _newOwner) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(hasRole(DEFAULT_ADMIN_ROLE, _newOwner), "!ADMIN"); - address _prevOwner = _owner; - _owner = _newOwner; - - emit OwnerUpdated(_prevOwner, _newOwner); - } - - /// @dev Lets a contract admin set the URI for contract-level metadata. - function setContractURI(string calldata _uri) external onlyRole(DEFAULT_ADMIN_ROLE) { - contractURI = _uri; - } - - /*/////////////////////////////////////////////////////////////// - Miscellaneous - //////////////////////////////////////////////////////////////*/ - - /// @dev Burns `tokenId`. See {ERC721-_burn}. - function burn(uint256 tokenId) public virtual { - //solhint-disable-next-line max-line-length - require(_isApprovedOrOwner(_msgSender(), tokenId), "caller not owner nor approved"); - _burn(tokenId); - } - - /// @dev See {ERC721-_beforeTokenTransfer}. - function _beforeTokenTransfer( - address from, - address to, - uint256 tokenId - ) internal virtual override(ERC721EnumerableUpgradeable) { - super._beforeTokenTransfer(from, to, tokenId); - - // if transfer is restricted on the contract, we still want to allow burning and minting - if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { - require(hasRole(TRANSFER_ROLE, from) || hasRole(TRANSFER_ROLE, to), "!TRANSFER_ROLE"); - } - } - - function _msgSender() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (address sender) - { - return ERC2771ContextUpgradeable._msgSender(); - } - - function _msgData() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (bytes calldata) - { - return ERC2771ContextUpgradeable._msgData(); - } -} diff --git a/contracts/drop/drop.md b/contracts/drop/drop.md deleted file mode 100644 index d450ff941..000000000 --- a/contracts/drop/drop.md +++ /dev/null @@ -1,158 +0,0 @@ -# Drop design document. - -This is a live document that explains what the [thirdweb](https://thirdweb.com/) `Drop` smart contracts are, how they work and can be used, and why they are written the way they are. - -The document is written for technical and non-technical readers. To ask further questions about any of thirdweb’s `Drop`, please join the [thirdweb discord](https://discord.gg/thirdweb) or create a github issue. - ---- - -## Background - -The thirdweb `Drop` contracts are distribution mechanisms for tokens. This distribution mechanism is offered for ERC20, ERC721 and ERC1155 tokens, as `DropERC20`, `DropERC721` and `DropERC1155`. - -The `Drop` contracts are meant to be used when the goal of the contract creator is for an audience to come in and claim tokens within certain restrictions e.g. — ‘only addresses in an allowlist can mint tokens’, or ‘minters must pay *x* amount of price in *y* currency to mint’, etc. - -The `Drop` contracts let the contract creator establish phases (periods of time), where each phase can specify multiple such restrictions on the minting of tokens during that period of time. We refer to such a phase as a ‘claim condition’. - -### Why we’re building `Drop` - -We’ve observed that there are largely three distinct contexts under which one mints tokens — - -1. Minting tokens for yourself on a contract you own. E.g. a person wants to mint their Twitter profile picture as an NFT. -2. Having an audience mint tokens on a contract you own. - 1. The nature of tokens to be minted by the audience is pre-determined by the contract admin. E.g. a 10k NFT drop where the contents of the NFTs to be minted by the audience is already known and determined by the contract admin before the audience comes in to mint NFTs. - 2. The nature of tokens to be minted by the audience is *not* pre-determined by the contract admin. E.g. a course ‘certificate’ dynamically generated with the name of the course participant, to be minted by the course participant at the time of course completion. - -The thirdweb `Token` contracts serve the cases described in (1) and 2(ii). - -The thirdweb `Drop` contracts serve the case described in 2(i). They are written to give a contract creator granular control over restrictions around an audience minting tokens from the same contract (or ‘collection’, in the case of NFTs) over an extended period of time. - -## Technical Details - -The distribution mechanism of `Drop` is as follows — A contract admin establishes a series of ‘claim conditions’. A ‘claim condition’ is a period of time in which accounts can mint tokens on the respective `Drop` contract, within a set of restrictions defined by the ‘claim condition’. - -### Claim Conditions - -The following makes up a claim condition — - -```solidity -struct ClaimCondition { - uint256 startTimestamp; - uint256 maxClaimableSupply; - uint256 supplyClaimed; - uint256 quantityLimitPerTransaction; - uint256 waitTimeInSecondsBetweenClaims; - bytes32 merkleRoot; - uint256 pricePerToken; - address currency; -} -``` - -| Parameters | Type | Description | -| --- | --- | --- | -| startTimestamp | uint256 | The unix timestamp after which the claim condition applies. The same claim condition applies until the startTimestamp of the next claim condition. | -| maxClaimableSupply | uint256 | The maximum total number of tokens that can be claimed under the claim condition. | -| supplyClaimed | uint256 | At any given point, the number of tokens that have been claimed under the claim condition. | -| quantityLimitPerTransaction | uint256 | The maximum number of tokens that can be claimed in a single transaction. | -| waitTimeInSecondsBetweenClaims | uint256 | The least number of seconds an account must wait after claiming tokens, to be able to claim tokens again. | -| merkleRoot | bytes32 | The allowlist of addresses that can claim tokens under the claim condition. - -(Optional) The allowlist may specify the exact amount of tokens that an address in the allowlist is eligible to claim. - -The parameters that make up a claim condition can be composed in different ways to create specific restrictions around a mint. For example, a single claim condition where: - -- `quantityLimitPerTransaction = 5` -- `waitTimeInSecondsBetweenClaims = type(uint256).max` -- `merkleRoot = bytes32(0)` - -creates restrictions around a mint, where (1) any wallet can participate in the mint, (2) a wallet can mint at most 5 tokens and (3) a wallet can claim tokens only once. - -A `Drop` contract lets a contract admin establish a series of claim conditions, at once. Since each claim condition specifies a `startTime`, a contract admin can establish a series of claim conditions, ordered by their start time, to specify different set of restrictions around minting, during different periods of time. - -At any moment, there is only one active claim condition, and an account attempting to mint tokens on the respective `Drop` contract successfully or unsuccessfully, based on whether the account passes the restrictions defined by that moment’s active claim condition. - -A `Drop` contract natively keeps track of claim conditions set by a contract admin in a ‘claim conditions list’, which looks as follows — - -```solidity -struct ClaimConditionList { - uint256 currentStartId; - uint256 count; - mapping(uint256 => ClaimCondition) phases; - mapping(uint256 => mapping(address => uint256)) limitLastClaimTimestamp; - mapping(uint256 => BitMapsUpgradeable.BitMap) limitMerkleProofClaim; -} -``` - -| Parameter | Description | -| --- | --- | -| currentStartId | The uid for the first claim condition amongst the current set of claim conditions. The uid for each next claim condition is one more than the previous claim condition's uid. | -| count | The total number of phases / claim conditions in the list of claim conditions. | -| phases | The claim conditions at a given uid. Claim conditions are ordered in an ascending order by their startTimestamp. | -| limitLastClaimTimestamp | Map from an account and uid for a claim condition, to the last timestamp at which the account claimed tokens under that claim condition. | -| limitMerkleProofClaim | Map from a claim condition uid to whether an address in an allowlist has already claimed tokens i.e. used their place in the allowlist. | - -### Setting claim conditions - -In all `Drop` contracts, a contract admin specifies the following when setting claim conditions: - -| Parameter | Type | Description | -| --- | --- | --- | -| phases | ClaimCondition[] | Claim conditions in ascending order by startTimestamp. | -| resetClaimEligibility | bool | Whether to reset limitLastClaimTimestamp and limitMerkleProofClaim values when setting new claim conditions. | - -When setting claim conditions, any existing set of claim conditions stored in `ClaimConditionsList` are overwritten with the new claim conditions specified in `phases`. - -The claim conditions specified in `phases` are expected to be in ordered in ascending order, by their ‘start time’. As a result, only one claim condition is active during at any given time. - -Each of the claim conditions specified in `phases` is assigned a unique integer ID. The UID of the first condition in `phases` is stored as the `ClaimConditionList.currentStartId` and each next claim condition’s UID is one more than the previous condition’s UID. - -![claim-conditions-diagram-1.png](/assets/claim-conditions-diagram-1.png) - -The `resetClaimEligibility` boolean flag determines what UIDs are assigned to the claim conditions specified in `phases`. Since `ClaimConditionList.limitLastClaimTimestamp` and `ClaimConditionList.limitMerkleProofClaim` are both indexed by the UID of claim conditions, this gives a contract admin more granular control over the restrictions that claim conditions can express. We now illustrate this with an example: - -Let’s say an existing claim condition **C1** specifies the following restrictions: - -- `quantityLimitPerTransaction = 1` -- `waitTimeInSecondsBetweenClaims = type(uint256).max` -- `merkleRoot = bytes32(0)` -- `pricePerToken = 0.1 ether` -- `currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` (i.e. native token of the chain e.g ether for Ethereum mainnet) - -At a high level, **C1** expresses the following restrictions on minting — any address can claim at most one token, ever, by paying 0.1 ether in price. - -Let’s say the contract admin wants to increase the price per token from 0.1 ether to 0.2 ether, while ensuring that wallets that have already claimed tokens are not able to claim tokens again. Essentially, the contract admin now wants to instantiate a claim condition **C2** with the following restrictions: - -- `quantityLimitPerTransaction = 1` -- `waitTimeInSecondsBetweenClaims = type(uint256).max` -- `merkleRoot = bytes32(0)` -- `pricePerToken = 0.2 ether` -- `currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` (i.e. native token of the chain e.g ether for Ethereum mainnet) - -To go from **C1** to **C2** while ensuring that wallets that have already claimed tokens are not able to claim tokens again, the contract admin will set claim conditions while specifying `resetClaimEligibility == false`. As a result, the **C2** will be assigned the same UID as **C1**. Since `ClaimConditionList.limitLastClaimTimestamp` is indexed by the UID of claim conditions, the information of the timestamp at which a wallet claimed tokens during **C1** will not be lost. And so, wallets that claimed tokens during **C1** will now be ineligible to claim tokens during **C2** since the following check will always fail: - -```solidity -// pseudo-code -nextValidClaimTimestamp = - limitLastClaimTimestamp[UID_of_C2][claimer_address] + C2.waitTimeInSecondsBetweenClaims - -require(block.timestamp >= nextValidClaimTimestamp); -``` - -### EIPs supported / implemented - -The distribution mechanism for tokens expressed by thirdweb’s `Drop` is implemented for ERC20, ERC721 and ERC1155 tokens, as `DropERC20`, `DropERC721` and `DropERC1155`. - -There are a few key differences between the three implementations — - -- `DropERC20` is written for the distribution of completely fungible, ERC20 tokens. On the other hand, `DropERC721` and `DropERC1155` are written for the distribution of NFTs, which requires ‘lazy minting’ i.e. defining the content of the NFTs before an audience comes in to mint them during a claim condition. -- Both `DropERC20` and `DropERC721` maintain a global, contract-wide `ClaimConditionsList` which stores the claim conditions under which tokens can be minted. The `DropERC1155` contract, on the other hand, maintains a `ClaimConditionList` for every integer token ID that an NFT can assume. And so, a contract admin can set up claim conditions per NFT i.e. per token ID, in the `DropERC1155` contract. - -## Limitations - -The distribution mechanism of thirdweb’s `Drop` contracts is vulnerable to [sybil attacks](https://en.wikipedia.org/wiki/Sybil_attack). That is, despite the various ways in which restrictions can be applied to the minting of tokens, some restrictions that claim conditions can express target wallets and not persons. - -For example, the restriction `waitTimeInSecondsBetweenClaims` expresses the least amount of time a *wallet* must wait, before claiming tokens again during the respective claim condition. A sophisticated actor may generate multiple wallets to claim tokens in a way that undermine such restrictions, when viewing such restrictions as restrictions on unique persons, and not wallets. - -## Authors -- [nkrishang](https://github.com/nkrishang) -- [thirdweb team](https://github.com/thirdweb-dev) \ No newline at end of file diff --git a/contracts/eip/ERC1155.sol b/contracts/eip/ERC1155.sol new file mode 100644 index 000000000..5a0d5647e --- /dev/null +++ b/contracts/eip/ERC1155.sol @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.0; + +import "./interface/IERC1155.sol"; +import "./interface/IERC1155Metadata.sol"; +import "./interface/IERC1155Receiver.sol"; + +contract ERC1155 is IERC1155, IERC1155Metadata { + /*////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + string public name; + string public symbol; + + /*////////////////////////////////////////////////////////////// + Mappings + //////////////////////////////////////////////////////////////*/ + + mapping(address => mapping(uint256 => uint256)) public balanceOf; + + mapping(address => mapping(address => bool)) public isApprovedForAll; + + mapping(uint256 => string) internal _uri; + + /*////////////////////////////////////////////////////////////// + Constructor + //////////////////////////////////////////////////////////////*/ + + constructor(string memory _name, string memory _symbol) { + name = _name; + symbol = _symbol; + } + + /*////////////////////////////////////////////////////////////// + View functions + //////////////////////////////////////////////////////////////*/ + + function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { + return + interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 + interfaceId == 0xd9b67a26 || // ERC165 Interface ID for ERC1155 + interfaceId == 0x0e89341c; // ERC165 Interface ID for ERC1155MetadataURI + } + + function uri(uint256 tokenId) public view virtual override returns (string memory) { + return _uri[tokenId]; + } + + function balanceOfBatch( + address[] memory accounts, + uint256[] memory ids + ) public view virtual override returns (uint256[] memory) { + require(accounts.length == ids.length, "LENGTH_MISMATCH"); + + uint256[] memory batchBalances = new uint256[](accounts.length); + + for (uint256 i = 0; i < accounts.length; ++i) { + batchBalances[i] = balanceOf[accounts[i]][ids[i]]; + } + + return batchBalances; + } + + /*////////////////////////////////////////////////////////////// + ERC1155 logic + //////////////////////////////////////////////////////////////*/ + + function setApprovalForAll(address operator, bool approved) public virtual override { + address owner = msg.sender; + require(owner != operator, "APPROVING_SELF"); + isApprovedForAll[owner][operator] = approved; + emit ApprovalForAll(owner, operator, approved); + } + + function safeTransferFrom( + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) public virtual override { + require(from == msg.sender || isApprovedForAll[from][msg.sender], "!OWNER_OR_APPROVED"); + _safeTransferFrom(from, to, id, amount, data); + } + + function safeBatchTransferFrom( + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) public virtual override { + require(from == msg.sender || isApprovedForAll[from][msg.sender], "!OWNER_OR_APPROVED"); + _safeBatchTransferFrom(from, to, ids, amounts, data); + } + + /*////////////////////////////////////////////////////////////// + Internal logic + //////////////////////////////////////////////////////////////*/ + + function _safeTransferFrom( + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) internal virtual { + require(to != address(0), "TO_ZERO_ADDR"); + + address operator = msg.sender; + + _beforeTokenTransfer(operator, from, to, _asSingletonArray(id), _asSingletonArray(amount), data); + + uint256 fromBalance = balanceOf[from][id]; + require(fromBalance >= amount, "INSUFFICIENT_BAL"); + unchecked { + balanceOf[from][id] = fromBalance - amount; + } + balanceOf[to][id] += amount; + + emit TransferSingle(operator, from, to, id, amount); + + _doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data); + } + + function _safeBatchTransferFrom( + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal virtual { + require(ids.length == amounts.length, "LENGTH_MISMATCH"); + require(to != address(0), "TO_ZERO_ADDR"); + + address operator = msg.sender; + + _beforeTokenTransfer(operator, from, to, ids, amounts, data); + + for (uint256 i = 0; i < ids.length; ++i) { + uint256 id = ids[i]; + uint256 amount = amounts[i]; + + uint256 fromBalance = balanceOf[from][id]; + require(fromBalance >= amount, "INSUFFICIENT_BAL"); + unchecked { + balanceOf[from][id] = fromBalance - amount; + } + balanceOf[to][id] += amount; + } + + emit TransferBatch(operator, from, to, ids, amounts); + + _doSafeBatchTransferAcceptanceCheck(operator, from, to, ids, amounts, data); + } + + function _setTokenURI(uint256 tokenId, string memory newuri) internal virtual { + _uri[tokenId] = newuri; + } + + function _mint(address to, uint256 id, uint256 amount, bytes memory data) internal virtual { + require(to != address(0), "TO_ZERO_ADDR"); + + address operator = msg.sender; + + _beforeTokenTransfer(operator, address(0), to, _asSingletonArray(id), _asSingletonArray(amount), data); + + balanceOf[to][id] += amount; + emit TransferSingle(operator, address(0), to, id, amount); + + _doSafeTransferAcceptanceCheck(operator, address(0), to, id, amount, data); + } + + function _mintBatch( + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal virtual { + require(to != address(0), "TO_ZERO_ADDR"); + require(ids.length == amounts.length, "LENGTH_MISMATCH"); + + address operator = msg.sender; + + _beforeTokenTransfer(operator, address(0), to, ids, amounts, data); + + for (uint256 i = 0; i < ids.length; i++) { + balanceOf[to][ids[i]] += amounts[i]; + } + + emit TransferBatch(operator, address(0), to, ids, amounts); + + _doSafeBatchTransferAcceptanceCheck(operator, address(0), to, ids, amounts, data); + } + + function _burn(address from, uint256 id, uint256 amount) internal virtual { + require(from != address(0), "FROM_ZERO_ADDR"); + + address operator = msg.sender; + + _beforeTokenTransfer(operator, from, address(0), _asSingletonArray(id), _asSingletonArray(amount), ""); + + uint256 fromBalance = balanceOf[from][id]; + require(fromBalance >= amount, "INSUFFICIENT_BAL"); + unchecked { + balanceOf[from][id] = fromBalance - amount; + } + + emit TransferSingle(operator, from, address(0), id, amount); + } + + function _burnBatch(address from, uint256[] memory ids, uint256[] memory amounts) internal virtual { + require(from != address(0), "FROM_ZERO_ADDR"); + require(ids.length == amounts.length, "LENGTH_MISMATCH"); + + address operator = msg.sender; + + _beforeTokenTransfer(operator, from, address(0), ids, amounts, ""); + + for (uint256 i = 0; i < ids.length; i++) { + uint256 id = ids[i]; + uint256 amount = amounts[i]; + + uint256 fromBalance = balanceOf[from][id]; + require(fromBalance >= amount, "INSUFFICIENT_BAL"); + unchecked { + balanceOf[from][id] = fromBalance - amount; + } + } + + emit TransferBatch(operator, from, address(0), ids, amounts); + } + + function _beforeTokenTransfer( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal virtual {} + + function _doSafeTransferAcceptanceCheck( + address operator, + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) private { + if (to.code.length > 0) { + try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns (bytes4 response) { + if (response != IERC1155Receiver.onERC1155Received.selector) { + revert("TOKENS_REJECTED"); + } + } catch Error(string memory reason) { + revert(reason); + } catch { + revert("!ERC1155RECEIVER"); + } + } + } + + function _doSafeBatchTransferAcceptanceCheck( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) private { + if (to.code.length > 0) { + try IERC1155Receiver(to).onERC1155BatchReceived(operator, from, ids, amounts, data) returns ( + bytes4 response + ) { + if (response != IERC1155Receiver.onERC1155BatchReceived.selector) { + revert("TOKENS_REJECTED"); + } + } catch Error(string memory reason) { + revert(reason); + } catch { + revert("!ERC1155RECEIVER"); + } + } + } + + function _asSingletonArray(uint256 element) private pure returns (uint256[] memory) { + uint256[] memory array = new uint256[](1); + array[0] = element; + + return array; + } +} diff --git a/contracts/eip/ERC1271.sol b/contracts/eip/ERC1271.sol new file mode 100644 index 000000000..a3170cd45 --- /dev/null +++ b/contracts/eip/ERC1271.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.0; + +abstract contract ERC1271 { + // bytes4(keccak256("isValidSignature(bytes32,bytes)") + bytes4 internal constant MAGICVALUE = 0x1626ba7e; + + /** + * @dev Should return whether the signature provided is valid for the provided hash + * @param _hash Hash of the data to be signed + * @param _signature Signature byte array associated with _hash + * + * MUST return the bytes4 magic value 0x1626ba7e when function passes. + * MUST NOT modify state (using STATICCALL for solc < 0.5, view modifier for solc > 0.5) + * MUST allow external calls + */ + function isValidSignature(bytes32 _hash, bytes memory _signature) public view virtual returns (bytes4 magicValue); +} diff --git a/contracts/eip/ERC721A.sol b/contracts/eip/ERC721A.sol index 2d8951316..9ae9c99f4 100644 --- a/contracts/eip/ERC721A.sol +++ b/contracts/eip/ERC721A.sol @@ -5,25 +5,25 @@ pragma solidity ^0.8.4; import "./interface/IERC721A.sol"; -import "../openzeppelin-presets/token/ERC721/IERC721Receiver.sol"; -import "../lib/TWAddress.sol"; -import "../openzeppelin-presets/utils/Context.sol"; -import "../lib/TWStrings.sol"; +import "../external-deps/openzeppelin/token/ERC721/IERC721Receiver.sol"; +import "../lib/Address.sol"; +import "../external-deps/openzeppelin/utils/Context.sol"; +import "../lib/Strings.sol"; import "./ERC165.sol"; /** - * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including + * @dev Implementation of [ERC721](https://eips.ethereum.org/EIPS/eip-721) Non-Fungible Token Standard, including * the Metadata extension. Built to optimize for lower gas during batch mints. * * Assumes serials are sequentially minted starting at _startTokenId() (defaults to 0, e.g. 0, 1, 2, 3..). * - * Assumes that an owner cannot have more than 2**64 - 1 (max value of uint64) of supply. + * Assumes that an owner cannot have more than 2^64 - 1 (max value of uint64) of supply. * - * Assumes that the maximum token id cannot exceed 2**256 - 1 (max value of uint256). + * Assumes that the maximum token id cannot exceed 2^256 - 1 (max value of uint256). */ contract ERC721A is Context, ERC165, IERC721A { - using TWAddress for address; - using TWStrings for uint256; + using Address for address; + using Strings for uint256; // The tokenId of the next token to be minted. uint256 internal _currentIndex; @@ -248,34 +248,21 @@ contract ERC721A is Context, ERC165, IERC721A { /** * @dev See {IERC721-transferFrom}. */ - function transferFrom( - address from, - address to, - uint256 tokenId - ) public virtual override { + function transferFrom(address from, address to, uint256 tokenId) public virtual override { _transfer(from, to, tokenId); } /** * @dev See {IERC721-safeTransferFrom}. */ - function safeTransferFrom( - address from, - address to, - uint256 tokenId - ) public virtual override { + function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override { safeTransferFrom(from, to, tokenId, ""); } /** * @dev See {IERC721-safeTransferFrom}. */ - function safeTransferFrom( - address from, - address to, - uint256 tokenId, - bytes memory _data - ) public virtual override { + function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public virtual override { _transfer(from, to, tokenId); if (to.isContract()) if (!_checkContractOnERC721Received(from, to, tokenId, _data)) { @@ -312,11 +299,7 @@ contract ERC721A is Context, ERC165, IERC721A { * * Emits a {Transfer} event. */ - function _safeMint( - address to, - uint256 quantity, - bytes memory _data - ) internal { + function _safeMint(address to, uint256 quantity, bytes memory _data) internal { uint256 startTokenId = _currentIndex; if (to == address(0)) revert MintToZeroAddress(); if (quantity == 0) revert MintZeroQuantity(); @@ -404,11 +387,7 @@ contract ERC721A is Context, ERC165, IERC721A { * * Emits a {Transfer} event. */ - function _transfer( - address from, - address to, - uint256 tokenId - ) private { + function _transfer(address from, address to, uint256 tokenId) private { TokenOwnership memory prevOwnership = _ownershipOf(tokenId); if (prevOwnership.addr != from) revert TransferFromIncorrectOwner(); @@ -531,11 +510,7 @@ contract ERC721A is Context, ERC165, IERC721A { * * Emits a {Approval} event. */ - function _approve( - address to, - uint256 tokenId, - address owner - ) private { + function _approve(address to, uint256 tokenId, address owner) private { _tokenApprovals[tokenId] = to; emit Approval(owner, to, tokenId); } @@ -583,12 +558,7 @@ contract ERC721A is Context, ERC165, IERC721A { * - When `to` is zero, `tokenId` will be burned by `from`. * - `from` and `to` are never both zero. */ - function _beforeTokenTransfers( - address from, - address to, - uint256 startTokenId, - uint256 quantity - ) internal virtual {} + function _beforeTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity) internal virtual {} /** * @dev Hook that is called after a set of serially-ordered token ids have been transferred. This includes @@ -606,10 +576,5 @@ contract ERC721A is Context, ERC165, IERC721A { * - When `to` is zero, `tokenId` has been burned by `from`. * - `from` and `to` are never both zero. */ - function _afterTokenTransfers( - address from, - address to, - uint256 startTokenId, - uint256 quantity - ) internal virtual {} + function _afterTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity) internal virtual {} } diff --git a/contracts/eip/ERC721AUpgradeable.sol b/contracts/eip/ERC721AUpgradeable.sol new file mode 100644 index 000000000..50fdef270 --- /dev/null +++ b/contracts/eip/ERC721AUpgradeable.sol @@ -0,0 +1,625 @@ +// SPDX-License-Identifier: MIT +// ERC721A Contracts v3.3.0 +// Creator: Chiru Labs + +pragma solidity ^0.8.4; + +////////// CHANGELOG: turn `approve` to virtual ////////// + +import "./interface/IERC721A.sol"; +import "./interface/IERC721Receiver.sol"; +import "../lib/Address.sol"; +import "../external-deps/openzeppelin/utils/Context.sol"; +import "../lib/Strings.sol"; +import "./ERC165.sol"; +import "../extension/upgradeable/Initializable.sol"; + +library ERC721AStorage { + /// @custom:storage-location erc7201:erc721.a.storage + /// @dev keccak256(abi.encode(uint256(keccak256("erc721.a.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant ERC721A_STORAGE_POSITION = + 0xe2efff925b8936e8a3471e86ad87942375e24de600ddfb2b841647ce1379ed00; + + struct Data { + // The tokenId of the next token to be minted. + uint256 _currentIndex; + // The number of tokens burned. + uint256 _burnCounter; + // Token name + string _name; + // Token symbol + string _symbol; + // Mapping from token ID to ownership details + // An empty struct value does not necessarily mean the token is unowned. See _ownershipOf implementation for details. + mapping(uint256 => IERC721A.TokenOwnership) _ownerships; + // Mapping owner address to address data + mapping(address => IERC721A.AddressData) _addressData; + // Mapping from token ID to approved address + mapping(uint256 => address) _tokenApprovals; + // Mapping from owner to operator approvals + mapping(address => mapping(address => bool)) _operatorApprovals; + } + + function erc721AStorage() internal pure returns (Data storage erc721AData) { + bytes32 position = ERC721A_STORAGE_POSITION; + assembly { + erc721AData.slot := position + } + } +} + +/** + * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including + * the Metadata extension. Built to optimize for lower gas during batch mints. + * + * Assumes serials are sequentially minted starting at _startTokenId() (defaults to 0, e.g. 0, 1, 2, 3..). + * + * Assumes that an owner cannot have more than 2**64 - 1 (max value of uint64) of supply. + * + * Assumes that the maximum token id cannot exceed 2**256 - 1 (max value of uint256). + */ +contract ERC721AUpgradeable is Initializable, Context, ERC165, IERC721A { + using Address for address; + using Strings for uint256; + + function __ERC721A_init(string memory name_, string memory symbol_) internal onlyInitializing { + __ERC721A_init_unchained(name_, symbol_); + } + + function __ERC721A_init_unchained(string memory name_, string memory symbol_) internal onlyInitializing { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + + data._name = name_; + data._symbol = symbol_; + data._currentIndex = _startTokenId(); + } + + /** + * To change the starting tokenId, please override this function. + */ + function _startTokenId() internal view virtual returns (uint256) { + return 0; + } + + /** + * @dev Burned tokens are calculated here, use _totalMinted() if you want to count just minted tokens. + */ + function totalSupply() public view override returns (uint256) { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + + // Counter underflow is impossible as _burnCounter cannot be incremented + // more than _currentIndex - _startTokenId() times + unchecked { + return data._currentIndex - data._burnCounter - _startTokenId(); + } + } + + /** + * Returns the total amount of tokens minted in the contract. + */ + function _totalMinted() internal view returns (uint256) { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + // Counter underflow is impossible as _currentIndex does not decrement, + // and it is initialized to _startTokenId() + unchecked { + return data._currentIndex - _startTokenId(); + } + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165) returns (bool) { + return + interfaceId == type(IERC721).interfaceId || + interfaceId == type(IERC721Metadata).interfaceId || + super.supportsInterface(interfaceId); + } + + /** + * @dev See {IERC721-balanceOf}. + */ + function balanceOf(address owner) public view override returns (uint256) { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + if (owner == address(0)) revert BalanceQueryForZeroAddress(); + return uint256(data._addressData[owner].balance); + } + + /** + * Returns the number of tokens minted by `owner`. + */ + function _numberMinted(address owner) internal view returns (uint256) { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + return uint256(data._addressData[owner].numberMinted); + } + + /** + * Returns the number of tokens burned by or on behalf of `owner`. + */ + function _numberBurned(address owner) internal view returns (uint256) { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + return uint256(data._addressData[owner].numberBurned); + } + + /** + * Returns the auxillary data for `owner`. (e.g. number of whitelist mint slots used). + */ + function _getAux(address owner) internal view returns (uint64) { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + return data._addressData[owner].aux; + } + + /** + * Sets the auxillary data for `owner`. (e.g. number of whitelist mint slots used). + * If there are multiple variables, please pack them into a uint64. + */ + function _setAux(address owner, uint64 aux) internal { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + data._addressData[owner].aux = aux; + } + + /** + * Gas spent here starts off proportional to the maximum mint batch size. + * It gradually moves to O(1) as tokens get transferred around in the collection over time. + */ + function _ownershipOf(uint256 tokenId) internal view returns (TokenOwnership memory) { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + + uint256 curr = tokenId; + + unchecked { + if (_startTokenId() <= curr) + if (curr < data._currentIndex) { + TokenOwnership memory ownership = data._ownerships[curr]; + if (!ownership.burned) { + if (ownership.addr != address(0)) { + return ownership; + } + // Invariant: + // There will always be an ownership that has an address and is not burned + // before an ownership that does not have an address and is not burned. + // Hence, curr will not underflow. + while (true) { + curr--; + ownership = data._ownerships[curr]; + if (ownership.addr != address(0)) { + return ownership; + } + } + } + } + } + revert OwnerQueryForNonexistentToken(); + } + + /** + * @dev See {IERC721-ownerOf}. + */ + function ownerOf(uint256 tokenId) public view override returns (address) { + return _ownershipOf(tokenId).addr; + } + + /** + * @dev See {IERC721Metadata-name}. + */ + function name() public view virtual override returns (string memory) { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + return data._name; + } + + /** + * @dev See {IERC721Metadata-symbol}. + */ + function symbol() public view virtual override returns (string memory) { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + return data._symbol; + } + + /** + * @dev See {IERC721Metadata-tokenURI}. + */ + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); + + string memory baseURI = _baseURI(); + return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : ""; + } + + /** + * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each + * token will be the concatenation of the `baseURI` and the `tokenId`. Empty + * by default, can be overriden in child contracts. + */ + function _baseURI() internal view virtual returns (string memory) { + return ""; + } + + /** + * @dev See {IERC721-approve}. + */ + function approve(address to, uint256 tokenId) public virtual override { + address owner = ERC721AUpgradeable.ownerOf(tokenId); + if (to == owner) revert ApprovalToCurrentOwner(); + + if (_msgSender() != owner) + if (!isApprovedForAll(owner, _msgSender())) { + revert ApprovalCallerNotOwnerNorApproved(); + } + + _approve(to, tokenId, owner); + } + + /** + * @dev See {IERC721-getApproved}. + */ + function getApproved(uint256 tokenId) public view virtual override returns (address) { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + + if (!_exists(tokenId)) revert ApprovalQueryForNonexistentToken(); + + return data._tokenApprovals[tokenId]; + } + + /** + * @dev See {IERC721-setApprovalForAll}. + */ + function setApprovalForAll(address operator, bool approved) public virtual override { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + + if (operator == _msgSender()) revert ApproveToCaller(); + + data._operatorApprovals[_msgSender()][operator] = approved; + emit ApprovalForAll(_msgSender(), operator, approved); + } + + /** + * @dev See {IERC721-isApprovedForAll}. + */ + function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + return data._operatorApprovals[owner][operator]; + } + + /** + * @dev See {IERC721-transferFrom}. + */ + function transferFrom(address from, address to, uint256 tokenId) public virtual override { + _transfer(from, to, tokenId); + } + + /** + * @dev See {IERC721-safeTransferFrom}. + */ + function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override { + safeTransferFrom(from, to, tokenId, ""); + } + + /** + * @dev See {IERC721-safeTransferFrom}. + */ + function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public virtual override { + _transfer(from, to, tokenId); + if (to.isContract()) + if (!_checkContractOnERC721Received(from, to, tokenId, _data)) { + revert TransferToNonERC721ReceiverImplementer(); + } + } + + /** + * @dev Returns whether `tokenId` exists. + * + * Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}. + * + * Tokens start existing when they are minted (`_mint`), + */ + function _exists(uint256 tokenId) internal view returns (bool) { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + return _startTokenId() <= tokenId && tokenId < data._currentIndex && !data._ownerships[tokenId].burned; + } + + /** + * @dev Equivalent to `_safeMint(to, quantity, '')`. + */ + function _safeMint(address to, uint256 quantity) internal { + _safeMint(to, quantity, ""); + } + + /** + * @dev Safely mints `quantity` tokens and transfers them to `to`. + * + * Requirements: + * + * - If `to` refers to a smart contract, it must implement + * {IERC721Receiver-onERC721Received}, which is called for each safe transfer. + * - `quantity` must be greater than 0. + * + * Emits a {Transfer} event. + */ + function _safeMint(address to, uint256 quantity, bytes memory _data) internal { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + + uint256 startTokenId = data._currentIndex; + if (to == address(0)) revert MintToZeroAddress(); + if (quantity == 0) revert MintZeroQuantity(); + + _beforeTokenTransfers(address(0), to, startTokenId, quantity); + + // Overflows are incredibly unrealistic. + // balance or numberMinted overflow if current value of either + quantity > 1.8e19 (2**64) - 1 + // updatedIndex overflows if _currentIndex + quantity > 1.2e77 (2**256) - 1 + unchecked { + data._addressData[to].balance += uint64(quantity); + data._addressData[to].numberMinted += uint64(quantity); + + data._ownerships[startTokenId].addr = to; + data._ownerships[startTokenId].startTimestamp = uint64(block.timestamp); + + uint256 updatedIndex = startTokenId; + uint256 end = updatedIndex + quantity; + + if (to.isContract()) { + do { + emit Transfer(address(0), to, updatedIndex); + if (!_checkContractOnERC721Received(address(0), to, updatedIndex++, _data)) { + revert TransferToNonERC721ReceiverImplementer(); + } + } while (updatedIndex < end); + // Reentrancy protection + if (data._currentIndex != startTokenId) revert(); + } else { + do { + emit Transfer(address(0), to, updatedIndex++); + } while (updatedIndex < end); + } + data._currentIndex = updatedIndex; + } + _afterTokenTransfers(address(0), to, startTokenId, quantity); + } + + /** + * @dev Mints `quantity` tokens and transfers them to `to`. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `quantity` must be greater than 0. + * + * Emits a {Transfer} event. + */ + function _mint(address to, uint256 quantity) internal { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + + uint256 startTokenId = data._currentIndex; + if (to == address(0)) revert MintToZeroAddress(); + if (quantity == 0) revert MintZeroQuantity(); + + _beforeTokenTransfers(address(0), to, startTokenId, quantity); + + // Overflows are incredibly unrealistic. + // balance or numberMinted overflow if current value of either + quantity > 1.8e19 (2**64) - 1 + // updatedIndex overflows if _currentIndex + quantity > 1.2e77 (2**256) - 1 + unchecked { + data._addressData[to].balance += uint64(quantity); + data._addressData[to].numberMinted += uint64(quantity); + + data._ownerships[startTokenId].addr = to; + data._ownerships[startTokenId].startTimestamp = uint64(block.timestamp); + + uint256 updatedIndex = startTokenId; + uint256 end = updatedIndex + quantity; + + do { + emit Transfer(address(0), to, updatedIndex++); + } while (updatedIndex < end); + + data._currentIndex = updatedIndex; + } + _afterTokenTransfers(address(0), to, startTokenId, quantity); + } + + /** + * @dev Transfers `tokenId` from `from` to `to`. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * + * Emits a {Transfer} event. + */ + function _transfer(address from, address to, uint256 tokenId) private { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + + TokenOwnership memory prevOwnership = _ownershipOf(tokenId); + + if (prevOwnership.addr != from) revert TransferFromIncorrectOwner(); + + bool isApprovedOrOwner = (_msgSender() == from || + isApprovedForAll(from, _msgSender()) || + getApproved(tokenId) == _msgSender()); + + if (!isApprovedOrOwner) revert TransferCallerNotOwnerNorApproved(); + if (to == address(0)) revert TransferToZeroAddress(); + + _beforeTokenTransfers(from, to, tokenId, 1); + + // Clear approvals from the previous owner + _approve(address(0), tokenId, from); + + // Underflow of the sender's balance is impossible because we check for + // ownership above and the recipient's balance can't realistically overflow. + // Counter overflow is incredibly unrealistic as tokenId would have to be 2**256. + unchecked { + data._addressData[from].balance -= 1; + data._addressData[to].balance += 1; + + TokenOwnership storage currSlot = data._ownerships[tokenId]; + currSlot.addr = to; + currSlot.startTimestamp = uint64(block.timestamp); + + // If the ownership slot of tokenId+1 is not explicitly set, that means the transfer initiator owns it. + // Set the slot of tokenId+1 explicitly in storage to maintain correctness for ownerOf(tokenId+1) calls. + uint256 nextTokenId = tokenId + 1; + TokenOwnership storage nextSlot = data._ownerships[nextTokenId]; + if (nextSlot.addr == address(0)) { + // This will suffice for checking _exists(nextTokenId), + // as a burned slot cannot contain the zero address. + if (nextTokenId != data._currentIndex) { + nextSlot.addr = from; + nextSlot.startTimestamp = prevOwnership.startTimestamp; + } + } + } + + emit Transfer(from, to, tokenId); + _afterTokenTransfers(from, to, tokenId, 1); + } + + /** + * @dev Equivalent to `_burn(tokenId, false)`. + */ + function _burn(uint256 tokenId) internal virtual { + _burn(tokenId, false); + } + + /** + * @dev Destroys `tokenId`. + * The approval is cleared when the token is burned. + * + * Requirements: + * + * - `tokenId` must exist. + * + * Emits a {Transfer} event. + */ + function _burn(uint256 tokenId, bool approvalCheck) internal virtual { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + + TokenOwnership memory prevOwnership = _ownershipOf(tokenId); + + address from = prevOwnership.addr; + + if (approvalCheck) { + bool isApprovedOrOwner = (_msgSender() == from || + isApprovedForAll(from, _msgSender()) || + getApproved(tokenId) == _msgSender()); + + if (!isApprovedOrOwner) revert TransferCallerNotOwnerNorApproved(); + } + + _beforeTokenTransfers(from, address(0), tokenId, 1); + + // Clear approvals from the previous owner + _approve(address(0), tokenId, from); + + // Underflow of the sender's balance is impossible because we check for + // ownership above and the recipient's balance can't realistically overflow. + // Counter overflow is incredibly unrealistic as tokenId would have to be 2**256. + unchecked { + AddressData storage addressData = data._addressData[from]; + addressData.balance -= 1; + addressData.numberBurned += 1; + + // Keep track of who burned the token, and the timestamp of burning. + TokenOwnership storage currSlot = data._ownerships[tokenId]; + currSlot.addr = from; + currSlot.startTimestamp = uint64(block.timestamp); + currSlot.burned = true; + + // If the ownership slot of tokenId+1 is not explicitly set, that means the burn initiator owns it. + // Set the slot of tokenId+1 explicitly in storage to maintain correctness for ownerOf(tokenId+1) calls. + uint256 nextTokenId = tokenId + 1; + TokenOwnership storage nextSlot = data._ownerships[nextTokenId]; + if (nextSlot.addr == address(0)) { + // This will suffice for checking _exists(nextTokenId), + // as a burned slot cannot contain the zero address. + if (nextTokenId != data._currentIndex) { + nextSlot.addr = from; + nextSlot.startTimestamp = prevOwnership.startTimestamp; + } + } + } + + emit Transfer(from, address(0), tokenId); + _afterTokenTransfers(from, address(0), tokenId, 1); + + // Overflow not possible, as _burnCounter cannot be exceed _currentIndex times. + unchecked { + data._burnCounter++; + } + } + + /** + * @dev Approve `to` to operate on `tokenId` + * + * Emits a {Approval} event. + */ + function _approve(address to, uint256 tokenId, address owner) private { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + data._tokenApprovals[tokenId] = to; + emit Approval(owner, to, tokenId); + } + + /** + * @dev Internal function to invoke {IERC721Receiver-onERC721Received} on a target contract. + * + * @param from address representing the previous owner of the given token ID + * @param to target address that will receive the tokens + * @param tokenId uint256 ID of the token to be transferred + * @param _data bytes optional data to send along with the call + * @return bool whether the call correctly returned the expected magic value + */ + function _checkContractOnERC721Received( + address from, + address to, + uint256 tokenId, + bytes memory _data + ) private returns (bool) { + try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (bytes4 retval) { + return retval == IERC721Receiver(to).onERC721Received.selector; + } catch (bytes memory reason) { + if (reason.length == 0) { + revert TransferToNonERC721ReceiverImplementer(); + } else { + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } + + /** + * @dev Hook that is called before a set of serially-ordered token ids are about to be transferred. This includes minting. + * And also called before burning one token. + * + * startTokenId - the first token id to be transferred + * quantity - the amount to be transferred + * + * Calling conditions: + * + * - When `from` and `to` are both non-zero, `from`'s `tokenId` will be + * transferred to `to`. + * - When `from` is zero, `tokenId` will be minted for `to`. + * - When `to` is zero, `tokenId` will be burned by `from`. + * - `from` and `to` are never both zero. + */ + function _beforeTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity) internal virtual {} + + /** + * @dev Hook that is called after a set of serially-ordered token ids have been transferred. This includes + * minting. + * And also called after one token has been burned. + * + * startTokenId - the first token id to be transferred + * quantity - the amount to be transferred + * + * Calling conditions: + * + * - When `from` and `to` are both non-zero, `from`'s `tokenId` has been + * transferred to `to`. + * - When `from` is zero, `tokenId` has been minted for `to`. + * - When `to` is zero, `tokenId` has been burned by `from`. + * - `from` and `to` are never both zero. + */ + function _afterTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity) internal virtual {} +} diff --git a/contracts/eip/ERC721AVirtualApprove.sol b/contracts/eip/ERC721AVirtualApprove.sol new file mode 100644 index 000000000..45c5232ce --- /dev/null +++ b/contracts/eip/ERC721AVirtualApprove.sol @@ -0,0 +1,582 @@ +// SPDX-License-Identifier: MIT +// ERC721A Contracts v3.3.0 +// Creator: Chiru Labs + +pragma solidity ^0.8.4; + +////////// CHANGELOG: turn `approve` to virtual ////////// + +import "./interface/IERC721A.sol"; +import "./interface/IERC721Receiver.sol"; +import "../lib/Address.sol"; +import "../external-deps/openzeppelin/utils/Context.sol"; +import "../lib/Strings.sol"; +import "./ERC165.sol"; + +/** + * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including + * the Metadata extension. Built to optimize for lower gas during batch mints. + * + * Assumes serials are sequentially minted starting at _startTokenId() (defaults to 0, e.g. 0, 1, 2, 3..). + * + * Assumes that an owner cannot have more than 2**64 - 1 (max value of uint64) of supply. + * + * Assumes that the maximum token id cannot exceed 2**256 - 1 (max value of uint256). + */ +contract ERC721A is Context, ERC165, IERC721A { + using Address for address; + using Strings for uint256; + + // The tokenId of the next token to be minted. + uint256 internal _currentIndex; + + // The number of tokens burned. + uint256 internal _burnCounter; + + // Token name + string private _name; + + // Token symbol + string private _symbol; + + // Mapping from token ID to ownership details + // An empty struct value does not necessarily mean the token is unowned. See _ownershipOf implementation for details. + mapping(uint256 => TokenOwnership) internal _ownerships; + + // Mapping owner address to address data + mapping(address => AddressData) private _addressData; + + // Mapping from token ID to approved address + mapping(uint256 => address) private _tokenApprovals; + + // Mapping from owner to operator approvals + mapping(address => mapping(address => bool)) private _operatorApprovals; + + constructor(string memory name_, string memory symbol_) { + _name = name_; + _symbol = symbol_; + _currentIndex = _startTokenId(); + } + + /** + * To change the starting tokenId, please override this function. + */ + function _startTokenId() internal view virtual returns (uint256) { + return 0; + } + + /** + * @dev Burned tokens are calculated here, use _totalMinted() if you want to count just minted tokens. + */ + function totalSupply() public view override returns (uint256) { + // Counter underflow is impossible as _burnCounter cannot be incremented + // more than _currentIndex - _startTokenId() times + unchecked { + return _currentIndex - _burnCounter - _startTokenId(); + } + } + + /** + * Returns the total amount of tokens minted in the contract. + */ + function _totalMinted() internal view returns (uint256) { + // Counter underflow is impossible as _currentIndex does not decrement, + // and it is initialized to _startTokenId() + unchecked { + return _currentIndex - _startTokenId(); + } + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165) returns (bool) { + return + interfaceId == type(IERC721).interfaceId || + interfaceId == type(IERC721Metadata).interfaceId || + super.supportsInterface(interfaceId); + } + + /** + * @dev See {IERC721-balanceOf}. + */ + function balanceOf(address owner) public view override returns (uint256) { + if (owner == address(0)) revert BalanceQueryForZeroAddress(); + return uint256(_addressData[owner].balance); + } + + /** + * Returns the number of tokens minted by `owner`. + */ + function _numberMinted(address owner) internal view returns (uint256) { + return uint256(_addressData[owner].numberMinted); + } + + /** + * Returns the number of tokens burned by or on behalf of `owner`. + */ + function _numberBurned(address owner) internal view returns (uint256) { + return uint256(_addressData[owner].numberBurned); + } + + /** + * Returns the auxillary data for `owner`. (e.g. number of whitelist mint slots used). + */ + function _getAux(address owner) internal view returns (uint64) { + return _addressData[owner].aux; + } + + /** + * Sets the auxillary data for `owner`. (e.g. number of whitelist mint slots used). + * If there are multiple variables, please pack them into a uint64. + */ + function _setAux(address owner, uint64 aux) internal { + _addressData[owner].aux = aux; + } + + /** + * Gas spent here starts off proportional to the maximum mint batch size. + * It gradually moves to O(1) as tokens get transferred around in the collection over time. + */ + function _ownershipOf(uint256 tokenId) internal view returns (TokenOwnership memory) { + uint256 curr = tokenId; + + unchecked { + if (_startTokenId() <= curr) + if (curr < _currentIndex) { + TokenOwnership memory ownership = _ownerships[curr]; + if (!ownership.burned) { + if (ownership.addr != address(0)) { + return ownership; + } + // Invariant: + // There will always be an ownership that has an address and is not burned + // before an ownership that does not have an address and is not burned. + // Hence, curr will not underflow. + while (true) { + curr--; + ownership = _ownerships[curr]; + if (ownership.addr != address(0)) { + return ownership; + } + } + } + } + } + revert OwnerQueryForNonexistentToken(); + } + + /** + * @dev See {IERC721-ownerOf}. + */ + function ownerOf(uint256 tokenId) public view override returns (address) { + return _ownershipOf(tokenId).addr; + } + + /** + * @dev See {IERC721Metadata-name}. + */ + function name() public view virtual override returns (string memory) { + return _name; + } + + /** + * @dev See {IERC721Metadata-symbol}. + */ + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + /** + * @dev See {IERC721Metadata-tokenURI}. + */ + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); + + string memory baseURI = _baseURI(); + return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : ""; + } + + /** + * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each + * token will be the concatenation of the `baseURI` and the `tokenId`. Empty + * by default, can be overriden in child contracts. + */ + function _baseURI() internal view virtual returns (string memory) { + return ""; + } + + /** + * @dev See {IERC721-approve}. + */ + function approve(address to, uint256 tokenId) public virtual override { + address owner = ERC721A.ownerOf(tokenId); + if (to == owner) revert ApprovalToCurrentOwner(); + + if (_msgSender() != owner) + if (!isApprovedForAll(owner, _msgSender())) { + revert ApprovalCallerNotOwnerNorApproved(); + } + + _approve(to, tokenId, owner); + } + + /** + * @dev See {IERC721-getApproved}. + */ + function getApproved(uint256 tokenId) public view override returns (address) { + if (!_exists(tokenId)) revert ApprovalQueryForNonexistentToken(); + + return _tokenApprovals[tokenId]; + } + + /** + * @dev See {IERC721-setApprovalForAll}. + */ + function setApprovalForAll(address operator, bool approved) public virtual override { + if (operator == _msgSender()) revert ApproveToCaller(); + + _operatorApprovals[_msgSender()][operator] = approved; + emit ApprovalForAll(_msgSender(), operator, approved); + } + + /** + * @dev See {IERC721-isApprovedForAll}. + */ + function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) { + return _operatorApprovals[owner][operator]; + } + + /** + * @dev See {IERC721-transferFrom}. + */ + function transferFrom(address from, address to, uint256 tokenId) public virtual override { + _transfer(from, to, tokenId); + } + + /** + * @dev See {IERC721-safeTransferFrom}. + */ + function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override { + safeTransferFrom(from, to, tokenId, ""); + } + + /** + * @dev See {IERC721-safeTransferFrom}. + */ + function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public virtual override { + _transfer(from, to, tokenId); + if (to.isContract()) + if (!_checkContractOnERC721Received(from, to, tokenId, _data)) { + revert TransferToNonERC721ReceiverImplementer(); + } + } + + /** + * @dev Returns whether `tokenId` exists. + * + * Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}. + * + * Tokens start existing when they are minted (`_mint`), + */ + function _exists(uint256 tokenId) internal view returns (bool) { + return _startTokenId() <= tokenId && tokenId < _currentIndex && !_ownerships[tokenId].burned; + } + + /** + * @dev Equivalent to `_safeMint(to, quantity, '')`. + */ + function _safeMint(address to, uint256 quantity) internal { + _safeMint(to, quantity, ""); + } + + /** + * @dev Safely mints `quantity` tokens and transfers them to `to`. + * + * Requirements: + * + * - If `to` refers to a smart contract, it must implement + * {IERC721Receiver-onERC721Received}, which is called for each safe transfer. + * - `quantity` must be greater than 0. + * + * Emits a {Transfer} event. + */ + function _safeMint(address to, uint256 quantity, bytes memory _data) internal { + uint256 startTokenId = _currentIndex; + if (to == address(0)) revert MintToZeroAddress(); + if (quantity == 0) revert MintZeroQuantity(); + + _beforeTokenTransfers(address(0), to, startTokenId, quantity); + + // Overflows are incredibly unrealistic. + // balance or numberMinted overflow if current value of either + quantity > 1.8e19 (2**64) - 1 + // updatedIndex overflows if _currentIndex + quantity > 1.2e77 (2**256) - 1 + unchecked { + _addressData[to].balance += uint64(quantity); + _addressData[to].numberMinted += uint64(quantity); + + _ownerships[startTokenId].addr = to; + _ownerships[startTokenId].startTimestamp = uint64(block.timestamp); + + uint256 updatedIndex = startTokenId; + uint256 end = updatedIndex + quantity; + + if (to.isContract()) { + do { + emit Transfer(address(0), to, updatedIndex); + if (!_checkContractOnERC721Received(address(0), to, updatedIndex++, _data)) { + revert TransferToNonERC721ReceiverImplementer(); + } + } while (updatedIndex < end); + // Reentrancy protection + if (_currentIndex != startTokenId) revert(); + } else { + do { + emit Transfer(address(0), to, updatedIndex++); + } while (updatedIndex < end); + } + _currentIndex = updatedIndex; + } + _afterTokenTransfers(address(0), to, startTokenId, quantity); + } + + /** + * @dev Mints `quantity` tokens and transfers them to `to`. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `quantity` must be greater than 0. + * + * Emits a {Transfer} event. + */ + function _mint(address to, uint256 quantity) internal { + uint256 startTokenId = _currentIndex; + if (to == address(0)) revert MintToZeroAddress(); + if (quantity == 0) revert MintZeroQuantity(); + + _beforeTokenTransfers(address(0), to, startTokenId, quantity); + + // Overflows are incredibly unrealistic. + // balance or numberMinted overflow if current value of either + quantity > 1.8e19 (2**64) - 1 + // updatedIndex overflows if _currentIndex + quantity > 1.2e77 (2**256) - 1 + unchecked { + _addressData[to].balance += uint64(quantity); + _addressData[to].numberMinted += uint64(quantity); + + _ownerships[startTokenId].addr = to; + _ownerships[startTokenId].startTimestamp = uint64(block.timestamp); + + uint256 updatedIndex = startTokenId; + uint256 end = updatedIndex + quantity; + + do { + emit Transfer(address(0), to, updatedIndex++); + } while (updatedIndex < end); + + _currentIndex = updatedIndex; + } + _afterTokenTransfers(address(0), to, startTokenId, quantity); + } + + /** + * @dev Transfers `tokenId` from `from` to `to`. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * + * Emits a {Transfer} event. + */ + function _transfer(address from, address to, uint256 tokenId) private { + TokenOwnership memory prevOwnership = _ownershipOf(tokenId); + + if (prevOwnership.addr != from) revert TransferFromIncorrectOwner(); + + bool isApprovedOrOwner = (_msgSender() == from || + isApprovedForAll(from, _msgSender()) || + getApproved(tokenId) == _msgSender()); + + if (!isApprovedOrOwner) revert TransferCallerNotOwnerNorApproved(); + if (to == address(0)) revert TransferToZeroAddress(); + + _beforeTokenTransfers(from, to, tokenId, 1); + + // Clear approvals from the previous owner + _approve(address(0), tokenId, from); + + // Underflow of the sender's balance is impossible because we check for + // ownership above and the recipient's balance can't realistically overflow. + // Counter overflow is incredibly unrealistic as tokenId would have to be 2**256. + unchecked { + _addressData[from].balance -= 1; + _addressData[to].balance += 1; + + TokenOwnership storage currSlot = _ownerships[tokenId]; + currSlot.addr = to; + currSlot.startTimestamp = uint64(block.timestamp); + + // If the ownership slot of tokenId+1 is not explicitly set, that means the transfer initiator owns it. + // Set the slot of tokenId+1 explicitly in storage to maintain correctness for ownerOf(tokenId+1) calls. + uint256 nextTokenId = tokenId + 1; + TokenOwnership storage nextSlot = _ownerships[nextTokenId]; + if (nextSlot.addr == address(0)) { + // This will suffice for checking _exists(nextTokenId), + // as a burned slot cannot contain the zero address. + if (nextTokenId != _currentIndex) { + nextSlot.addr = from; + nextSlot.startTimestamp = prevOwnership.startTimestamp; + } + } + } + + emit Transfer(from, to, tokenId); + _afterTokenTransfers(from, to, tokenId, 1); + } + + /** + * @dev Equivalent to `_burn(tokenId, false)`. + */ + function _burn(uint256 tokenId) internal virtual { + _burn(tokenId, false); + } + + /** + * @dev Destroys `tokenId`. + * The approval is cleared when the token is burned. + * + * Requirements: + * + * - `tokenId` must exist. + * + * Emits a {Transfer} event. + */ + function _burn(uint256 tokenId, bool approvalCheck) internal virtual { + TokenOwnership memory prevOwnership = _ownershipOf(tokenId); + + address from = prevOwnership.addr; + + if (approvalCheck) { + bool isApprovedOrOwner = (_msgSender() == from || + isApprovedForAll(from, _msgSender()) || + getApproved(tokenId) == _msgSender()); + + if (!isApprovedOrOwner) revert TransferCallerNotOwnerNorApproved(); + } + + _beforeTokenTransfers(from, address(0), tokenId, 1); + + // Clear approvals from the previous owner + _approve(address(0), tokenId, from); + + // Underflow of the sender's balance is impossible because we check for + // ownership above and the recipient's balance can't realistically overflow. + // Counter overflow is incredibly unrealistic as tokenId would have to be 2**256. + unchecked { + AddressData storage addressData = _addressData[from]; + addressData.balance -= 1; + addressData.numberBurned += 1; + + // Keep track of who burned the token, and the timestamp of burning. + TokenOwnership storage currSlot = _ownerships[tokenId]; + currSlot.addr = from; + currSlot.startTimestamp = uint64(block.timestamp); + currSlot.burned = true; + + // If the ownership slot of tokenId+1 is not explicitly set, that means the burn initiator owns it. + // Set the slot of tokenId+1 explicitly in storage to maintain correctness for ownerOf(tokenId+1) calls. + uint256 nextTokenId = tokenId + 1; + TokenOwnership storage nextSlot = _ownerships[nextTokenId]; + if (nextSlot.addr == address(0)) { + // This will suffice for checking _exists(nextTokenId), + // as a burned slot cannot contain the zero address. + if (nextTokenId != _currentIndex) { + nextSlot.addr = from; + nextSlot.startTimestamp = prevOwnership.startTimestamp; + } + } + } + + emit Transfer(from, address(0), tokenId); + _afterTokenTransfers(from, address(0), tokenId, 1); + + // Overflow not possible, as _burnCounter cannot be exceed _currentIndex times. + unchecked { + _burnCounter++; + } + } + + /** + * @dev Approve `to` to operate on `tokenId` + * + * Emits a {Approval} event. + */ + function _approve(address to, uint256 tokenId, address owner) private { + _tokenApprovals[tokenId] = to; + emit Approval(owner, to, tokenId); + } + + /** + * @dev Internal function to invoke {IERC721Receiver-onERC721Received} on a target contract. + * + * @param from address representing the previous owner of the given token ID + * @param to target address that will receive the tokens + * @param tokenId uint256 ID of the token to be transferred + * @param _data bytes optional data to send along with the call + * @return bool whether the call correctly returned the expected magic value + */ + function _checkContractOnERC721Received( + address from, + address to, + uint256 tokenId, + bytes memory _data + ) private returns (bool) { + try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (bytes4 retval) { + return retval == IERC721Receiver(to).onERC721Received.selector; + } catch (bytes memory reason) { + if (reason.length == 0) { + revert TransferToNonERC721ReceiverImplementer(); + } else { + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } + + /** + * @dev Hook that is called before a set of serially-ordered token ids are about to be transferred. This includes minting. + * And also called before burning one token. + * + * startTokenId - the first token id to be transferred + * quantity - the amount to be transferred + * + * Calling conditions: + * + * - When `from` and `to` are both non-zero, `from`'s `tokenId` will be + * transferred to `to`. + * - When `from` is zero, `tokenId` will be minted for `to`. + * - When `to` is zero, `tokenId` will be burned by `from`. + * - `from` and `to` are never both zero. + */ + function _beforeTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity) internal virtual {} + + /** + * @dev Hook that is called after a set of serially-ordered token ids have been transferred. This includes + * minting. + * And also called after one token has been burned. + * + * startTokenId - the first token id to be transferred + * quantity - the amount to be transferred + * + * Calling conditions: + * + * - When `from` and `to` are both non-zero, `from`'s `tokenId` has been + * transferred to `to`. + * - When `from` is zero, `tokenId` has been minted for `to`. + * - When `to` is zero, `tokenId` has been burned by `from`. + * - `from` and `to` are never both zero. + */ + function _afterTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity) internal virtual {} +} diff --git a/contracts/eip/ERC721AVirtualApproveUpgradeable.sol b/contracts/eip/ERC721AVirtualApproveUpgradeable.sol new file mode 100644 index 000000000..55d233bbc --- /dev/null +++ b/contracts/eip/ERC721AVirtualApproveUpgradeable.sol @@ -0,0 +1,598 @@ +// SPDX-License-Identifier: MIT +// ERC721A Contracts v3.3.0 +// Creator: Chiru Labs + +////////// CHANGELOG: turn `approve` to virtual ////////// + +pragma solidity ^0.8.4; + +import "erc721a-upgradeable/contracts/IERC721AUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721ReceiverUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +/** + * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including + * the Metadata extension. Built to optimize for lower gas during batch mints. + * + * Assumes serials are sequentially minted starting at _startTokenId() (defaults to 0, e.g. 0, 1, 2, 3..). + * + * Assumes that an owner cannot have more than 2**64 - 1 (max value of uint64) of supply. + * + * Assumes that the maximum token id cannot exceed 2**256 - 1 (max value of uint256). + */ +contract ERC721AUpgradeable is Initializable, ContextUpgradeable, ERC165Upgradeable, IERC721AUpgradeable { + using AddressUpgradeable for address; + using StringsUpgradeable for uint256; + + // The tokenId of the next token to be minted. + uint256 internal _currentIndex; + + // The number of tokens burned. + uint256 internal _burnCounter; + + // Token name + string private _name; + + // Token symbol + string private _symbol; + + // Mapping from token ID to ownership details + // An empty struct value does not necessarily mean the token is unowned. See _ownershipOf implementation for details. + mapping(uint256 => TokenOwnership) internal _ownerships; + + // Mapping owner address to address data + mapping(address => AddressData) private _addressData; + + // Mapping from token ID to approved address + mapping(uint256 => address) private _tokenApprovals; + + // Mapping from owner to operator approvals + mapping(address => mapping(address => bool)) private _operatorApprovals; + + function __ERC721A_init(string memory name_, string memory symbol_) internal onlyInitializing { + __ERC721A_init_unchained(name_, symbol_); + } + + function __ERC721A_init_unchained(string memory name_, string memory symbol_) internal onlyInitializing { + _name = name_; + _symbol = symbol_; + _currentIndex = _startTokenId(); + } + + /** + * To change the starting tokenId, please override this function. + */ + function _startTokenId() internal view virtual returns (uint256) { + return 0; + } + + /** + * @dev Burned tokens are calculated here, use _totalMinted() if you want to count just minted tokens. + */ + function totalSupply() public view override returns (uint256) { + // Counter underflow is impossible as _burnCounter cannot be incremented + // more than _currentIndex - _startTokenId() times + unchecked { + return _currentIndex - _burnCounter - _startTokenId(); + } + } + + /** + * Returns the total amount of tokens minted in the contract. + */ + function _totalMinted() internal view returns (uint256) { + // Counter underflow is impossible as _currentIndex does not decrement, + // and it is initialized to _startTokenId() + unchecked { + return _currentIndex - _startTokenId(); + } + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC165Upgradeable, IERC165Upgradeable) returns (bool) { + return + interfaceId == type(IERC721Upgradeable).interfaceId || + interfaceId == type(IERC721MetadataUpgradeable).interfaceId || + super.supportsInterface(interfaceId); + } + + /** + * @dev See {IERC721-balanceOf}. + */ + function balanceOf(address owner) public view override returns (uint256) { + if (owner == address(0)) revert BalanceQueryForZeroAddress(); + return uint256(_addressData[owner].balance); + } + + /** + * Returns the number of tokens minted by `owner`. + */ + function _numberMinted(address owner) internal view returns (uint256) { + return uint256(_addressData[owner].numberMinted); + } + + /** + * Returns the number of tokens burned by or on behalf of `owner`. + */ + function _numberBurned(address owner) internal view returns (uint256) { + return uint256(_addressData[owner].numberBurned); + } + + /** + * Returns the auxillary data for `owner`. (e.g. number of whitelist mint slots used). + */ + function _getAux(address owner) internal view returns (uint64) { + return _addressData[owner].aux; + } + + /** + * Sets the auxillary data for `owner`. (e.g. number of whitelist mint slots used). + * If there are multiple variables, please pack them into a uint64. + */ + function _setAux(address owner, uint64 aux) internal { + _addressData[owner].aux = aux; + } + + /** + * Gas spent here starts off proportional to the maximum mint batch size. + * It gradually moves to O(1) as tokens get transferred around in the collection over time. + */ + function _ownershipOf(uint256 tokenId) internal view returns (TokenOwnership memory) { + uint256 curr = tokenId; + + unchecked { + if (_startTokenId() <= curr) + if (curr < _currentIndex) { + TokenOwnership memory ownership = _ownerships[curr]; + if (!ownership.burned) { + if (ownership.addr != address(0)) { + return ownership; + } + // Invariant: + // There will always be an ownership that has an address and is not burned + // before an ownership that does not have an address and is not burned. + // Hence, curr will not underflow. + while (true) { + curr--; + ownership = _ownerships[curr]; + if (ownership.addr != address(0)) { + return ownership; + } + } + } + } + } + revert OwnerQueryForNonexistentToken(); + } + + /** + * @dev See {IERC721-ownerOf}. + */ + function ownerOf(uint256 tokenId) public view override returns (address) { + return _ownershipOf(tokenId).addr; + } + + /** + * @dev See {IERC721Metadata-name}. + */ + function name() public view virtual override returns (string memory) { + return _name; + } + + /** + * @dev See {IERC721Metadata-symbol}. + */ + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + /** + * @dev See {IERC721Metadata-tokenURI}. + */ + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); + + string memory baseURI = _baseURI(); + return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : ""; + } + + /** + * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each + * token will be the concatenation of the `baseURI` and the `tokenId`. Empty + * by default, can be overriden in child contracts. + */ + function _baseURI() internal view virtual returns (string memory) { + return ""; + } + + /** + * @dev See {IERC721-approve}. + */ + function approve(address to, uint256 tokenId) public virtual override { + address owner = ERC721AUpgradeable.ownerOf(tokenId); + if (to == owner) revert ApprovalToCurrentOwner(); + + if (_msgSender() != owner) + if (!isApprovedForAll(owner, _msgSender())) { + revert ApprovalCallerNotOwnerNorApproved(); + } + + _approve(to, tokenId, owner); + } + + /** + * @dev See {IERC721-getApproved}. + */ + function getApproved(uint256 tokenId) public view override returns (address) { + if (!_exists(tokenId)) revert ApprovalQueryForNonexistentToken(); + + return _tokenApprovals[tokenId]; + } + + /** + * @dev See {IERC721-setApprovalForAll}. + */ + function setApprovalForAll(address operator, bool approved) public virtual override { + if (operator == _msgSender()) revert ApproveToCaller(); + + _operatorApprovals[_msgSender()][operator] = approved; + emit ApprovalForAll(_msgSender(), operator, approved); + } + + /** + * @dev See {IERC721-isApprovedForAll}. + */ + function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) { + return _operatorApprovals[owner][operator]; + } + + /** + * @dev See {IERC721-transferFrom}. + */ + function transferFrom(address from, address to, uint256 tokenId) public virtual override { + _transfer(from, to, tokenId); + } + + /** + * @dev See {IERC721-safeTransferFrom}. + */ + function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override { + safeTransferFrom(from, to, tokenId, ""); + } + + /** + * @dev See {IERC721-safeTransferFrom}. + */ + function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public virtual override { + _transfer(from, to, tokenId); + if (to.isContract()) + if (!_checkContractOnERC721Received(from, to, tokenId, _data)) { + revert TransferToNonERC721ReceiverImplementer(); + } + } + + /** + * @dev Returns whether `tokenId` exists. + * + * Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}. + * + * Tokens start existing when they are minted (`_mint`), + */ + function _exists(uint256 tokenId) internal view returns (bool) { + return _startTokenId() <= tokenId && tokenId < _currentIndex && !_ownerships[tokenId].burned; + } + + /** + * @dev Equivalent to `_safeMint(to, quantity, '')`. + */ + function _safeMint(address to, uint256 quantity) internal { + _safeMint(to, quantity, ""); + } + + /** + * @dev Safely mints `quantity` tokens and transfers them to `to`. + * + * Requirements: + * + * - If `to` refers to a smart contract, it must implement + * {IERC721Receiver-onERC721Received}, which is called for each safe transfer. + * - `quantity` must be greater than 0. + * + * Emits a {Transfer} event. + */ + function _safeMint(address to, uint256 quantity, bytes memory _data) internal { + uint256 startTokenId = _currentIndex; + if (to == address(0)) revert MintToZeroAddress(); + if (quantity == 0) revert MintZeroQuantity(); + + _beforeTokenTransfers(address(0), to, startTokenId, quantity); + + // Overflows are incredibly unrealistic. + // balance or numberMinted overflow if current value of either + quantity > 1.8e19 (2**64) - 1 + // updatedIndex overflows if _currentIndex + quantity > 1.2e77 (2**256) - 1 + unchecked { + _addressData[to].balance += uint64(quantity); + _addressData[to].numberMinted += uint64(quantity); + + _ownerships[startTokenId].addr = to; + _ownerships[startTokenId].startTimestamp = uint64(block.timestamp); + + uint256 updatedIndex = startTokenId; + uint256 end = updatedIndex + quantity; + + if (to.isContract()) { + do { + emit Transfer(address(0), to, updatedIndex); + if (!_checkContractOnERC721Received(address(0), to, updatedIndex++, _data)) { + revert TransferToNonERC721ReceiverImplementer(); + } + } while (updatedIndex < end); + // Reentrancy protection + if (_currentIndex != startTokenId) revert(); + } else { + do { + emit Transfer(address(0), to, updatedIndex++); + } while (updatedIndex < end); + } + _currentIndex = updatedIndex; + } + _afterTokenTransfers(address(0), to, startTokenId, quantity); + } + + /** + * @dev Mints `quantity` tokens and transfers them to `to`. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `quantity` must be greater than 0. + * + * Emits a {Transfer} event. + */ + function _mint(address to, uint256 quantity) internal { + uint256 startTokenId = _currentIndex; + if (to == address(0)) revert MintToZeroAddress(); + if (quantity == 0) revert MintZeroQuantity(); + + _beforeTokenTransfers(address(0), to, startTokenId, quantity); + + // Overflows are incredibly unrealistic. + // balance or numberMinted overflow if current value of either + quantity > 1.8e19 (2**64) - 1 + // updatedIndex overflows if _currentIndex + quantity > 1.2e77 (2**256) - 1 + unchecked { + _addressData[to].balance += uint64(quantity); + _addressData[to].numberMinted += uint64(quantity); + + _ownerships[startTokenId].addr = to; + _ownerships[startTokenId].startTimestamp = uint64(block.timestamp); + + uint256 updatedIndex = startTokenId; + uint256 end = updatedIndex + quantity; + + do { + emit Transfer(address(0), to, updatedIndex++); + } while (updatedIndex < end); + + _currentIndex = updatedIndex; + } + _afterTokenTransfers(address(0), to, startTokenId, quantity); + } + + /** + * @dev Transfers `tokenId` from `from` to `to`. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * + * Emits a {Transfer} event. + */ + function _transfer(address from, address to, uint256 tokenId) private { + TokenOwnership memory prevOwnership = _ownershipOf(tokenId); + + if (prevOwnership.addr != from) revert TransferFromIncorrectOwner(); + + bool isApprovedOrOwner = (_msgSender() == from || + isApprovedForAll(from, _msgSender()) || + getApproved(tokenId) == _msgSender()); + + if (!isApprovedOrOwner) revert TransferCallerNotOwnerNorApproved(); + if (to == address(0)) revert TransferToZeroAddress(); + + _beforeTokenTransfers(from, to, tokenId, 1); + + // Clear approvals from the previous owner + _approve(address(0), tokenId, from); + + // Underflow of the sender's balance is impossible because we check for + // ownership above and the recipient's balance can't realistically overflow. + // Counter overflow is incredibly unrealistic as tokenId would have to be 2**256. + unchecked { + _addressData[from].balance -= 1; + _addressData[to].balance += 1; + + TokenOwnership storage currSlot = _ownerships[tokenId]; + currSlot.addr = to; + currSlot.startTimestamp = uint64(block.timestamp); + + // If the ownership slot of tokenId+1 is not explicitly set, that means the transfer initiator owns it. + // Set the slot of tokenId+1 explicitly in storage to maintain correctness for ownerOf(tokenId+1) calls. + uint256 nextTokenId = tokenId + 1; + TokenOwnership storage nextSlot = _ownerships[nextTokenId]; + if (nextSlot.addr == address(0)) { + // This will suffice for checking _exists(nextTokenId), + // as a burned slot cannot contain the zero address. + if (nextTokenId != _currentIndex) { + nextSlot.addr = from; + nextSlot.startTimestamp = prevOwnership.startTimestamp; + } + } + } + + emit Transfer(from, to, tokenId); + _afterTokenTransfers(from, to, tokenId, 1); + } + + /** + * @dev Equivalent to `_burn(tokenId, false)`. + */ + function _burn(uint256 tokenId) internal virtual { + _burn(tokenId, false); + } + + /** + * @dev Destroys `tokenId`. + * The approval is cleared when the token is burned. + * + * Requirements: + * + * - `tokenId` must exist. + * + * Emits a {Transfer} event. + */ + function _burn(uint256 tokenId, bool approvalCheck) internal virtual { + TokenOwnership memory prevOwnership = _ownershipOf(tokenId); + + address from = prevOwnership.addr; + + if (approvalCheck) { + bool isApprovedOrOwner = (_msgSender() == from || + isApprovedForAll(from, _msgSender()) || + getApproved(tokenId) == _msgSender()); + + if (!isApprovedOrOwner) revert TransferCallerNotOwnerNorApproved(); + } + + _beforeTokenTransfers(from, address(0), tokenId, 1); + + // Clear approvals from the previous owner + _approve(address(0), tokenId, from); + + // Underflow of the sender's balance is impossible because we check for + // ownership above and the recipient's balance can't realistically overflow. + // Counter overflow is incredibly unrealistic as tokenId would have to be 2**256. + unchecked { + AddressData storage addressData = _addressData[from]; + addressData.balance -= 1; + addressData.numberBurned += 1; + + // Keep track of who burned the token, and the timestamp of burning. + TokenOwnership storage currSlot = _ownerships[tokenId]; + currSlot.addr = from; + currSlot.startTimestamp = uint64(block.timestamp); + currSlot.burned = true; + + // If the ownership slot of tokenId+1 is not explicitly set, that means the burn initiator owns it. + // Set the slot of tokenId+1 explicitly in storage to maintain correctness for ownerOf(tokenId+1) calls. + uint256 nextTokenId = tokenId + 1; + TokenOwnership storage nextSlot = _ownerships[nextTokenId]; + if (nextSlot.addr == address(0)) { + // This will suffice for checking _exists(nextTokenId), + // as a burned slot cannot contain the zero address. + if (nextTokenId != _currentIndex) { + nextSlot.addr = from; + nextSlot.startTimestamp = prevOwnership.startTimestamp; + } + } + } + + emit Transfer(from, address(0), tokenId); + _afterTokenTransfers(from, address(0), tokenId, 1); + + // Overflow not possible, as _burnCounter cannot be exceed _currentIndex times. + unchecked { + _burnCounter++; + } + } + + /** + * @dev Approve `to` to operate on `tokenId` + * + * Emits a {Approval} event. + */ + function _approve(address to, uint256 tokenId, address owner) private { + _tokenApprovals[tokenId] = to; + emit Approval(owner, to, tokenId); + } + + /** + * @dev Internal function to invoke {IERC721Receiver-onERC721Received} on a target contract. + * + * @param from address representing the previous owner of the given token ID + * @param to target address that will receive the tokens + * @param tokenId uint256 ID of the token to be transferred + * @param _data bytes optional data to send along with the call + * @return bool whether the call correctly returned the expected magic value + */ + function _checkContractOnERC721Received( + address from, + address to, + uint256 tokenId, + bytes memory _data + ) private returns (bool) { + try IERC721ReceiverUpgradeable(to).onERC721Received(_msgSender(), from, tokenId, _data) returns ( + bytes4 retval + ) { + return retval == IERC721ReceiverUpgradeable(to).onERC721Received.selector; + } catch (bytes memory reason) { + if (reason.length == 0) { + revert TransferToNonERC721ReceiverImplementer(); + } else { + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } + + /** + * @dev Hook that is called before a set of serially-ordered token ids are about to be transferred. This includes minting. + * And also called before burning one token. + * + * startTokenId - the first token id to be transferred + * quantity - the amount to be transferred + * + * Calling conditions: + * + * - When `from` and `to` are both non-zero, `from`'s `tokenId` will be + * transferred to `to`. + * - When `from` is zero, `tokenId` will be minted for `to`. + * - When `to` is zero, `tokenId` will be burned by `from`. + * - `from` and `to` are never both zero. + */ + function _beforeTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity) internal virtual {} + + /** + * @dev Hook that is called after a set of serially-ordered token ids have been transferred. This includes + * minting. + * And also called after one token has been burned. + * + * startTokenId - the first token id to be transferred + * quantity - the amount to be transferred + * + * Calling conditions: + * + * - When `from` and `to` are both non-zero, `from`'s `tokenId` has been + * transferred to `to`. + * - When `from` is zero, `tokenId` has been minted for `to`. + * - When `to` is zero, `tokenId` has been burned by `from`. + * - `from` and `to` are never both zero. + */ + function _afterTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity) internal virtual {} + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[42] private __gap; +} diff --git a/contracts/eip/interface/IERC1155.sol b/contracts/eip/interface/IERC1155.sol index cfe341157..1f2d9e919 100644 --- a/contracts/eip/interface/IERC1155.sol +++ b/contracts/eip/interface/IERC1155.sol @@ -69,13 +69,7 @@ interface IERC1155 { @param _value Transfer amount @param _data Additional data with no specified format, MUST be sent unaltered in call to `onERC1155Received` on `_to` */ - function safeTransferFrom( - address _from, - address _to, - uint256 _id, - uint256 _value, - bytes calldata _data - ) external; + function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data) external; /** @notice Transfers `_values` amount(s) of `_ids` from the `_from` address to the `_to` address specified (with safety call). @@ -115,10 +109,10 @@ interface IERC1155 { @param _ids ID of the Tokens @return The _owner's balance of the Token types requested (i.e. balance for each (owner, id) pair) */ - function balanceOfBatch(address[] calldata _owners, uint256[] calldata _ids) - external - view - returns (uint256[] memory); + function balanceOfBatch( + address[] calldata _owners, + uint256[] calldata _ids + ) external view returns (uint256[] memory); /** @notice Enable or disable approval for a third party ("operator") to manage all of the caller's tokens. diff --git a/contracts/eip/interface/IERC1155Receiver.sol b/contracts/eip/interface/IERC1155Receiver.sol new file mode 100644 index 000000000..8a38c31e8 --- /dev/null +++ b/contracts/eip/interface/IERC1155Receiver.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC1155/IERC1155Receiver.sol) + +pragma solidity ^0.8.0; + +import "./IERC165.sol"; + +/** + * @dev _Available since v3.1._ + */ +interface IERC1155Receiver is IERC165 { + /** + * @dev Handles the receipt of a single ERC1155 token type. This function is + * called at the end of a `safeTransferFrom` after the balance has been updated. + * + * NOTE: To accept the transfer, this must return + * `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + * (i.e. 0xf23a6e61, or its own function selector). + * + * @param operator The address which initiated the transfer (i.e. msg.sender) + * @param from The address which previously owned the token + * @param id The ID of the token being transferred + * @param value The amount of tokens being transferred + * @param data Additional data with no specified format + * @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed + */ + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes calldata data + ) external returns (bytes4); + + /** + * @dev Handles the receipt of a multiple ERC1155 token types. This function + * is called at the end of a `safeBatchTransferFrom` after the balances have + * been updated. + * + * NOTE: To accept the transfer(s), this must return + * `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + * (i.e. 0xbc197c81, or its own function selector). + * + * @param operator The address which initiated the batch transfer (i.e. msg.sender) + * @param from The address which previously owned the token + * @param ids An array containing ids of each token being transferred (order and length must match values array) + * @param values An array containing amounts of each token being transferred (order and length must match ids array) + * @param data Additional data with no specified format + * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed + */ + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external returns (bytes4); +} diff --git a/contracts/eip/interface/IERC165.sol b/contracts/eip/interface/IERC165.sol index e8cdbdbf6..281c3f2db 100644 --- a/contracts/eip/interface/IERC165.sol +++ b/contracts/eip/interface/IERC165.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; /** * @dev Interface of the ERC165 standard, as defined in the - * https://eips.ethereum.org/EIPS/eip-165[EIP]. + * [EIP](https://eips.ethereum.org/EIPS/eip-165). * * Implementers can declare support of contract interfaces, which can then be * queried by others ({ERC165Checker}). @@ -16,7 +16,7 @@ interface IERC165 { /** * @dev Returns true if this contract implements the interface defined by * `interfaceId`. See the corresponding - * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] + * [EIP section](https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified) * to learn more about how these ids are created. * * This function call must use less than 30 000 gas. diff --git a/contracts/eip/interface/IERC20.sol b/contracts/eip/interface/IERC20.sol index 6350dfc2c..246af8424 100644 --- a/contracts/eip/interface/IERC20.sol +++ b/contracts/eip/interface/IERC20.sol @@ -16,11 +16,7 @@ interface IERC20 { function approve(address spender, uint256 value) external returns (bool); - function transferFrom( - address from, - address to, - uint256 value - ) external returns (bool); + function transferFrom(address from, address to, uint256 value) external returns (bool); event Transfer(address indexed from, address indexed to, uint256 value); diff --git a/contracts/eip/interface/IERC20Permit.sol b/contracts/eip/interface/IERC20Permit.sol new file mode 100644 index 000000000..6363b1408 --- /dev/null +++ b/contracts/eip/interface/IERC20Permit.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/draft-IERC20Permit.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by + * presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + */ +interface IERC20Permit { + /** + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * + * IMPORTANT: The same issues {IERC20-approve} has related to transaction + * ordering also apply here. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + * + * For more information on the signature format, see the + * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /** + * @dev Returns the current nonce for `owner`. This value must be + * included whenever a signature is generated for {permit}. + * + * Every successful call to {permit} increases ``owner``'s nonce by one. This + * prevents a signature from being used multiple times. + */ + function nonces(address owner) external view returns (uint256); + + /** + * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32); +} diff --git a/contracts/eip/interface/IERC2981.sol b/contracts/eip/interface/IERC2981.sol index 79276a538..e25265b33 100644 --- a/contracts/eip/interface/IERC2981.sol +++ b/contracts/eip/interface/IERC2981.sol @@ -16,8 +16,8 @@ interface IERC2981 is IERC165 { * @dev Returns how much royalty is owed and to whom, based on a sale price that may be denominated in any unit of * exchange. The royalty amount is denominated and should be payed in that same unit of exchange. */ - function royaltyInfo(uint256 tokenId, uint256 salePrice) - external - view - returns (address receiver, uint256 royaltyAmount); + function royaltyInfo( + uint256 tokenId, + uint256 salePrice + ) external view returns (address receiver, uint256 royaltyAmount); } diff --git a/contracts/eip/interface/IERC4906.sol b/contracts/eip/interface/IERC4906.sol new file mode 100644 index 000000000..d52537eff --- /dev/null +++ b/contracts/eip/interface/IERC4906.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "./IERC165.sol"; +import "./IERC721.sol"; + +interface IERC4906 is IERC165 { + /// @dev This event emits when the metadata of a token is changed. + /// So that the third-party platforms such as NFT market could + /// timely update the images and related attributes of the NFT. + event MetadataUpdate(uint256 _tokenId); + + /// @dev This event emits when the metadata of a range of tokens is changed. + /// So that the third-party platforms such as NFT market could + /// timely update the images and related attributes of the NFTs. + event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); +} diff --git a/contracts/eip/interface/IERC721.sol b/contracts/eip/interface/IERC721.sol index 62fca471e..6bc14e70d 100644 --- a/contracts/eip/interface/IERC721.sol +++ b/contracts/eip/interface/IERC721.sol @@ -50,11 +50,7 @@ interface IERC721 { * * Emits a {Transfer} event. */ - function safeTransferFrom( - address from, - address to, - uint256 tokenId - ) external; + function safeTransferFrom(address from, address to, uint256 tokenId) external; /** * @dev Transfers `tokenId` token from `from` to `to`. @@ -70,11 +66,7 @@ interface IERC721 { * * Emits a {Transfer} event. */ - function transferFrom( - address from, - address to, - uint256 tokenId - ) external; + function transferFrom(address from, address to, uint256 tokenId) external; /** * @dev Gives permission to `to` to transfer `tokenId` token to another account. @@ -132,10 +124,5 @@ interface IERC721 { * * Emits a {Transfer} event. */ - function safeTransferFrom( - address from, - address to, - uint256 tokenId, - bytes calldata data - ) external; + function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external; } diff --git a/contracts/openzeppelin-presets/token/ERC721/IERC721Receiver.sol b/contracts/eip/interface/IERC721Receiver.sol similarity index 100% rename from contracts/openzeppelin-presets/token/ERC721/IERC721Receiver.sol rename to contracts/eip/interface/IERC721Receiver.sol diff --git a/contracts/eip/queryable/ERC721AQueryable.sol b/contracts/eip/queryable/ERC721AQueryable.sol new file mode 100644 index 000000000..a5026b487 --- /dev/null +++ b/contracts/eip/queryable/ERC721AQueryable.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: MIT +// ERC721A Contracts v3.3.0 +// Creator: Chiru Labs + +pragma solidity ^0.8.4; + +import "./IERC721AQueryable.sol"; +import "../ERC721AVirtualApprove.sol"; + +/** + * @title ERC721A Queryable + * @dev ERC721A subclass with convenience query functions. + */ +abstract contract ERC721AQueryable is ERC721A, IERC721AQueryable { + /** + * @dev Returns the `TokenOwnership` struct at `tokenId` without reverting. + * + * If the `tokenId` is out of bounds: + * - `addr` = `address(0)` + * - `startTimestamp` = `0` + * - `burned` = `false` + * + * If the `tokenId` is burned: + * - `addr` = `
` + * - `startTimestamp` = `` + * - `burned = `true` + * + * Otherwise: + * - `addr` = `
` + * - `startTimestamp` = `` + * - `burned = `false` + */ + function explicitOwnershipOf(uint256 tokenId) public view override returns (TokenOwnership memory) { + TokenOwnership memory ownership; + if (tokenId < _startTokenId() || tokenId >= _currentIndex) { + return ownership; + } + ownership = _ownerships[tokenId]; + if (ownership.burned) { + return ownership; + } + return _ownershipOf(tokenId); + } + + /** + * @dev Returns an array of `TokenOwnership` structs at `tokenIds` in order. + * See {ERC721AQueryable-explicitOwnershipOf} + */ + function explicitOwnershipsOf(uint256[] memory tokenIds) external view override returns (TokenOwnership[] memory) { + unchecked { + uint256 tokenIdsLength = tokenIds.length; + TokenOwnership[] memory ownerships = new TokenOwnership[](tokenIdsLength); + for (uint256 i; i != tokenIdsLength; ++i) { + ownerships[i] = explicitOwnershipOf(tokenIds[i]); + } + return ownerships; + } + } + + /** + * @dev Returns an array of token IDs owned by `owner`, + * in the range [`start`, `stop`) + * (i.e. `start <= tokenId < stop`). + * + * This function allows for tokens to be queried if the collection + * grows too big for a single call of {ERC721AQueryable-tokensOfOwner}. + * + * Requirements: + * + * - `start` < `stop` + */ + /* solhint-disable*/ + function tokensOfOwnerIn( + address owner, + uint256 start, + uint256 stop + ) external view override returns (uint256[] memory) { + unchecked { + if (start >= stop) revert InvalidQueryRange(); + uint256 tokenIdsIdx; + uint256 stopLimit = _currentIndex; + // Set `start = max(start, _startTokenId())`. + if (start < _startTokenId()) { + start = _startTokenId(); + } + // Set `stop = min(stop, _currentIndex)`. + if (stop > stopLimit) { + stop = stopLimit; + } + uint256 tokenIdsMaxLength = balanceOf(owner); + // Set `tokenIdsMaxLength = min(balanceOf(owner), stop - start)`, + // to cater for cases where `balanceOf(owner)` is too big. + if (start < stop) { + uint256 rangeLength = stop - start; + if (rangeLength < tokenIdsMaxLength) { + tokenIdsMaxLength = rangeLength; + } + } else { + tokenIdsMaxLength = 0; + } + uint256[] memory tokenIds = new uint256[](tokenIdsMaxLength); + if (tokenIdsMaxLength == 0) { + return tokenIds; + } + // We need to call `explicitOwnershipOf(start)`, + // because the slot at `start` may not be initialized. + TokenOwnership memory ownership = explicitOwnershipOf(start); + address currOwnershipAddr; + // If the starting slot exists (i.e. not burned), initialize `currOwnershipAddr`. + // `ownership.address` will not be zero, as `start` is clamped to the valid token ID range. + if (!ownership.burned) { + currOwnershipAddr = ownership.addr; + } + for (uint256 i = start; i != stop && tokenIdsIdx != tokenIdsMaxLength; ++i) { + ownership = _ownerships[i]; + if (ownership.burned) { + continue; + } + if (ownership.addr != address(0)) { + currOwnershipAddr = ownership.addr; + } + if (currOwnershipAddr == owner) { + tokenIds[tokenIdsIdx++] = i; + } + } + // Downsize the array to fit. + assembly { + mstore(tokenIds, tokenIdsIdx) + } + return tokenIds; + } + } + + /* solhint-enable */ + + /** + * @dev Returns an array of token IDs owned by `owner`. + * + * This function scans the ownership mapping and is O(totalSupply) in complexity. + * It is meant to be called off-chain. + * + * See {ERC721AQueryable-tokensOfOwnerIn} for splitting the scan into + * multiple smaller scans if the collection is large enough to cause + * an out-of-gas error (10K pfp collections should be fine). + */ + function tokensOfOwner(address owner) external view override returns (uint256[] memory) { + unchecked { + uint256 tokenIdsIdx; + address currOwnershipAddr; + uint256 tokenIdsLength = balanceOf(owner); + uint256[] memory tokenIds = new uint256[](tokenIdsLength); + TokenOwnership memory ownership; + for (uint256 i = _startTokenId(); tokenIdsIdx != tokenIdsLength; ++i) { + ownership = _ownerships[i]; + if (ownership.burned) { + continue; + } + if (ownership.addr != address(0)) { + currOwnershipAddr = ownership.addr; + } + if (currOwnershipAddr == owner) { + tokenIds[tokenIdsIdx++] = i; + } + } + return tokenIds; + } + } +} diff --git a/contracts/eip/queryable/ERC721AQueryableUpgradeable.sol b/contracts/eip/queryable/ERC721AQueryableUpgradeable.sol new file mode 100644 index 000000000..9eec8f7b6 --- /dev/null +++ b/contracts/eip/queryable/ERC721AQueryableUpgradeable.sol @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: MIT +// ERC721A Contracts v4.2.3 +// Creator: Chiru Labs + +pragma solidity ^0.8.4; + +import "./IERC721AQueryableUpgradeable.sol"; +import "./ERC721AUpgradeable.sol"; +import "./ERC721A__Initializable.sol"; + +/** + * @title ERC721AQueryable. + * + * @dev ERC721A subclass with convenience query functions. + */ +abstract contract ERC721AQueryableUpgradeable is + ERC721A__Initializable, + ERC721AUpgradeable, + IERC721AQueryableUpgradeable +{ + function __ERC721AQueryable_init() internal onlyInitializingERC721A { + __ERC721AQueryable_init_unchained(); + } + + function __ERC721AQueryable_init_unchained() internal onlyInitializingERC721A {} + + /** + * @dev Returns the `TokenOwnership` struct at `tokenId` without reverting. + * + * If the `tokenId` is out of bounds: + * + * - `addr = address(0)` + * - `startTimestamp = 0` + * - `burned = false` + * - `extraData = 0` + * + * If the `tokenId` is burned: + * + * - `addr =
` + * - `startTimestamp = ` + * - `burned = true` + * - `extraData = ` + * + * Otherwise: + * + * - `addr =
` + * - `startTimestamp = ` + * - `burned = false` + * - `extraData = ` + */ + function explicitOwnershipOf( + uint256 tokenId + ) public view virtual override returns (TokenOwnership memory ownership) { + unchecked { + if (tokenId >= _startTokenId()) { + if (tokenId < _nextTokenId()) { + // If the `tokenId` is within bounds, + // scan backwards for the initialized ownership slot. + while (!_ownershipIsInitialized(tokenId)) --tokenId; + return _ownershipAt(tokenId); + } + } + } + } + + /** + * @dev Returns an array of token IDs owned by `owner`, + * in the range [`start`, `stop`) + * (i.e. `start <= tokenId < stop`). + * + * This function allows for tokens to be queried if the collection + * grows too big for a single call of {ERC721AQueryable-tokensOfOwner}. + * + * Requirements: + * + * - `start < stop` + */ + function tokensOfOwnerIn( + address owner, + uint256 start, + uint256 stop + ) external view virtual override returns (uint256[] memory) { + return _tokensOfOwnerIn(owner, start, stop); + } + + /** + * @dev Returns an array of token IDs owned by `owner`. + * + * This function scans the ownership mapping and is O(`totalSupply`) in complexity. + * It is meant to be called off-chain. + * + * See {ERC721AQueryable-tokensOfOwnerIn} for splitting the scan into + * multiple smaller scans if the collection is large enough to cause + * an out-of-gas error (10K collections should be fine). + */ + function tokensOfOwner(address owner) external view virtual override returns (uint256[] memory) { + uint256 start = _startTokenId(); + uint256 stop = _nextTokenId(); + uint256[] memory tokenIds; + if (start != stop) tokenIds = _tokensOfOwnerIn(owner, start, stop); + return tokenIds; + } + + /** + * @dev Helper function for returning an array of token IDs owned by `owner`. + * + * Note that this function is optimized for smaller bytecode size over runtime gas, + * since it is meant to be called off-chain. + */ + function _tokensOfOwnerIn(address owner, uint256 start, uint256 stop) private view returns (uint256[] memory) { + unchecked { + if (start >= stop) _revert(InvalidQueryRange.selector); + // Set `start = max(start, _startTokenId())`. + if (start < _startTokenId()) { + start = _startTokenId(); + } + uint256 stopLimit = _nextTokenId(); + // Set `stop = min(stop, stopLimit)`. + if (stop >= stopLimit) { + stop = stopLimit; + } + uint256[] memory tokenIds; + uint256 tokenIdsMaxLength = balanceOf(owner); + bool startLtStop = start < stop; + assembly { + // Set `tokenIdsMaxLength` to zero if `start` is less than `stop`. + tokenIdsMaxLength := mul(tokenIdsMaxLength, startLtStop) + } + if (tokenIdsMaxLength != 0) { + // Set `tokenIdsMaxLength = min(balanceOf(owner), stop - start)`, + // to cater for cases where `balanceOf(owner)` is too big. + if (stop - start <= tokenIdsMaxLength) { + tokenIdsMaxLength = stop - start; + } + assembly { + // Grab the free memory pointer. + tokenIds := mload(0x40) + // Allocate one word for the length, and `tokenIdsMaxLength` words + // for the data. `shl(5, x)` is equivalent to `mul(32, x)`. + mstore(0x40, add(tokenIds, shl(5, add(tokenIdsMaxLength, 1)))) + } + // We need to call `explicitOwnershipOf(start)`, + // because the slot at `start` may not be initialized. + TokenOwnership memory ownership = explicitOwnershipOf(start); + address currOwnershipAddr; + // If the starting slot exists (i.e. not burned), + // initialize `currOwnershipAddr`. + // `ownership.address` will not be zero, + // as `start` is clamped to the valid token ID range. + if (!ownership.burned) { + currOwnershipAddr = ownership.addr; + } + uint256 tokenIdsIdx; + // Use a do-while, which is slightly more efficient for this case, + // as the array will at least contain one element. + do { + ownership = _ownershipAt(start); + assembly { + switch mload(add(ownership, 0x40)) + // if `ownership.burned == false`. + case 0 { + // if `ownership.addr != address(0)`. + // The `addr` already has it's upper 96 bits clearned, + // since it is written to memory with regular Solidity. + if mload(ownership) { + currOwnershipAddr := mload(ownership) + } + // if `currOwnershipAddr == owner`. + // The `shl(96, x)` is to make the comparison agnostic to any + // dirty upper 96 bits in `owner`. + if iszero(shl(96, xor(currOwnershipAddr, owner))) { + tokenIdsIdx := add(tokenIdsIdx, 1) + mstore(add(tokenIds, shl(5, tokenIdsIdx)), start) + } + } + // Otherwise, reset `currOwnershipAddr`. + // This handles the case of batch burned tokens + // (burned bit of first slot set, remaining slots left uninitialized). + default { + currOwnershipAddr := 0 + } + start := add(start, 1) + } + } while (!(start == stop || tokenIdsIdx == tokenIdsMaxLength)); + // Store the length of the array. + assembly { + mstore(tokenIds, tokenIdsIdx) + } + } + return tokenIds; + } + } +} diff --git a/contracts/eip/queryable/ERC721AStorage.sol b/contracts/eip/queryable/ERC721AStorage.sol new file mode 100644 index 000000000..d9f5c12cd --- /dev/null +++ b/contracts/eip/queryable/ERC721AStorage.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +library ERC721AStorage { + // Bypass for a `--via-ir` bug (https://github.com/chiru-labs/ERC721A/pull/364). + struct TokenApprovalRef { + address value; + } + + struct Layout { + // ============================================================= + // STORAGE + // ============================================================= + + // The next token ID to be minted. + uint256 _currentIndex; + // The number of tokens burned. + uint256 _burnCounter; + // Token name + string _name; + // Token symbol + string _symbol; + // Mapping from token ID to ownership details + // An empty struct value does not necessarily mean the token is unowned. + // See {_packedOwnershipOf} implementation for details. + // + // Bits Layout: + // - [0..159] `addr` + // - [160..223] `startTimestamp` + // - [224] `burned` + // - [225] `nextInitialized` + // - [232..255] `extraData` + mapping(uint256 => uint256) _packedOwnerships; + // Mapping owner address to address data. + // + // Bits Layout: + // - [0..63] `balance` + // - [64..127] `numberMinted` + // - [128..191] `numberBurned` + // - [192..255] `aux` + mapping(address => uint256) _packedAddressData; + // Mapping from token ID to approved address. + mapping(uint256 => ERC721AStorage.TokenApprovalRef) _tokenApprovals; + // Mapping from owner to operator approvals + mapping(address => mapping(address => bool)) _operatorApprovals; + } + + bytes32 internal constant STORAGE_SLOT = keccak256("ERC721A.contracts.storage.ERC721A"); + + function layout() internal pure returns (Layout storage l) { + bytes32 slot = STORAGE_SLOT; + assembly { + l.slot := slot + } + } +} diff --git a/contracts/eip/queryable/ERC721AUpgradeable.sol b/contracts/eip/queryable/ERC721AUpgradeable.sol new file mode 100644 index 000000000..af8bb4204 --- /dev/null +++ b/contracts/eip/queryable/ERC721AUpgradeable.sol @@ -0,0 +1,1075 @@ +// SPDX-License-Identifier: MIT +// ERC721A Contracts v4.2.3 +// Creator: Chiru Labs + +pragma solidity ^0.8.4; + +import "./IERC721AUpgradeable.sol"; +import { ERC721AStorage } from "./ERC721AStorage.sol"; +import "./ERC721A__Initializable.sol"; + +/** + * @dev Interface of ERC721 token receiver. + */ +interface ERC721A__IERC721ReceiverUpgradeable { + function onERC721Received( + address operator, + address from, + uint256 tokenId, + bytes calldata data + ) external returns (bytes4); +} + +/** + * @title ERC721A + * + * @dev Implementation of the [ERC721](https://eips.ethereum.org/EIPS/eip-721) + * Non-Fungible Token Standard, including the Metadata extension. + * Optimized for lower gas during batch mints. + * + * Token IDs are minted in sequential order (e.g. 0, 1, 2, 3, ...) + * starting from `_startTokenId()`. + * + * Assumptions: + * + * - An owner cannot have more than 2**64 - 1 (max value of uint64) of supply. + * - The maximum token ID cannot exceed 2**256 - 1 (max value of uint256). + */ +contract ERC721AUpgradeable is ERC721A__Initializable, IERC721AUpgradeable { + using ERC721AStorage for ERC721AStorage.Layout; + + // ============================================================= + // CONSTANTS + // ============================================================= + + // Mask of an entry in packed address data. + uint256 private constant _BITMASK_ADDRESS_DATA_ENTRY = (1 << 64) - 1; + + // The bit position of `numberMinted` in packed address data. + uint256 private constant _BITPOS_NUMBER_MINTED = 64; + + // The bit position of `numberBurned` in packed address data. + uint256 private constant _BITPOS_NUMBER_BURNED = 128; + + // The bit position of `aux` in packed address data. + uint256 private constant _BITPOS_AUX = 192; + + // Mask of all 256 bits in packed address data except the 64 bits for `aux`. + uint256 private constant _BITMASK_AUX_COMPLEMENT = (1 << 192) - 1; + + // The bit position of `startTimestamp` in packed ownership. + uint256 private constant _BITPOS_START_TIMESTAMP = 160; + + // The bit mask of the `burned` bit in packed ownership. + uint256 private constant _BITMASK_BURNED = 1 << 224; + + // The bit position of the `nextInitialized` bit in packed ownership. + uint256 private constant _BITPOS_NEXT_INITIALIZED = 225; + + // The bit mask of the `nextInitialized` bit in packed ownership. + uint256 private constant _BITMASK_NEXT_INITIALIZED = 1 << 225; + + // The bit position of `extraData` in packed ownership. + uint256 private constant _BITPOS_EXTRA_DATA = 232; + + // Mask of all 256 bits in a packed ownership except the 24 bits for `extraData`. + uint256 private constant _BITMASK_EXTRA_DATA_COMPLEMENT = (1 << 232) - 1; + + // The mask of the lower 160 bits for addresses. + uint256 private constant _BITMASK_ADDRESS = (1 << 160) - 1; + + // The maximum `quantity` that can be minted with {_mintERC2309}. + // This limit is to prevent overflows on the address data entries. + // For a limit of 5000, a total of 3.689e15 calls to {_mintERC2309} + // is required to cause an overflow, which is unrealistic. + uint256 private constant _MAX_MINT_ERC2309_QUANTITY_LIMIT = 5000; + + // The `Transfer` event signature is given by: + // `keccak256(bytes("Transfer(address,address,uint256)"))`. + bytes32 private constant _TRANSFER_EVENT_SIGNATURE = + 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef; + + // ============================================================= + // CONSTRUCTOR + // ============================================================= + + function __ERC721A_init(string memory name_, string memory symbol_) internal onlyInitializingERC721A { + __ERC721A_init_unchained(name_, symbol_); + } + + function __ERC721A_init_unchained(string memory name_, string memory symbol_) internal onlyInitializingERC721A { + ERC721AStorage.layout()._name = name_; + ERC721AStorage.layout()._symbol = symbol_; + ERC721AStorage.layout()._currentIndex = _startTokenId(); + } + + // ============================================================= + // TOKEN COUNTING OPERATIONS + // ============================================================= + + /** + * @dev Returns the starting token ID. + * To change the starting token ID, please override this function. + */ + function _startTokenId() internal view virtual returns (uint256) { + return 0; + } + + /** + * @dev Returns the next token ID to be minted. + */ + function _nextTokenId() internal view virtual returns (uint256) { + return ERC721AStorage.layout()._currentIndex; + } + + /** + * @dev Returns the total number of tokens in existence. + * Burned tokens will reduce the count. + * To get the total number of tokens minted, please see {_totalMinted}. + */ + function totalSupply() public view virtual override returns (uint256) { + // Counter underflow is impossible as _burnCounter cannot be incremented + // more than `_currentIndex - _startTokenId()` times. + unchecked { + return ERC721AStorage.layout()._currentIndex - ERC721AStorage.layout()._burnCounter - _startTokenId(); + } + } + + /** + * @dev Returns the total amount of tokens minted in the contract. + */ + function _totalMinted() internal view virtual returns (uint256) { + // Counter underflow is impossible as `_currentIndex` does not decrement, + // and it is initialized to `_startTokenId()`. + unchecked { + return ERC721AStorage.layout()._currentIndex - _startTokenId(); + } + } + + /** + * @dev Returns the total number of tokens burned. + */ + function _totalBurned() internal view virtual returns (uint256) { + return ERC721AStorage.layout()._burnCounter; + } + + // ============================================================= + // ADDRESS DATA OPERATIONS + // ============================================================= + + /** + * @dev Returns the number of tokens in `owner`'s account. + */ + function balanceOf(address owner) public view virtual override returns (uint256) { + if (owner == address(0)) _revert(BalanceQueryForZeroAddress.selector); + return ERC721AStorage.layout()._packedAddressData[owner] & _BITMASK_ADDRESS_DATA_ENTRY; + } + + /** + * Returns the number of tokens minted by `owner`. + */ + function _numberMinted(address owner) internal view returns (uint256) { + return + (ERC721AStorage.layout()._packedAddressData[owner] >> _BITPOS_NUMBER_MINTED) & _BITMASK_ADDRESS_DATA_ENTRY; + } + + /** + * Returns the number of tokens burned by or on behalf of `owner`. + */ + function _numberBurned(address owner) internal view returns (uint256) { + return + (ERC721AStorage.layout()._packedAddressData[owner] >> _BITPOS_NUMBER_BURNED) & _BITMASK_ADDRESS_DATA_ENTRY; + } + + /** + * Returns the auxiliary data for `owner`. (e.g. number of whitelist mint slots used). + */ + function _getAux(address owner) internal view returns (uint64) { + return uint64(ERC721AStorage.layout()._packedAddressData[owner] >> _BITPOS_AUX); + } + + /** + * Sets the auxiliary data for `owner`. (e.g. number of whitelist mint slots used). + * If there are multiple variables, please pack them into a uint64. + */ + function _setAux(address owner, uint64 aux) internal virtual { + uint256 packed = ERC721AStorage.layout()._packedAddressData[owner]; + uint256 auxCasted; + // Cast `aux` with assembly to avoid redundant masking. + assembly { + auxCasted := aux + } + packed = (packed & _BITMASK_AUX_COMPLEMENT) | (auxCasted << _BITPOS_AUX); + ERC721AStorage.layout()._packedAddressData[owner] = packed; + } + + // ============================================================= + // IERC165 + // ============================================================= + + /** + * @dev Returns true if this contract implements the interface defined by + * `interfaceId`. See the corresponding + * [EIP section](https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified) + * to learn more about how these ids are created. + * + * This function call must use less than 30000 gas. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + // The interface IDs are constants representing the first 4 bytes + // of the XOR of all function selectors in the interface. + // See: [ERC165](https://eips.ethereum.org/EIPS/eip-165) + // (e.g. `bytes4(i.functionA.selector ^ i.functionB.selector ^ ...)`) + return + interfaceId == 0x01ffc9a7 || // ERC165 interface ID for ERC165. + interfaceId == 0x80ac58cd || // ERC165 interface ID for ERC721. + interfaceId == 0x5b5e139f; // ERC165 interface ID for ERC721Metadata. + } + + // ============================================================= + // IERC721Metadata + // ============================================================= + + /** + * @dev Returns the token collection name. + */ + function name() public view virtual override returns (string memory) { + return ERC721AStorage.layout()._name; + } + + /** + * @dev Returns the token collection symbol. + */ + function symbol() public view virtual override returns (string memory) { + return ERC721AStorage.layout()._symbol; + } + + /** + * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token. + */ + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + if (!_exists(tokenId)) _revert(URIQueryForNonexistentToken.selector); + + string memory baseURI = _baseURI(); + return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, _toString(tokenId))) : ""; + } + + /** + * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each + * token will be the concatenation of the `baseURI` and the `tokenId`. Empty + * by default, it can be overridden in child contracts. + */ + function _baseURI() internal view virtual returns (string memory) { + return ""; + } + + // ============================================================= + // OWNERSHIPS OPERATIONS + // ============================================================= + + /** + * @dev Returns the owner of the `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function ownerOf(uint256 tokenId) public view virtual override returns (address) { + return address(uint160(_packedOwnershipOf(tokenId))); + } + + /** + * @dev Gas spent here starts off proportional to the maximum mint batch size. + * It gradually moves to O(1) as tokens get transferred around over time. + */ + function _ownershipOf(uint256 tokenId) internal view virtual returns (TokenOwnership memory) { + return _unpackedOwnership(_packedOwnershipOf(tokenId)); + } + + /** + * @dev Returns the unpacked `TokenOwnership` struct at `index`. + */ + function _ownershipAt(uint256 index) internal view virtual returns (TokenOwnership memory) { + return _unpackedOwnership(ERC721AStorage.layout()._packedOwnerships[index]); + } + + /** + * @dev Returns whether the ownership slot at `index` is initialized. + * An uninitialized slot does not necessarily mean that the slot has no owner. + */ + function _ownershipIsInitialized(uint256 index) internal view virtual returns (bool) { + return ERC721AStorage.layout()._packedOwnerships[index] != 0; + } + + /** + * @dev Initializes the ownership slot minted at `index` for efficiency purposes. + */ + function _initializeOwnershipAt(uint256 index) internal virtual { + if (ERC721AStorage.layout()._packedOwnerships[index] == 0) { + ERC721AStorage.layout()._packedOwnerships[index] = _packedOwnershipOf(index); + } + } + + /** + * Returns the packed ownership data of `tokenId`. + */ + function _packedOwnershipOf(uint256 tokenId) private view returns (uint256 packed) { + if (_startTokenId() <= tokenId) { + packed = ERC721AStorage.layout()._packedOwnerships[tokenId]; + // If the data at the starting slot does not exist, start the scan. + if (packed == 0) { + if (tokenId >= ERC721AStorage.layout()._currentIndex) _revert(OwnerQueryForNonexistentToken.selector); + // Invariant: + // There will always be an initialized ownership slot + // (i.e. `ownership.addr != address(0) && ownership.burned == false`) + // before an unintialized ownership slot + // (i.e. `ownership.addr == address(0) && ownership.burned == false`) + // Hence, `tokenId` will not underflow. + // + // We can directly compare the packed value. + // If the address is zero, packed will be zero. + for (;;) { + unchecked { + packed = ERC721AStorage.layout()._packedOwnerships[--tokenId]; + } + if (packed == 0) continue; + if (packed & _BITMASK_BURNED == 0) return packed; + // Otherwise, the token is burned, and we must revert. + // This handles the case of batch burned tokens, where only the burned bit + // of the starting slot is set, and remaining slots are left uninitialized. + _revert(OwnerQueryForNonexistentToken.selector); + } + } + // Otherwise, the data exists and we can skip the scan. + // This is possible because we have already achieved the target condition. + // This saves 2143 gas on transfers of initialized tokens. + // If the token is not burned, return `packed`. Otherwise, revert. + if (packed & _BITMASK_BURNED == 0) return packed; + } + _revert(OwnerQueryForNonexistentToken.selector); + } + + /** + * @dev Returns the unpacked `TokenOwnership` struct from `packed`. + */ + function _unpackedOwnership(uint256 packed) private pure returns (TokenOwnership memory ownership) { + ownership.addr = address(uint160(packed)); + ownership.startTimestamp = uint64(packed >> _BITPOS_START_TIMESTAMP); + ownership.burned = packed & _BITMASK_BURNED != 0; + ownership.extraData = uint24(packed >> _BITPOS_EXTRA_DATA); + } + + /** + * @dev Packs ownership data into a single uint256. + */ + function _packOwnershipData(address owner, uint256 flags) private view returns (uint256 result) { + assembly { + // Mask `owner` to the lower 160 bits, in case the upper bits somehow aren't clean. + owner := and(owner, _BITMASK_ADDRESS) + // `owner | (block.timestamp << _BITPOS_START_TIMESTAMP) | flags`. + result := or(owner, or(shl(_BITPOS_START_TIMESTAMP, timestamp()), flags)) + } + } + + /** + * @dev Returns the `nextInitialized` flag set if `quantity` equals 1. + */ + function _nextInitializedFlag(uint256 quantity) private pure returns (uint256 result) { + // For branchless setting of the `nextInitialized` flag. + assembly { + // `(quantity == 1) << _BITPOS_NEXT_INITIALIZED`. + result := shl(_BITPOS_NEXT_INITIALIZED, eq(quantity, 1)) + } + } + + // ============================================================= + // APPROVAL OPERATIONS + // ============================================================= + + /** + * @dev Gives permission to `to` to transfer `tokenId` token to another account. See {ERC721A-_approve}. + * + * Requirements: + * + * - The caller must own the token or be an approved operator. + */ + function approve(address to, uint256 tokenId) public payable virtual override { + _approve(to, tokenId, true); + } + + /** + * @dev Returns the account approved for `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function getApproved(uint256 tokenId) public view virtual override returns (address) { + if (!_exists(tokenId)) _revert(ApprovalQueryForNonexistentToken.selector); + + return ERC721AStorage.layout()._tokenApprovals[tokenId].value; + } + + /** + * @dev Approve or remove `operator` as an operator for the caller. + * Operators can call {transferFrom} or {safeTransferFrom} + * for any token owned by the caller. + * + * Requirements: + * + * - The `operator` cannot be the caller. + * + * Emits an {ApprovalForAll} event. + */ + function setApprovalForAll(address operator, bool approved) public virtual override { + ERC721AStorage.layout()._operatorApprovals[_msgSenderERC721A()][operator] = approved; + emit ApprovalForAll(_msgSenderERC721A(), operator, approved); + } + + /** + * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`. + * + * See {setApprovalForAll}. + */ + function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) { + return ERC721AStorage.layout()._operatorApprovals[owner][operator]; + } + + /** + * @dev Returns whether `tokenId` exists. + * + * Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}. + * + * Tokens start existing when they are minted. See {_mint}. + */ + function _exists(uint256 tokenId) internal view virtual returns (bool result) { + if (_startTokenId() <= tokenId) { + if (tokenId < ERC721AStorage.layout()._currentIndex) { + uint256 packed; + while ((packed = ERC721AStorage.layout()._packedOwnerships[tokenId]) == 0) --tokenId; + result = packed & _BITMASK_BURNED == 0; + } + } + } + + /** + * @dev Returns whether `msgSender` is equal to `approvedAddress` or `owner`. + */ + function _isSenderApprovedOrOwner( + address approvedAddress, + address owner, + address msgSender + ) private pure returns (bool result) { + assembly { + // Mask `owner` to the lower 160 bits, in case the upper bits somehow aren't clean. + owner := and(owner, _BITMASK_ADDRESS) + // Mask `msgSender` to the lower 160 bits, in case the upper bits somehow aren't clean. + msgSender := and(msgSender, _BITMASK_ADDRESS) + // `msgSender == owner || msgSender == approvedAddress`. + result := or(eq(msgSender, owner), eq(msgSender, approvedAddress)) + } + } + + /** + * @dev Returns the storage slot and value for the approved address of `tokenId`. + */ + function _getApprovedSlotAndAddress( + uint256 tokenId + ) private view returns (uint256 approvedAddressSlot, address approvedAddress) { + ERC721AStorage.TokenApprovalRef storage tokenApproval = ERC721AStorage.layout()._tokenApprovals[tokenId]; + // The following is equivalent to `approvedAddress = _tokenApprovals[tokenId].value`. + assembly { + approvedAddressSlot := tokenApproval.slot + approvedAddress := sload(approvedAddressSlot) + } + } + + // ============================================================= + // TRANSFER OPERATIONS + // ============================================================= + + /** + * @dev Transfers `tokenId` from `from` to `to`. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token + * by either {approve} or {setApprovalForAll}. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 tokenId) public payable virtual override { + uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId); + + // Mask `from` to the lower 160 bits, in case the upper bits somehow aren't clean. + from = address(uint160(uint256(uint160(from)) & _BITMASK_ADDRESS)); + + if (address(uint160(prevOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); + + (uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId); + + // The nested ifs save around 20+ gas over a compound boolean condition. + if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A())) + if (!isApprovedForAll(from, _msgSenderERC721A())) _revert(TransferCallerNotOwnerNorApproved.selector); + + _beforeTokenTransfers(from, to, tokenId, 1); + + // Clear approvals from the previous owner. + assembly { + if approvedAddress { + // This is equivalent to `delete _tokenApprovals[tokenId]`. + sstore(approvedAddressSlot, 0) + } + } + + // Underflow of the sender's balance is impossible because we check for + // ownership above and the recipient's balance can't realistically overflow. + // Counter overflow is incredibly unrealistic as `tokenId` would have to be 2**256. + unchecked { + // We can directly increment and decrement the balances. + --ERC721AStorage.layout()._packedAddressData[from]; // Updates: `balance -= 1`. + ++ERC721AStorage.layout()._packedAddressData[to]; // Updates: `balance += 1`. + + // Updates: + // - `address` to the next owner. + // - `startTimestamp` to the timestamp of transfering. + // - `burned` to `false`. + // - `nextInitialized` to `true`. + ERC721AStorage.layout()._packedOwnerships[tokenId] = _packOwnershipData( + to, + _BITMASK_NEXT_INITIALIZED | _nextExtraData(from, to, prevOwnershipPacked) + ); + + // If the next slot may not have been initialized (i.e. `nextInitialized == false`) . + if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) { + uint256 nextTokenId = tokenId + 1; + // If the next slot's address is zero and not burned (i.e. packed value is zero). + if (ERC721AStorage.layout()._packedOwnerships[nextTokenId] == 0) { + // If the next slot is within bounds. + if (nextTokenId != ERC721AStorage.layout()._currentIndex) { + // Initialize the next slot to maintain correctness for `ownerOf(tokenId + 1)`. + ERC721AStorage.layout()._packedOwnerships[nextTokenId] = prevOwnershipPacked; + } + } + } + } + + // Mask `to` to the lower 160 bits, in case the upper bits somehow aren't clean. + uint256 toMasked = uint256(uint160(to)) & _BITMASK_ADDRESS; + assembly { + // Emit the `Transfer` event. + log4( + 0, // Start of data (0, since no data). + 0, // End of data (0, since no data). + _TRANSFER_EVENT_SIGNATURE, // Signature. + from, // `from`. + toMasked, // `to`. + tokenId // `tokenId`. + ) + } + if (toMasked == 0) _revert(TransferToZeroAddress.selector); + + _afterTokenTransfers(from, to, tokenId, 1); + } + + /** + * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`. + */ + function safeTransferFrom(address from, address to, uint256 tokenId) public payable virtual override { + safeTransferFrom(from, to, tokenId, ""); + } + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token + * by either {approve} or {setApprovalForAll}. + * - If `to` refers to a smart contract, it must implement + * {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes memory _data + ) public payable virtual override { + transferFrom(from, to, tokenId); + if (to.code.length != 0) + if (!_checkContractOnERC721Received(from, to, tokenId, _data)) { + _revert(TransferToNonERC721ReceiverImplementer.selector); + } + } + + /** + * @dev Hook that is called before a set of serially-ordered token IDs + * are about to be transferred. This includes minting. + * And also called before burning one token. + * + * `startTokenId` - the first token ID to be transferred. + * `quantity` - the amount to be transferred. + * + * Calling conditions: + * + * - When `from` and `to` are both non-zero, `from`'s `tokenId` will be + * transferred to `to`. + * - When `from` is zero, `tokenId` will be minted for `to`. + * - When `to` is zero, `tokenId` will be burned by `from`. + * - `from` and `to` are never both zero. + */ + function _beforeTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity) internal virtual {} + + /** + * @dev Hook that is called after a set of serially-ordered token IDs + * have been transferred. This includes minting. + * And also called after one token has been burned. + * + * `startTokenId` - the first token ID to be transferred. + * `quantity` - the amount to be transferred. + * + * Calling conditions: + * + * - When `from` and `to` are both non-zero, `from`'s `tokenId` has been + * transferred to `to`. + * - When `from` is zero, `tokenId` has been minted for `to`. + * - When `to` is zero, `tokenId` has been burned by `from`. + * - `from` and `to` are never both zero. + */ + function _afterTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity) internal virtual {} + + /** + * @dev Private function to invoke {IERC721Receiver-onERC721Received} on a target contract. + * + * `from` - Previous owner of the given token ID. + * `to` - Target address that will receive the token. + * `tokenId` - Token ID to be transferred. + * `_data` - Optional data to send along with the call. + * + * Returns whether the call correctly returned the expected magic value. + */ + function _checkContractOnERC721Received( + address from, + address to, + uint256 tokenId, + bytes memory _data + ) private returns (bool) { + try + ERC721A__IERC721ReceiverUpgradeable(to).onERC721Received(_msgSenderERC721A(), from, tokenId, _data) + returns (bytes4 retval) { + return retval == ERC721A__IERC721ReceiverUpgradeable(to).onERC721Received.selector; + } catch (bytes memory reason) { + if (reason.length == 0) { + _revert(TransferToNonERC721ReceiverImplementer.selector); + } + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + + // ============================================================= + // MINT OPERATIONS + // ============================================================= + + /** + * @dev Mints `quantity` tokens and transfers them to `to`. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `quantity` must be greater than 0. + * + * Emits a {Transfer} event for each mint. + */ + function _mint(address to, uint256 quantity) internal virtual { + uint256 startTokenId = ERC721AStorage.layout()._currentIndex; + if (quantity == 0) _revert(MintZeroQuantity.selector); + + _beforeTokenTransfers(address(0), to, startTokenId, quantity); + + // Overflows are incredibly unrealistic. + // `balance` and `numberMinted` have a maximum limit of 2**64. + // `tokenId` has a maximum limit of 2**256. + unchecked { + // Updates: + // - `address` to the owner. + // - `startTimestamp` to the timestamp of minting. + // - `burned` to `false`. + // - `nextInitialized` to `quantity == 1`. + ERC721AStorage.layout()._packedOwnerships[startTokenId] = _packOwnershipData( + to, + _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0) + ); + + // Updates: + // - `balance += quantity`. + // - `numberMinted += quantity`. + // + // We can directly add to the `balance` and `numberMinted`. + ERC721AStorage.layout()._packedAddressData[to] += quantity * ((1 << _BITPOS_NUMBER_MINTED) | 1); + + // Mask `to` to the lower 160 bits, in case the upper bits somehow aren't clean. + uint256 toMasked = uint256(uint160(to)) & _BITMASK_ADDRESS; + + if (toMasked == 0) _revert(MintToZeroAddress.selector); + + uint256 end = startTokenId + quantity; + uint256 tokenId = startTokenId; + + do { + assembly { + // Emit the `Transfer` event. + log4( + 0, // Start of data (0, since no data). + 0, // End of data (0, since no data). + _TRANSFER_EVENT_SIGNATURE, // Signature. + 0, // `address(0)`. + toMasked, // `to`. + tokenId // `tokenId`. + ) + } + // The `!=` check ensures that large values of `quantity` + // that overflows uint256 will make the loop run out of gas. + } while (++tokenId != end); + + ERC721AStorage.layout()._currentIndex = end; + } + _afterTokenTransfers(address(0), to, startTokenId, quantity); + } + + /** + * @dev Mints `quantity` tokens and transfers them to `to`. + * + * This function is intended for efficient minting only during contract creation. + * + * It emits only one {ConsecutiveTransfer} as defined in + * [ERC2309](https://eips.ethereum.org/EIPS/eip-2309), + * instead of a sequence of {Transfer} event(s). + * + * Calling this function outside of contract creation WILL make your contract + * non-compliant with the ERC721 standard. + * For full ERC721 compliance, substituting ERC721 {Transfer} event(s) with the ERC2309 + * {ConsecutiveTransfer} event is only permissible during contract creation. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `quantity` must be greater than 0. + * + * Emits a {ConsecutiveTransfer} event. + */ + function _mintERC2309(address to, uint256 quantity) internal virtual { + uint256 startTokenId = ERC721AStorage.layout()._currentIndex; + if (to == address(0)) _revert(MintToZeroAddress.selector); + if (quantity == 0) _revert(MintZeroQuantity.selector); + if (quantity > _MAX_MINT_ERC2309_QUANTITY_LIMIT) _revert(MintERC2309QuantityExceedsLimit.selector); + + _beforeTokenTransfers(address(0), to, startTokenId, quantity); + + // Overflows are unrealistic due to the above check for `quantity` to be below the limit. + unchecked { + // Updates: + // - `balance += quantity`. + // - `numberMinted += quantity`. + // + // We can directly add to the `balance` and `numberMinted`. + ERC721AStorage.layout()._packedAddressData[to] += quantity * ((1 << _BITPOS_NUMBER_MINTED) | 1); + + // Updates: + // - `address` to the owner. + // - `startTimestamp` to the timestamp of minting. + // - `burned` to `false`. + // - `nextInitialized` to `quantity == 1`. + ERC721AStorage.layout()._packedOwnerships[startTokenId] = _packOwnershipData( + to, + _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0) + ); + + emit ConsecutiveTransfer(startTokenId, startTokenId + quantity - 1, address(0), to); + + ERC721AStorage.layout()._currentIndex = startTokenId + quantity; + } + _afterTokenTransfers(address(0), to, startTokenId, quantity); + } + + /** + * @dev Safely mints `quantity` tokens and transfers them to `to`. + * + * Requirements: + * + * - If `to` refers to a smart contract, it must implement + * {IERC721Receiver-onERC721Received}, which is called for each safe transfer. + * - `quantity` must be greater than 0. + * + * See {_mint}. + * + * Emits a {Transfer} event for each mint. + */ + function _safeMint(address to, uint256 quantity, bytes memory _data) internal virtual { + _mint(to, quantity); + + unchecked { + if (to.code.length != 0) { + uint256 end = ERC721AStorage.layout()._currentIndex; + uint256 index = end - quantity; + do { + if (!_checkContractOnERC721Received(address(0), to, index++, _data)) { + _revert(TransferToNonERC721ReceiverImplementer.selector); + } + } while (index < end); + // Reentrancy protection. + if (ERC721AStorage.layout()._currentIndex != end) _revert(bytes4(0)); + } + } + } + + /** + * @dev Equivalent to `_safeMint(to, quantity, '')`. + */ + function _safeMint(address to, uint256 quantity) internal virtual { + _safeMint(to, quantity, ""); + } + + // ============================================================= + // APPROVAL OPERATIONS + // ============================================================= + + /** + * @dev Equivalent to `_approve(to, tokenId, false)`. + */ + function _approve(address to, uint256 tokenId) internal virtual { + _approve(to, tokenId, false); + } + + /** + * @dev Gives permission to `to` to transfer `tokenId` token to another account. + * The approval is cleared when the token is transferred. + * + * Only a single account can be approved at a time, so approving the + * zero address clears previous approvals. + * + * Requirements: + * + * - `tokenId` must exist. + * + * Emits an {Approval} event. + */ + function _approve(address to, uint256 tokenId, bool approvalCheck) internal virtual { + address owner = ownerOf(tokenId); + + if (approvalCheck && _msgSenderERC721A() != owner) + if (!isApprovedForAll(owner, _msgSenderERC721A())) { + _revert(ApprovalCallerNotOwnerNorApproved.selector); + } + + ERC721AStorage.layout()._tokenApprovals[tokenId].value = to; + emit Approval(owner, to, tokenId); + } + + // ============================================================= + // BURN OPERATIONS + // ============================================================= + + /** + * @dev Equivalent to `_burn(tokenId, false)`. + */ + function _burn(uint256 tokenId) internal virtual { + _burn(tokenId, false); + } + + /** + * @dev Destroys `tokenId`. + * The approval is cleared when the token is burned. + * + * Requirements: + * + * - `tokenId` must exist. + * + * Emits a {Transfer} event. + */ + function _burn(uint256 tokenId, bool approvalCheck) internal virtual { + uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId); + + address from = address(uint160(prevOwnershipPacked)); + + (uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId); + + if (approvalCheck) { + // The nested ifs save around 20+ gas over a compound boolean condition. + if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A())) + if (!isApprovedForAll(from, _msgSenderERC721A())) _revert(TransferCallerNotOwnerNorApproved.selector); + } + + _beforeTokenTransfers(from, address(0), tokenId, 1); + + // Clear approvals from the previous owner. + assembly { + if approvedAddress { + // This is equivalent to `delete _tokenApprovals[tokenId]`. + sstore(approvedAddressSlot, 0) + } + } + + // Underflow of the sender's balance is impossible because we check for + // ownership above and the recipient's balance can't realistically overflow. + // Counter overflow is incredibly unrealistic as `tokenId` would have to be 2**256. + unchecked { + // Updates: + // - `balance -= 1`. + // - `numberBurned += 1`. + // + // We can directly decrement the balance, and increment the number burned. + // This is equivalent to `packed -= 1; packed += 1 << _BITPOS_NUMBER_BURNED;`. + ERC721AStorage.layout()._packedAddressData[from] += (1 << _BITPOS_NUMBER_BURNED) - 1; + + // Updates: + // - `address` to the last owner. + // - `startTimestamp` to the timestamp of burning. + // - `burned` to `true`. + // - `nextInitialized` to `true`. + ERC721AStorage.layout()._packedOwnerships[tokenId] = _packOwnershipData( + from, + (_BITMASK_BURNED | _BITMASK_NEXT_INITIALIZED) | _nextExtraData(from, address(0), prevOwnershipPacked) + ); + + // If the next slot may not have been initialized (i.e. `nextInitialized == false`) . + if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) { + uint256 nextTokenId = tokenId + 1; + // If the next slot's address is zero and not burned (i.e. packed value is zero). + if (ERC721AStorage.layout()._packedOwnerships[nextTokenId] == 0) { + // If the next slot is within bounds. + if (nextTokenId != ERC721AStorage.layout()._currentIndex) { + // Initialize the next slot to maintain correctness for `ownerOf(tokenId + 1)`. + ERC721AStorage.layout()._packedOwnerships[nextTokenId] = prevOwnershipPacked; + } + } + } + } + + emit Transfer(from, address(0), tokenId); + _afterTokenTransfers(from, address(0), tokenId, 1); + + // Overflow not possible, as _burnCounter cannot be exceed _currentIndex times. + unchecked { + ERC721AStorage.layout()._burnCounter++; + } + } + + // ============================================================= + // EXTRA DATA OPERATIONS + // ============================================================= + + /** + * @dev Directly sets the extra data for the ownership data `index`. + */ + function _setExtraDataAt(uint256 index, uint24 extraData) internal virtual { + uint256 packed = ERC721AStorage.layout()._packedOwnerships[index]; + if (packed == 0) _revert(OwnershipNotInitializedForExtraData.selector); + uint256 extraDataCasted; + // Cast `extraData` with assembly to avoid redundant masking. + assembly { + extraDataCasted := extraData + } + packed = (packed & _BITMASK_EXTRA_DATA_COMPLEMENT) | (extraDataCasted << _BITPOS_EXTRA_DATA); + ERC721AStorage.layout()._packedOwnerships[index] = packed; + } + + /** + * @dev Called during each token transfer to set the 24bit `extraData` field. + * Intended to be overridden by the cosumer contract. + * + * `previousExtraData` - the value of `extraData` before transfer. + * + * Calling conditions: + * + * - When `from` and `to` are both non-zero, `from`'s `tokenId` will be + * transferred to `to`. + * - When `from` is zero, `tokenId` will be minted for `to`. + * - When `to` is zero, `tokenId` will be burned by `from`. + * - `from` and `to` are never both zero. + */ + function _extraData(address from, address to, uint24 previousExtraData) internal view virtual returns (uint24) {} + + /** + * @dev Returns the next extra data for the packed ownership data. + * The returned result is shifted into position. + */ + function _nextExtraData(address from, address to, uint256 prevOwnershipPacked) private view returns (uint256) { + uint24 extraData = uint24(prevOwnershipPacked >> _BITPOS_EXTRA_DATA); + return uint256(_extraData(from, to, extraData)) << _BITPOS_EXTRA_DATA; + } + + // ============================================================= + // OTHER OPERATIONS + // ============================================================= + + /** + * @dev Returns the message sender (defaults to `msg.sender`). + * + * If you are writing GSN compatible contracts, you need to override this function. + */ + function _msgSenderERC721A() internal view virtual returns (address) { + return msg.sender; + } + + /** + * @dev Converts a uint256 to its ASCII string decimal representation. + */ + function _toString(uint256 value) internal pure virtual returns (string memory str) { + assembly { + // The maximum value of a uint256 contains 78 digits (1 byte per digit), but + // we allocate 0xa0 bytes to keep the free memory pointer 32-byte word aligned. + // We will need 1 word for the trailing zeros padding, 1 word for the length, + // and 3 words for a maximum of 78 digits. Total: 5 * 0x20 = 0xa0. + let m := add(mload(0x40), 0xa0) + // Update the free memory pointer to allocate. + mstore(0x40, m) + // Assign the `str` to the end. + str := sub(m, 0x20) + // Zeroize the slot after the string. + mstore(str, 0) + + // Cache the end of the memory to calculate the length later. + let end := str + + // We write the string from rightmost digit to leftmost digit. + // The following is essentially a do-while loop that also handles the zero case. + // prettier-ignore + for { let temp := value } 1 {} { + str := sub(str, 1) + // Write the character to the pointer. + // The ASCII index of the '0' character is 48. + mstore8(str, add(48, mod(temp, 10))) + // Keep dividing `temp` until zero. + temp := div(temp, 10) + // prettier-ignore + if iszero(temp) { break } + } + + let length := sub(end, str) + // Move the pointer 32 bytes leftwards to make room for the length. + str := sub(str, 0x20) + // Store the length. + mstore(str, length) + } + } + + /** + * @dev For more efficient reverts. + */ + function _revert(bytes4 errorSelector) internal pure { + assembly { + mstore(0x00, errorSelector) + revert(0x00, 0x04) + } + } +} diff --git a/contracts/eip/queryable/ERC721A__Initializable.sol b/contracts/eip/queryable/ERC721A__Initializable.sol new file mode 100644 index 000000000..feba56ea7 --- /dev/null +++ b/contracts/eip/queryable/ERC721A__Initializable.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @dev This is a base contract to aid in writing upgradeable diamond facet contracts, or any kind of contract that will be deployed + * behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an + * external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer + * function so it can only be called once. The {initializer} modifier provided by this contract will have this effect. + * + * TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as + * possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}. + * + * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure + * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity. + */ + +import { ERC721A__InitializableStorage } from "./ERC721A__InitializableStorage.sol"; + +abstract contract ERC721A__Initializable { + using ERC721A__InitializableStorage for ERC721A__InitializableStorage.Layout; + + /** + * @dev Modifier to protect an initializer function from being invoked twice. + */ + modifier initializerERC721A() { + // If the contract is initializing we ignore whether _initialized is set in order to support multiple + // inheritance patterns, but we only do this in the context of a constructor, because in other contexts the + // contract may have been reentered. + require( + ERC721A__InitializableStorage.layout()._initializing + ? _isConstructor() + : !ERC721A__InitializableStorage.layout()._initialized, + "ERC721A__Initializable: contract is already initialized" + ); + + bool isTopLevelCall = !ERC721A__InitializableStorage.layout()._initializing; + if (isTopLevelCall) { + ERC721A__InitializableStorage.layout()._initializing = true; + ERC721A__InitializableStorage.layout()._initialized = true; + } + + _; + + if (isTopLevelCall) { + ERC721A__InitializableStorage.layout()._initializing = false; + } + } + + /** + * @dev Modifier to protect an initialization function so that it can only be invoked by functions with the + * {initializer} modifier, directly or indirectly. + */ + modifier onlyInitializingERC721A() { + require( + ERC721A__InitializableStorage.layout()._initializing, + "ERC721A__Initializable: contract is not initializing" + ); + _; + } + + /// @dev Returns true if and only if the function is running in the constructor + function _isConstructor() private view returns (bool) { + // extcodesize checks the size of the code stored in an address, and + // address returns the current address. Since the code is still not + // deployed when running a constructor, any checks on its code size will + // yield zero, making it an effective way to detect if a contract is + // under construction or not. + address self = address(this); + uint256 cs; + assembly { + cs := extcodesize(self) + } + return cs == 0; + } +} diff --git a/contracts/eip/queryable/ERC721A__InitializableStorage.sol b/contracts/eip/queryable/ERC721A__InitializableStorage.sol new file mode 100644 index 000000000..6b649b7b1 --- /dev/null +++ b/contracts/eip/queryable/ERC721A__InitializableStorage.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @dev This is a base storage for the initialization function for upgradeable diamond facet contracts + **/ + +library ERC721A__InitializableStorage { + struct Layout { + /* + * Indicates that the contract has been initialized. + */ + bool _initialized; + /* + * Indicates that the contract is in the process of being initialized. + */ + bool _initializing; + } + + bytes32 internal constant STORAGE_SLOT = keccak256("ERC721A.contracts.storage.initializable.facet"); + + function layout() internal pure returns (Layout storage l) { + bytes32 slot = STORAGE_SLOT; + assembly { + l.slot := slot + } + } +} diff --git a/contracts/eip/queryable/IERC721AQueryable.sol b/contracts/eip/queryable/IERC721AQueryable.sol new file mode 100644 index 000000000..06fa1ca21 --- /dev/null +++ b/contracts/eip/queryable/IERC721AQueryable.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +// ERC721A Contracts v3.3.0 +// Creator: Chiru Labs + +pragma solidity ^0.8.4; + +import "../interface/IERC721A.sol"; + +/** + * @dev Interface of an ERC721AQueryable compliant contract. + */ +interface IERC721AQueryable is IERC721A { + /** + * Invalid query range (`start` >= `stop`). + */ + error InvalidQueryRange(); + + /** + * @dev Returns the `TokenOwnership` struct at `tokenId` without reverting. + * + * If the `tokenId` is out of bounds: + * - `addr` = `address(0)` + * - `startTimestamp` = `0` + * - `burned` = `false` + * + * If the `tokenId` is burned: + * - `addr` = `
` + * - `startTimestamp` = `` + * - `burned = `true` + * + * Otherwise: + * - `addr` = `
` + * - `startTimestamp` = `` + * - `burned = `false` + */ + function explicitOwnershipOf(uint256 tokenId) external view returns (TokenOwnership memory); + + /** + * @dev Returns an array of `TokenOwnership` structs at `tokenIds` in order. + * See {ERC721AQueryable-explicitOwnershipOf} + */ + function explicitOwnershipsOf(uint256[] memory tokenIds) external view returns (TokenOwnership[] memory); + + /** + * @dev Returns an array of token IDs owned by `owner`, + * in the range [`start`, `stop`) + * (i.e. `start <= tokenId < stop`). + * + * This function allows for tokens to be queried if the collection + * grows too big for a single call of {ERC721AQueryable-tokensOfOwner}. + * + * Requirements: + * + * - `start` < `stop` + */ + function tokensOfOwnerIn(address owner, uint256 start, uint256 stop) external view returns (uint256[] memory); + + /** + * @dev Returns an array of token IDs owned by `owner`. + * + * This function scans the ownership mapping and is O(totalSupply) in complexity. + * It is meant to be called off-chain. + * + * See {ERC721AQueryable-tokensOfOwnerIn} for splitting the scan into + * multiple smaller scans if the collection is large enough to cause + * an out-of-gas error (10K pfp collections should be fine). + */ + function tokensOfOwner(address owner) external view returns (uint256[] memory); +} diff --git a/contracts/eip/queryable/IERC721AQueryableUpgradeable.sol b/contracts/eip/queryable/IERC721AQueryableUpgradeable.sol new file mode 100644 index 000000000..ac52fb68c --- /dev/null +++ b/contracts/eip/queryable/IERC721AQueryableUpgradeable.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +// ERC721A Contracts v4.2.3 +// Creator: Chiru Labs + +pragma solidity ^0.8.4; + +import "./IERC721AUpgradeable.sol"; + +/** + * @dev Interface of ERC721AQueryable. + */ +interface IERC721AQueryableUpgradeable is IERC721AUpgradeable { + /** + * Invalid query range (`start` >= `stop`). + */ + error InvalidQueryRange(); + + /** + * @dev Returns the `TokenOwnership` struct at `tokenId` without reverting. + * + * If the `tokenId` is out of bounds: + * + * - `addr = address(0)` + * - `startTimestamp = 0` + * - `burned = false` + * - `extraData = 0` + * + * If the `tokenId` is burned: + * + * - `addr =
` + * - `startTimestamp = ` + * - `burned = true` + * - `extraData = ` + * + * Otherwise: + * + * - `addr =
` + * - `startTimestamp = ` + * - `burned = false` + * - `extraData = ` + */ + function explicitOwnershipOf(uint256 tokenId) external view returns (TokenOwnership memory); + + /** + * @dev Returns an array of token IDs owned by `owner`, + * in the range [`start`, `stop`) + * (i.e. `start <= tokenId < stop`). + * + * This function allows for tokens to be queried if the collection + * grows too big for a single call of {ERC721AQueryable-tokensOfOwner}. + * + * Requirements: + * + * - `start < stop` + */ + function tokensOfOwnerIn(address owner, uint256 start, uint256 stop) external view returns (uint256[] memory); + + /** + * @dev Returns an array of token IDs owned by `owner`. + * + * This function scans the ownership mapping and is O(`totalSupply`) in complexity. + * It is meant to be called off-chain. + * + * See {ERC721AQueryable-tokensOfOwnerIn} for splitting the scan into + * multiple smaller scans if the collection is large enough to cause + * an out-of-gas error (10K collections should be fine). + */ + function tokensOfOwner(address owner) external view returns (uint256[] memory); +} diff --git a/contracts/eip/queryable/IERC721AUpgradeable.sol b/contracts/eip/queryable/IERC721AUpgradeable.sol new file mode 100644 index 000000000..a2159f064 --- /dev/null +++ b/contracts/eip/queryable/IERC721AUpgradeable.sol @@ -0,0 +1,269 @@ +// SPDX-License-Identifier: MIT +// ERC721A Contracts v4.2.3 +// Creator: Chiru Labs + +pragma solidity ^0.8.4; + +/** + * @dev Interface of ERC721A. + */ +interface IERC721AUpgradeable { + /** + * The caller must own the token or be an approved operator. + */ + error ApprovalCallerNotOwnerNorApproved(); + + /** + * The token does not exist. + */ + error ApprovalQueryForNonexistentToken(); + + /** + * Cannot query the balance for the zero address. + */ + error BalanceQueryForZeroAddress(); + + /** + * Cannot mint to the zero address. + */ + error MintToZeroAddress(); + + /** + * The quantity of tokens minted must be more than zero. + */ + error MintZeroQuantity(); + + /** + * The token does not exist. + */ + error OwnerQueryForNonexistentToken(); + + /** + * The caller must own the token or be an approved operator. + */ + error TransferCallerNotOwnerNorApproved(); + + /** + * The token must be owned by `from`. + */ + error TransferFromIncorrectOwner(); + + /** + * Cannot safely transfer to a contract that does not implement the + * ERC721Receiver interface. + */ + error TransferToNonERC721ReceiverImplementer(); + + /** + * Cannot transfer to the zero address. + */ + error TransferToZeroAddress(); + + /** + * The token does not exist. + */ + error URIQueryForNonexistentToken(); + + /** + * The `quantity` minted with ERC2309 exceeds the safety limit. + */ + error MintERC2309QuantityExceedsLimit(); + + /** + * The `extraData` cannot be set on an unintialized ownership slot. + */ + error OwnershipNotInitializedForExtraData(); + + // ============================================================= + // STRUCTS + // ============================================================= + + struct TokenOwnership { + // The address of the owner. + address addr; + // Stores the start time of ownership with minimal overhead for tokenomics. + uint64 startTimestamp; + // Whether the token has been burned. + bool burned; + // Arbitrary data similar to `startTimestamp` that can be set via {_extraData}. + uint24 extraData; + } + + // ============================================================= + // TOKEN COUNTERS + // ============================================================= + + /** + * @dev Returns the total number of tokens in existence. + * Burned tokens will reduce the count. + * To get the total number of tokens minted, please see {_totalMinted}. + */ + function totalSupply() external view returns (uint256); + + // ============================================================= + // IERC165 + // ============================================================= + + /** + * @dev Returns true if this contract implements the interface defined by + * `interfaceId`. See the corresponding + * [EIP section](https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified) + * to learn more about how these ids are created. + * + * This function call must use less than 30000 gas. + */ + function supportsInterface(bytes4 interfaceId) external view returns (bool); + + // ============================================================= + // IERC721 + // ============================================================= + + /** + * @dev Emitted when `tokenId` token is transferred from `from` to `to`. + */ + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + + /** + * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token. + */ + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + + /** + * @dev Emitted when `owner` enables or disables + * (`approved`) `operator` to manage all of its assets. + */ + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + + /** + * @dev Returns the number of tokens in `owner`'s account. + */ + function balanceOf(address owner) external view returns (uint256 balance); + + /** + * @dev Returns the owner of the `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function ownerOf(uint256 tokenId) external view returns (address owner); + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`, + * checking first that contract recipients are aware of the ERC721 protocol + * to prevent tokens from being forever locked. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If the caller is not `from`, it must be have been allowed to move + * this token by either {approve} or {setApprovalForAll}. + * - If `to` refers to a smart contract, it must implement + * {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external payable; + + /** + * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`. + */ + function safeTransferFrom(address from, address to, uint256 tokenId) external payable; + + /** + * @dev Transfers `tokenId` from `from` to `to`. + * + * WARNING: Usage of this method is discouraged, use {safeTransferFrom} + * whenever possible. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token + * by either {approve} or {setApprovalForAll}. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 tokenId) external payable; + + /** + * @dev Gives permission to `to` to transfer `tokenId` token to another account. + * The approval is cleared when the token is transferred. + * + * Only a single account can be approved at a time, so approving the + * zero address clears previous approvals. + * + * Requirements: + * + * - The caller must own the token or be an approved operator. + * - `tokenId` must exist. + * + * Emits an {Approval} event. + */ + function approve(address to, uint256 tokenId) external payable; + + /** + * @dev Approve or remove `operator` as an operator for the caller. + * Operators can call {transferFrom} or {safeTransferFrom} + * for any token owned by the caller. + * + * Requirements: + * + * - The `operator` cannot be the caller. + * + * Emits an {ApprovalForAll} event. + */ + function setApprovalForAll(address operator, bool _approved) external; + + /** + * @dev Returns the account approved for `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function getApproved(uint256 tokenId) external view returns (address operator); + + /** + * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`. + * + * See {setApprovalForAll}. + */ + function isApprovedForAll(address owner, address operator) external view returns (bool); + + // ============================================================= + // IERC721Metadata + // ============================================================= + + /** + * @dev Returns the token collection name. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the token collection symbol. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token. + */ + function tokenURI(uint256 tokenId) external view returns (string memory); + + // ============================================================= + // IERC2309 + // ============================================================= + + /** + * @dev Emitted when tokens in `fromTokenId` to `toTokenId` + * (inclusive) is transferred from `from` to `to`, as defined in the + * [ERC2309](https://eips.ethereum.org/EIPS/eip-2309) standard. + * + * See {_mintERC2309} for more details. + */ + event ConsecutiveTransfer(uint256 indexed fromTokenId, uint256 toTokenId, address indexed from, address indexed to); +} diff --git a/contracts/extension/AppURI.sol b/contracts/extension/AppURI.sol new file mode 100644 index 000000000..99c92dd56 --- /dev/null +++ b/contracts/extension/AppURI.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./interface/IAppURI.sol"; + +/** + * Thirdweb's `AppURI` is a contract extension for any contract + * that wants to add an official App URI that follows the appUri spec + * + */ + +abstract contract AppURI is IAppURI { + /// @dev appURI + string public override appURI; + + /// @dev Lets a contract admin set the URI for app metadata. + function setAppURI(string memory _uri) public override { + if (!_canSetAppURI()) { + revert("Not authorized"); + } + + _setupAppURI(_uri); + } + + /// @dev Lets a contract admin set the URI for app metadata. + function _setupAppURI(string memory _uri) internal { + string memory prevURI = appURI; + appURI = _uri; + + emit AppURIUpdated(prevURI, _uri); + } + + /// @dev Returns whether appUri can be set in the given execution context. + function _canSetAppURI() internal view virtual returns (bool); +} diff --git a/contracts/extension/BatchMintMetadata.sol b/contracts/extension/BatchMintMetadata.sol index bb205b4e0..108d43a63 100644 --- a/contracts/extension/BatchMintMetadata.sol +++ b/contracts/extension/BatchMintMetadata.sol @@ -1,48 +1,84 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + /** - * The `BatchMintMetadata` is a contract extension for any base NFT contract. It lets the smart contract - * using this extension set metadata for `n` number of NFTs all at once. This is enabled by storing a single - * base URI for a batch of `n` NFTs, where the metadata for each NFT in a relevant batch is `baseURI/tokenId`. + * @title Batch-mint Metadata + * @notice The `BatchMintMetadata` is a contract extension for any base NFT contract. It lets the smart contract + * using this extension set metadata for `n` number of NFTs all at once. This is enabled by storing a single + * base URI for a batch of `n` NFTs, where the metadata for each NFT in a relevant batch is `baseURI/tokenId`. */ contract BatchMintMetadata { - /// @dev Largest tokenId of each batch of tokens with the same baseURI. + /// @dev Invalid index for batch + error BatchMintInvalidBatchId(uint256 index); + + /// @dev Invalid token + error BatchMintInvalidTokenId(uint256 tokenId); + + /// @dev Metadata frozen + error BatchMintMetadataFrozen(uint256 batchId); + + /// @dev Largest tokenId of each batch of tokens with the same baseURI + 1 {ex: batchId 100 at position 0 includes tokens 0-99} uint256[] private batchIds; /// @dev Mapping from id of a batch of tokens => to base URI for the respective batch of tokens. mapping(uint256 => string) private baseURI; - /// @dev Returns the number of batches of tokens having the same baseURI. + /// @dev Mapping from id of a batch of tokens => to whether the base URI for the respective batch of tokens is frozen. + mapping(uint256 => bool) public batchFrozen; + + /// @dev This event emits when the metadata of all tokens are frozen. + /// While not currently supported by marketplaces, this event allows + /// future indexing if desired. + event MetadataFrozen(); + + // @dev This event emits when the metadata of a range of tokens is updated. + /// So that the third-party platforms such as NFT market could + /// timely update the images and related attributes of the NFTs. + event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); + + /** + * @notice Returns the count of batches of NFTs. + * @dev Each batch of tokens has an in ID and an associated `baseURI`. + * See {batchIds}. + */ function getBaseURICount() public view returns (uint256) { return batchIds.length; } - /// @dev Returns the id for the batch of tokens the given tokenId belongs to. + /** + * @notice Returns the ID for the batch of tokens at the given index. + * @dev See {getBaseURICount}. + * @param _index Index of the desired batch in batchIds array. + */ function getBatchIdAtIndex(uint256 _index) public view returns (uint256) { if (_index >= getBaseURICount()) { - revert("Invalid index"); + revert BatchMintInvalidBatchId(_index); } return batchIds[_index]; } /// @dev Returns the id for the batch of tokens the given tokenId belongs to. - function getBatchId(uint256 _tokenId) internal view returns (uint256) { + function _getBatchId(uint256 _tokenId) internal view returns (uint256 batchId, uint256 index) { uint256 numOfTokenBatches = getBaseURICount(); uint256[] memory indices = batchIds; for (uint256 i = 0; i < numOfTokenBatches; i += 1) { if (_tokenId < indices[i]) { - return indices[i]; + index = i; + batchId = indices[i]; + + return (batchId, index); } } - revert("Invalid tokenId"); + revert BatchMintInvalidTokenId(_tokenId); } /// @dev Returns the baseURI for a token. The intended metadata URI for the token is baseURI + tokenId. - function getBaseURI(uint256 _tokenId) internal view returns (string memory) { + function _getBaseURI(uint256 _tokenId) internal view returns (string memory) { uint256 numOfTokenBatches = getBaseURICount(); uint256[] memory indices = batchIds; @@ -51,12 +87,44 @@ contract BatchMintMetadata { return baseURI[indices[i]]; } } - revert("Invalid tokenId"); + + revert BatchMintInvalidTokenId(_tokenId); + } + + /// @dev returns the starting tokenId of a given batchId. + function _getBatchStartId(uint256 _batchID) internal view returns (uint256) { + uint256 numOfTokenBatches = getBaseURICount(); + uint256[] memory indices = batchIds; + + for (uint256 i = 0; i < numOfTokenBatches; i++) { + if (_batchID == indices[i]) { + if (i > 0) { + return indices[i - 1]; + } + return 0; + } + } + + revert BatchMintInvalidBatchId(_batchID); } /// @dev Sets the base URI for the batch of tokens with the given batchId. function _setBaseURI(uint256 _batchId, string memory _baseURI) internal { + if (batchFrozen[_batchId]) { + revert BatchMintMetadataFrozen(_batchId); + } baseURI[_batchId] = _baseURI; + emit BatchMetadataUpdate(_getBatchStartId(_batchId), _batchId); + } + + /// @dev Freezes the base URI for the batch of tokens with the given batchId. + function _freezeBaseURI(uint256 _batchId) internal { + string memory baseURIForBatch = baseURI[_batchId]; + if (bytes(baseURIForBatch).length == 0) { + revert BatchMintInvalidBatchId(_batchId); + } + batchFrozen[_batchId] = true; + emit MetadataFrozen(); } /// @dev Mints a batch of tokenIds and associates a common baseURI to all those Ids. diff --git a/contracts/extension/BurnToClaim.sol b/contracts/extension/BurnToClaim.sol new file mode 100644 index 000000000..c9e6c1a21 --- /dev/null +++ b/contracts/extension/BurnToClaim.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import { ERC1155Burnable } from "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Burnable.sol"; +import { ERC721Burnable } from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol"; + +import "../eip/interface/IERC1155.sol"; +import "../eip/interface/IERC721.sol"; + +import "../external-deps/openzeppelin/utils/Context.sol"; +import "./interface/IBurnToClaim.sol"; + +abstract contract BurnToClaim is IBurnToClaim { + BurnToClaimInfo internal burnToClaimInfo; + + function getBurnToClaimInfo() public view returns (BurnToClaimInfo memory) { + return burnToClaimInfo; + } + + function setBurnToClaimInfo(BurnToClaimInfo calldata _burnToClaimInfo) external virtual { + require(_canSetBurnToClaim(), "Not authorized."); + require(_burnToClaimInfo.originContractAddress != address(0), "Origin contract not set."); + require(_burnToClaimInfo.currency != address(0), "Currency not set."); + + burnToClaimInfo = _burnToClaimInfo; + } + + function verifyBurnToClaim(address _tokenOwner, uint256 _tokenId, uint256 _quantity) public view virtual { + BurnToClaimInfo memory _burnToClaimInfo = burnToClaimInfo; + + if (_burnToClaimInfo.tokenType == IBurnToClaim.TokenType.ERC721) { + require(_quantity == 1, "Invalid amount"); + require(IERC721(_burnToClaimInfo.originContractAddress).ownerOf(_tokenId) == _tokenOwner, "!Owner"); + } else if (_burnToClaimInfo.tokenType == IBurnToClaim.TokenType.ERC1155) { + uint256 _eligible1155TokenId = _burnToClaimInfo.tokenId; + + require(_tokenId == _eligible1155TokenId, "Invalid token Id"); + require( + IERC1155(_burnToClaimInfo.originContractAddress).balanceOf(_tokenOwner, _tokenId) >= _quantity, + "!Balance" + ); + } + + // TODO: check if additional verification steps are required / override in main contract + } + + function _burnTokensOnOrigin(address _tokenOwner, uint256 _tokenId, uint256 _quantity) internal virtual { + BurnToClaimInfo memory _burnToClaimInfo = burnToClaimInfo; + if (_burnToClaimInfo.tokenType == IBurnToClaim.TokenType.ERC721) { + ERC721Burnable(_burnToClaimInfo.originContractAddress).burn(_tokenId); + } else if (_burnToClaimInfo.tokenType == IBurnToClaim.TokenType.ERC1155) { + ERC1155Burnable(_burnToClaimInfo.originContractAddress).burn(_tokenOwner, _tokenId, _quantity); + } + // TODO: check if additional migration steps are required / override in main contract + } + + function _canSetBurnToClaim() internal view virtual returns (bool); +} diff --git a/contracts/extension/ContractMetadata.sol b/contracts/extension/ContractMetadata.sol index 6a0a181b6..ec968f2b6 100644 --- a/contracts/extension/ContractMetadata.sol +++ b/contracts/extension/ContractMetadata.sol @@ -1,23 +1,35 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + import "./interface/IContractMetadata.sol"; /** - * Thirdweb's `ContractMetadata` is a contract extension for any base contracts. It lets you set a metadata URI - * for you contract. - * - * Additionally, `ContractMetadata` is necessary for NFT contracts that want royalties to get distributed on OpenSea. + * @title Contract Metadata + * @notice Thirdweb's `ContractMetadata` is a contract extension for any base contracts. It lets you set a metadata URI + * for you contract. + * Additionally, `ContractMetadata` is necessary for NFT contracts that want royalties to get distributed on OpenSea. */ abstract contract ContractMetadata is IContractMetadata { - /// @dev Contract level metadata. + /// @dev The sender is not authorized to perform the action + error ContractMetadataUnauthorized(); + + /// @notice Returns the contract metadata URI. string public override contractURI; - /// @dev Lets a contract admin set the URI for contract-level metadata. + /** + * @notice Lets a contract admin set the URI for contract-level metadata. + * @dev Caller should be authorized to setup contractURI, e.g. contract admin. + * See {_canSetContractURI}. + * Emits {ContractURIUpdated Event}. + * + * @param _uri keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + */ function setContractURI(string memory _uri) external override { if (!_canSetContractURI()) { - revert("Not authorized"); + revert ContractMetadataUnauthorized(); } _setupContractURI(_uri); @@ -32,5 +44,5 @@ abstract contract ContractMetadata is IContractMetadata { } /// @dev Returns whether contract metadata can be set in the given execution context. - function _canSetContractURI() internal virtual returns (bool); + function _canSetContractURI() internal view virtual returns (bool); } diff --git a/contracts/extension/DelayedReveal.sol b/contracts/extension/DelayedReveal.sol index 6ada7c5d9..bc0d09d22 100644 --- a/contracts/extension/DelayedReveal.sol +++ b/contracts/extension/DelayedReveal.sol @@ -1,33 +1,70 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + import "./interface/IDelayedReveal.sol"; /** - * Thirdweb's `DelayedReveal` is a contract extension for base NFT contracts. It lets you create batches of - * 'delayed-reveal' NFTs. You can learn more about the usage of delayed reveal NFTs here - https://blog.thirdweb.com/delayed-reveal-nfts + * @title Delayed Reveal + * @notice Thirdweb's `DelayedReveal` is a contract extension for base NFT contracts. It lets you create batches of + * 'delayed-reveal' NFTs. You can learn more about the usage of delayed reveal NFTs here - https://blog.thirdweb.com/delayed-reveal-nfts */ abstract contract DelayedReveal is IDelayedReveal { - /// @dev Mapping from id of a batch of tokens => to encrypted base URI for the respective batch of tokens. - mapping(uint256 => bytes) public encryptedBaseURI; + /// @dev The contract doesn't have any url to be delayed revealed + error DelayedRevealNothingToReveal(); + + /// @dev The result of the returned an incorrect hash + error DelayedRevealIncorrectResultHash(bytes32 expected, bytes32 actual); + + /// @dev Mapping from tokenId of a batch of tokens => to delayed reveal data. + mapping(uint256 => bytes) public encryptedData; - /// @dev Sets the encrypted baseURI for a batch of tokenIds. - function _setEncryptedBaseURI(uint256 _batchId, bytes memory _encryptedBaseURI) internal { - encryptedBaseURI[_batchId] = _encryptedBaseURI; + /// @dev Sets the delayed reveal data for a batchId. + function _setEncryptedData(uint256 _batchId, bytes memory _encryptedData) internal { + encryptedData[_batchId] = _encryptedData; } - /// @dev Returns the decrypted i.e. revealed URI for a batch of tokens. + /** + * @notice Returns revealed URI for a batch of NFTs. + * @dev Reveal encrypted base URI for `_batchId` with caller/admin's `_key` used for encryption. + * Reverts if there's no encrypted URI for `_batchId`. + * See {encryptDecrypt}. + * + * @param _batchId ID of the batch for which URI is being revealed. + * @param _key Secure key used by caller/admin for encryption of baseURI. + * + * @return revealedURI Decrypted base URI. + */ function getRevealURI(uint256 _batchId, bytes calldata _key) public view returns (string memory revealedURI) { - bytes memory encryptedURI = encryptedBaseURI[_batchId]; - if (encryptedURI.length == 0) { - revert("Nothing to reveal"); + bytes memory data = encryptedData[_batchId]; + if (data.length == 0) { + revert DelayedRevealNothingToReveal(); } + (bytes memory encryptedURI, bytes32 provenanceHash) = abi.decode(data, (bytes, bytes32)); + revealedURI = string(encryptDecrypt(encryptedURI, _key)); + + if (keccak256(abi.encodePacked(revealedURI, _key, block.chainid)) != provenanceHash) { + revert DelayedRevealIncorrectResultHash( + provenanceHash, + keccak256(abi.encodePacked(revealedURI, _key, block.chainid)) + ); + } } - /// @dev See: https://ethereum.stackexchange.com/questions/69825/decrypt-message-on-chain + /** + * @notice Encrypt/decrypt data on chain. + * @dev Encrypt/decrypt given `data` with `key`. Uses inline assembly. + * See: https://ethereum.stackexchange.com/questions/69825/decrypt-message-on-chain + * + * @param data Bytes of data to encrypt/decrypt. + * @param key Secure key used by caller for encryption/decryption. + * + * @return result Output after encryption/decryption of given data. + */ function encryptDecrypt(bytes memory data, bytes calldata key) public pure override returns (bytes memory result) { // Store data length on stack for later use uint256 length = data.length; @@ -63,8 +100,12 @@ abstract contract DelayedReveal is IDelayedReveal { } } - /// @dev Returns whether the relvant batch of NFTs is subject to a delayed reveal. + /** + * @notice Returns whether the relvant batch of NFTs is subject to a delayed reveal. + * @dev Returns `true` if `_batchId`'s base URI is encrypted. + * @param _batchId ID of a batch of NFTs. + */ function isEncryptedBatch(uint256 _batchId) public view returns (bool) { - return encryptedBaseURI[_batchId].length > 0; + return encryptedData[_batchId].length > 0; } } diff --git a/contracts/extension/Drop.sol b/contracts/extension/Drop.sol index 2c0b031c8..5a1ee1a62 100644 --- a/contracts/extension/Drop.sol +++ b/contracts/extension/Drop.sol @@ -1,12 +1,37 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + import "./interface/IDrop.sol"; import "../lib/MerkleProof.sol"; -import "../lib/TWBitMaps.sol"; abstract contract Drop is IDrop { - using TWBitMaps for TWBitMaps.BitMap; + /// @dev The sender is not authorized to perform the action + error DropUnauthorized(); + + /// @dev Exceeded the max token total supply + error DropExceedMaxSupply(); + + /// @dev No active claim condition + error DropNoActiveCondition(); + + /// @dev Claim condition invalid currency or price + error DropClaimInvalidTokenPrice( + address expectedCurrency, + uint256 expectedPricePerToken, + address actualCurrency, + uint256 actualExpectedPricePerToken + ); + + /// @dev Claim condition exceeded limit + error DropClaimExceedLimit(uint256 expected, uint256 actual); + + /// @dev Claim condition exceeded max supply + error DropClaimExceedMaxSupply(uint256 expected, uint256 actual); + + /// @dev Claim condition not started yet + error DropClaimNotStarted(uint256 expected, uint256 actual); /*/////////////////////////////////////////////////////////////// State variables @@ -31,55 +56,18 @@ abstract contract Drop is IDrop { _beforeClaim(_receiver, _quantity, _currency, _pricePerToken, _allowlistProof, _data); uint256 activeConditionId = getActiveClaimConditionId(); - ClaimCondition memory currentClaimPhase = claimCondition.conditions[activeConditionId]; - - /** - * We make allowlist checks (i.e. verifyClaimMerkleProof) before verifying the claim's general - * validity (i.e. verifyClaim) because we give precedence to the check of allow list quantity - * restriction over the check of the general claim condition's quantityLimitPerTransaction - * restriction. - */ - // Verify inclusion in allowlist. - (bool validMerkleProof, uint256 merkleProofIndex) = verifyClaimMerkleProof( - activeConditionId, - _dropMsgSender(), - _quantity, - _allowlistProof - ); - - // Verify claim validity. If not valid, revert. - // when there's allowlist present --> verifyClaimMerkleProof will verify the maxQuantityInAllowlist value with hashed leaf in the allowlist - // when there's no allowlist, this check is true --> verifyClaim will check for _quantity being equal/less than the limit - bool toVerifyMaxQuantityPerTransaction = _allowlistProof.maxQuantityInAllowlist == 0 || - currentClaimPhase.merkleRoot == bytes32(0); - - verifyClaim( - activeConditionId, - _dropMsgSender(), - _quantity, - _currency, - _pricePerToken, - toVerifyMaxQuantityPerTransaction - ); - - if (validMerkleProof && _allowlistProof.maxQuantityInAllowlist > 0) { - /** - * Mark the claimer's use of their position in the allowlist. A spot in an allowlist - * can be used only once. - */ - claimCondition.usedAllowlistSpot[activeConditionId].set(merkleProofIndex); - } + verifyClaim(activeConditionId, _dropMsgSender(), _quantity, _currency, _pricePerToken, _allowlistProof); // Update contract state. claimCondition.conditions[activeConditionId].supplyClaimed += _quantity; - claimCondition.lastClaimTimestamp[activeConditionId][_dropMsgSender()] = block.timestamp; + claimCondition.supplyClaimedByWallet[activeConditionId][_dropMsgSender()] += _quantity; // If there's a price, collect price. - collectPriceOnClaim(_quantity, _currency, _pricePerToken); + _collectPriceOnClaim(address(0), _quantity, _currency, _pricePerToken); - // Mint the relevant NFTs to claimer. - uint256 startTokenId = transferTokensOnClaim(_receiver, _quantity); + // Mint the relevant tokens to claimer. + uint256 startTokenId = _transferTokensOnClaim(_receiver, _quantity); emit TokensClaimed(activeConditionId, _dropMsgSender(), _receiver, startTokenId, _quantity); @@ -87,25 +75,23 @@ abstract contract Drop is IDrop { } /// @dev Lets a contract admin set claim conditions. - function setClaimConditions(ClaimCondition[] calldata _conditions, bool _resetClaimEligibility) - external - virtual - override - { + function setClaimConditions( + ClaimCondition[] calldata _conditions, + bool _resetClaimEligibility + ) external virtual override { if (!_canSetClaimConditions()) { - revert Drop__NotAuthorized(); + revert DropUnauthorized(); } uint256 existingStartIndex = claimCondition.currentStartId; uint256 existingPhaseCount = claimCondition.count; /** - * `lastClaimTimestamp` and `usedAllowListSpot` are mappings that use a - * claim condition's UID as a key. + * The mapping `supplyClaimedByWallet` uses a claim condition's UID as a key. * * If `_resetClaimEligibility == true`, we assign completely new UIDs to the claim * conditions in `_conditions`, effectively resetting the restrictions on claims expressed - * by `lastClaimTimestamp` and `usedAllowListSpot`. + * by `supplyClaimedByWallet`. */ uint256 newStartIndex = existingStartIndex; if (_resetClaimEligibility) { @@ -121,7 +107,7 @@ abstract contract Drop is IDrop { uint256 supplyClaimedAlready = claimCondition.conditions[newStartIndex + i].supplyClaimed; if (supplyClaimedAlready > _conditions[i].maxClaimableSupply) { - revert Drop__MaxSupplyClaimedAlready(supplyClaimedAlready); + revert DropExceedMaxSupply(); } claimCondition.conditions[newStartIndex + i] = _conditions[i]; @@ -143,13 +129,11 @@ abstract contract Drop is IDrop { if (_resetClaimEligibility) { for (uint256 i = existingStartIndex; i < newStartIndex; i++) { delete claimCondition.conditions[i]; - delete claimCondition.usedAllowlistSpot[i]; } } else { if (existingPhaseCount > _conditions.length) { for (uint256 i = _conditions.length; i < existingPhaseCount; i++) { delete claimCondition.conditions[newStartIndex + i]; - delete claimCondition.usedAllowlistSpot[newStartIndex + i]; } } } @@ -164,73 +148,63 @@ abstract contract Drop is IDrop { uint256 _quantity, address _currency, uint256 _pricePerToken, - bool verifyMaxQuantityPerTransaction - ) public view { + AllowlistProof calldata _allowlistProof + ) public view virtual returns (bool isOverride) { ClaimCondition memory currentClaimPhase = claimCondition.conditions[_conditionId]; + uint256 claimLimit = currentClaimPhase.quantityLimitPerWallet; + uint256 claimPrice = currentClaimPhase.pricePerToken; + address claimCurrency = currentClaimPhase.currency; - if (_currency != currentClaimPhase.currency || _pricePerToken != currentClaimPhase.pricePerToken) { - revert Drop__InvalidCurrencyOrPrice( - _currency, - currentClaimPhase.currency, - _pricePerToken, - currentClaimPhase.pricePerToken + /* + * Here `isOverride` implies that if the merkle proof verification fails, + * the claimer would claim through open claim limit instead of allowlisted limit. + */ + if (currentClaimPhase.merkleRoot != bytes32(0)) { + (isOverride, ) = MerkleProof.verify( + _allowlistProof.proof, + currentClaimPhase.merkleRoot, + keccak256( + abi.encodePacked( + _claimer, + _allowlistProof.quantityLimitPerWallet, + _allowlistProof.pricePerToken, + _allowlistProof.currency + ) + ) ); } - // If we're checking for an allowlist quantity restriction, ignore the general quantity restriction. - if ( - _quantity == 0 || - (verifyMaxQuantityPerTransaction && _quantity > currentClaimPhase.quantityLimitPerTransaction) - ) { - revert Drop__InvalidQuantity(); - } - if (currentClaimPhase.supplyClaimed + _quantity > currentClaimPhase.maxClaimableSupply) { - revert Drop__ExceedMaxClaimableSupply( - currentClaimPhase.supplyClaimed, - currentClaimPhase.maxClaimableSupply - ); + if (isOverride) { + claimLimit = _allowlistProof.quantityLimitPerWallet != 0 + ? _allowlistProof.quantityLimitPerWallet + : claimLimit; + claimPrice = _allowlistProof.pricePerToken != type(uint256).max + ? _allowlistProof.pricePerToken + : claimPrice; + claimCurrency = _allowlistProof.pricePerToken != type(uint256).max && _allowlistProof.currency != address(0) + ? _allowlistProof.currency + : claimCurrency; } - (uint256 lastClaimedAt, uint256 nextValidClaimTimestamp) = getClaimTimestamp(_conditionId, _claimer); - if ( - currentClaimPhase.startTimestamp > block.timestamp || - (lastClaimedAt != 0 && block.timestamp < nextValidClaimTimestamp) - ) { - revert Drop__CannotClaimYet( - block.timestamp, - currentClaimPhase.startTimestamp, - lastClaimedAt, - nextValidClaimTimestamp - ); + uint256 supplyClaimedByWallet = claimCondition.supplyClaimedByWallet[_conditionId][_claimer]; + + if (_currency != claimCurrency || _pricePerToken != claimPrice) { + revert DropClaimInvalidTokenPrice(_currency, _pricePerToken, claimCurrency, claimPrice); } - } - /// @dev Checks whether a claimer meets the claim condition's allowlist criteria. - function verifyClaimMerkleProof( - uint256 _conditionId, - address _claimer, - uint256 _quantity, - AllowlistProof calldata _allowlistProof - ) public view returns (bool validMerkleProof, uint256 merkleProofIndex) { - ClaimCondition memory currentClaimPhase = claimCondition.conditions[_conditionId]; + if (_quantity == 0 || (_quantity + supplyClaimedByWallet > claimLimit)) { + revert DropClaimExceedLimit(claimLimit, _quantity + supplyClaimedByWallet); + } - if (currentClaimPhase.merkleRoot != bytes32(0)) { - (validMerkleProof, merkleProofIndex) = MerkleProof.verify( - _allowlistProof.proof, - currentClaimPhase.merkleRoot, - keccak256(abi.encodePacked(_claimer, _allowlistProof.maxQuantityInAllowlist)) + if (currentClaimPhase.supplyClaimed + _quantity > currentClaimPhase.maxClaimableSupply) { + revert DropClaimExceedMaxSupply( + currentClaimPhase.maxClaimableSupply, + currentClaimPhase.supplyClaimed + _quantity ); - if (!validMerkleProof) { - revert Drop__NotInWhitelist(); - } - - if (claimCondition.usedAllowlistSpot[_conditionId].get(merkleProofIndex)) { - revert Drop__ProofClaimed(); - } + } - if (_allowlistProof.maxQuantityInAllowlist != 0 && _quantity > _allowlistProof.maxQuantityInAllowlist) { - revert Drop__InvalidQuantityProof(_allowlistProof.maxQuantityInAllowlist); - } + if (currentClaimPhase.startTimestamp > block.timestamp) { + revert DropClaimNotStarted(currentClaimPhase.startTimestamp, block.timestamp); } } @@ -242,7 +216,7 @@ abstract contract Drop is IDrop { } } - revert("!CONDITION."); + revert DropNoActiveCondition(); } /// @dev Returns the claim condition at the given uid. @@ -250,23 +224,12 @@ abstract contract Drop is IDrop { condition = claimCondition.conditions[_conditionId]; } - /// @dev Returns the timestamp for when a claimer is eligible for claiming NFTs again. - function getClaimTimestamp(uint256 _conditionId, address _claimer) - public - view - returns (uint256 lastClaimTimestamp, uint256 nextValidClaimTimestamp) - { - lastClaimTimestamp = claimCondition.lastClaimTimestamp[_conditionId][_claimer]; - - unchecked { - nextValidClaimTimestamp = - lastClaimTimestamp + - claimCondition.conditions[_conditionId].waitTimeInSecondsBetweenClaims; - - if (nextValidClaimTimestamp < lastClaimTimestamp) { - nextValidClaimTimestamp = type(uint256).max; - } - } + /// @dev Returns the supply claimed by claimer for a given conditionId. + function getSupplyClaimedByWallet( + uint256 _conditionId, + address _claimer + ) public view returns (uint256 supplyClaimedByWallet) { + supplyClaimedByWallet = claimCondition.supplyClaimedByWallet[_conditionId][_claimer]; } /*//////////////////////////////////////////////////////////////////// @@ -303,18 +266,19 @@ abstract contract Drop is IDrop { //////////////////////////////////////////////////////////////*/ /// @dev Collects and distributes the primary sale value of NFTs being claimed. - function collectPriceOnClaim( + function _collectPriceOnClaim( + address _primarySaleRecipient, uint256 _quantityToClaim, address _currency, uint256 _pricePerToken ) internal virtual; /// @dev Transfers the NFTs being claimed. - function transferTokensOnClaim(address _to, uint256 _quantityBeingClaimed) - internal - virtual - returns (uint256 startTokenId); + function _transferTokensOnClaim( + address _to, + uint256 _quantityBeingClaimed + ) internal virtual returns (uint256 startTokenId); /// @dev Determine what wallet can update claim conditions - function _canSetClaimConditions() internal virtual returns (bool); + function _canSetClaimConditions() internal view virtual returns (bool); } diff --git a/contracts/extension/Drop1155.sol b/contracts/extension/Drop1155.sol new file mode 100644 index 000000000..07631e32d --- /dev/null +++ b/contracts/extension/Drop1155.sol @@ -0,0 +1,300 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./interface/IDrop1155.sol"; +import "../lib/MerkleProof.sol"; + +abstract contract Drop1155 is IDrop1155 { + /// @dev The sender is not authorized to perform the action + error DropUnauthorized(); + + /// @dev Exceeded the max token total supply + error DropExceedMaxSupply(); + + /// @dev No active claim condition + error DropNoActiveCondition(); + + /// @dev Claim condition invalid currency or price + error DropClaimInvalidTokenPrice( + address expectedCurrency, + uint256 expectedPricePerToken, + address actualCurrency, + uint256 actualExpectedPricePerToken + ); + + /// @dev Claim condition exceeded limit + error DropClaimExceedLimit(uint256 expected, uint256 actual); + + /// @dev Claim condition exceeded max supply + error DropClaimExceedMaxSupply(uint256 expected, uint256 actual); + + /// @dev Claim condition not started yet + error DropClaimNotStarted(uint256 expected, uint256 actual); + + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + /// @dev Mapping from token ID => the set of all claim conditions, at any given moment, for tokens of the token ID. + mapping(uint256 => ClaimConditionList) public claimCondition; + + /*/////////////////////////////////////////////////////////////// + Drop logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Lets an account claim tokens. + function claim( + address _receiver, + uint256 _tokenId, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof, + bytes memory _data + ) public payable virtual override { + _beforeClaim(_tokenId, _receiver, _quantity, _currency, _pricePerToken, _allowlistProof, _data); + + uint256 activeConditionId = getActiveClaimConditionId(_tokenId); + + verifyClaim( + activeConditionId, + _dropMsgSender(), + _tokenId, + _quantity, + _currency, + _pricePerToken, + _allowlistProof + ); + + // Update contract state. + claimCondition[_tokenId].conditions[activeConditionId].supplyClaimed += _quantity; + claimCondition[_tokenId].supplyClaimedByWallet[activeConditionId][_dropMsgSender()] += _quantity; + + // If there's a price, collect price. + collectPriceOnClaim(_tokenId, address(0), _quantity, _currency, _pricePerToken); + + // Mint the relevant NFTs to claimer. + transferTokensOnClaim(_receiver, _tokenId, _quantity); + + emit TokensClaimed(activeConditionId, _dropMsgSender(), _receiver, _tokenId, _quantity); + + _afterClaim(_tokenId, _receiver, _quantity, _currency, _pricePerToken, _allowlistProof, _data); + } + + /// @dev Lets a contract admin set claim conditions. + function setClaimConditions( + uint256 _tokenId, + ClaimCondition[] calldata _conditions, + bool _resetClaimEligibility + ) external virtual override { + if (!_canSetClaimConditions()) { + revert DropUnauthorized(); + } + ClaimConditionList storage conditionList = claimCondition[_tokenId]; + uint256 existingStartIndex = conditionList.currentStartId; + uint256 existingPhaseCount = conditionList.count; + + /** + * The mapping `supplyClaimedByWallet` uses a claim condition's UID as a key. + * + * If `_resetClaimEligibility == true`, we assign completely new UIDs to the claim + * conditions in `_conditions`, effectively resetting the restrictions on claims expressed + * by `supplyClaimedByWallet`. + */ + uint256 newStartIndex = existingStartIndex; + if (_resetClaimEligibility) { + newStartIndex = existingStartIndex + existingPhaseCount; + } + + conditionList.count = _conditions.length; + conditionList.currentStartId = newStartIndex; + + uint256 lastConditionStartTimestamp; + for (uint256 i = 0; i < _conditions.length; i++) { + require(i == 0 || lastConditionStartTimestamp < _conditions[i].startTimestamp, "ST"); + + uint256 supplyClaimedAlready = conditionList.conditions[newStartIndex + i].supplyClaimed; + if (supplyClaimedAlready > _conditions[i].maxClaimableSupply) { + revert DropExceedMaxSupply(); + } + + conditionList.conditions[newStartIndex + i] = _conditions[i]; + conditionList.conditions[newStartIndex + i].supplyClaimed = supplyClaimedAlready; + + lastConditionStartTimestamp = _conditions[i].startTimestamp; + } + + /** + * Gas refunds (as much as possible) + * + * If `_resetClaimEligibility == true`, we assign completely new UIDs to the claim + * conditions in `_conditions`. So, we delete claim conditions with UID < `newStartIndex`. + * + * If `_resetClaimEligibility == false`, and there are more existing claim conditions + * than in `_conditions`, we delete the existing claim conditions that don't get replaced + * by the conditions in `_conditions`. + */ + if (_resetClaimEligibility) { + for (uint256 i = existingStartIndex; i < newStartIndex; i++) { + delete conditionList.conditions[i]; + } + } else { + if (existingPhaseCount > _conditions.length) { + for (uint256 i = _conditions.length; i < existingPhaseCount; i++) { + delete conditionList.conditions[newStartIndex + i]; + } + } + } + + emit ClaimConditionsUpdated(_tokenId, _conditions, _resetClaimEligibility); + } + + /// @dev Checks a request to claim NFTs against the active claim condition's criteria. + function verifyClaim( + uint256 _conditionId, + address _claimer, + uint256 _tokenId, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof + ) public view virtual returns (bool isOverride) { + ClaimCondition memory currentClaimPhase = claimCondition[_tokenId].conditions[_conditionId]; + uint256 claimLimit = currentClaimPhase.quantityLimitPerWallet; + uint256 claimPrice = currentClaimPhase.pricePerToken; + address claimCurrency = currentClaimPhase.currency; + + /* + * Here `isOverride` implies that if the merkle proof verification fails, + * the claimer would claim through open claim limit instead of allowlisted limit. + */ + if (currentClaimPhase.merkleRoot != bytes32(0)) { + (isOverride, ) = MerkleProof.verify( + _allowlistProof.proof, + currentClaimPhase.merkleRoot, + keccak256( + abi.encodePacked( + _claimer, + _allowlistProof.quantityLimitPerWallet, + _allowlistProof.pricePerToken, + _allowlistProof.currency + ) + ) + ); + } + + if (isOverride) { + claimLimit = _allowlistProof.quantityLimitPerWallet != 0 + ? _allowlistProof.quantityLimitPerWallet + : claimLimit; + claimPrice = _allowlistProof.pricePerToken != type(uint256).max + ? _allowlistProof.pricePerToken + : claimPrice; + claimCurrency = _allowlistProof.pricePerToken != type(uint256).max && _allowlistProof.currency != address(0) + ? _allowlistProof.currency + : claimCurrency; + } + + uint256 supplyClaimedByWallet = claimCondition[_tokenId].supplyClaimedByWallet[_conditionId][_claimer]; + + if (_currency != claimCurrency || _pricePerToken != claimPrice) { + revert DropClaimInvalidTokenPrice(_currency, _pricePerToken, claimCurrency, claimPrice); + } + + if (_quantity == 0 || (_quantity + supplyClaimedByWallet > claimLimit)) { + revert DropClaimExceedLimit(claimLimit, _quantity + supplyClaimedByWallet); + } + + if (currentClaimPhase.supplyClaimed + _quantity > currentClaimPhase.maxClaimableSupply) { + revert DropClaimExceedMaxSupply( + currentClaimPhase.maxClaimableSupply, + currentClaimPhase.supplyClaimed + _quantity + ); + } + + if (currentClaimPhase.startTimestamp > block.timestamp) { + revert DropClaimNotStarted(currentClaimPhase.startTimestamp, block.timestamp); + } + } + + /// @dev At any given moment, returns the uid for the active claim condition. + function getActiveClaimConditionId(uint256 _tokenId) public view returns (uint256) { + ClaimConditionList storage conditionList = claimCondition[_tokenId]; + for (uint256 i = conditionList.currentStartId + conditionList.count; i > conditionList.currentStartId; i--) { + if (block.timestamp >= conditionList.conditions[i - 1].startTimestamp) { + return i - 1; + } + } + + revert DropNoActiveCondition(); + } + + /// @dev Returns the claim condition at the given uid. + function getClaimConditionById( + uint256 _tokenId, + uint256 _conditionId + ) external view returns (ClaimCondition memory condition) { + condition = claimCondition[_tokenId].conditions[_conditionId]; + } + + /// @dev Returns the supply claimed by claimer for a given conditionId. + function getSupplyClaimedByWallet( + uint256 _tokenId, + uint256 _conditionId, + address _claimer + ) public view returns (uint256 supplyClaimedByWallet) { + supplyClaimedByWallet = claimCondition[_tokenId].supplyClaimedByWallet[_conditionId][_claimer]; + } + + /*//////////////////////////////////////////////////////////////////// + Optional hooks that can be implemented in the derived contract + ///////////////////////////////////////////////////////////////////*/ + + /// @dev Exposes the ability to override the msg sender. + function _dropMsgSender() internal virtual returns (address) { + return msg.sender; + } + + /// @dev Runs before every `claim` function call. + function _beforeClaim( + uint256 _tokenId, + address _receiver, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof, + bytes memory _data + ) internal virtual {} + + /// @dev Runs after every `claim` function call. + function _afterClaim( + uint256 _tokenId, + address _receiver, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof, + bytes memory _data + ) internal virtual {} + + /*/////////////////////////////////////////////////////////////// + Virtual functions: to be implemented in derived contract + //////////////////////////////////////////////////////////////*/ + + /// @dev Collects and distributes the primary sale value of NFTs being claimed. + function collectPriceOnClaim( + uint256 _tokenId, + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal virtual; + + /// @dev Transfers the NFTs being claimed. + function transferTokensOnClaim(address _to, uint256 _tokenId, uint256 _quantityBeingClaimed) internal virtual; + + /// @dev Determine what wallet can update claim conditions + function _canSetClaimConditions() internal view virtual returns (bool); +} diff --git a/contracts/extension/DropSinglePhase.sol b/contracts/extension/DropSinglePhase.sol index ca4d1ad66..e7b7cf29a 100644 --- a/contracts/extension/DropSinglePhase.sol +++ b/contracts/extension/DropSinglePhase.sol @@ -1,12 +1,37 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + import "./interface/IDropSinglePhase.sol"; import "../lib/MerkleProof.sol"; -import "../lib/TWBitMaps.sol"; abstract contract DropSinglePhase is IDropSinglePhase { - using TWBitMaps for TWBitMaps.BitMap; + /// @dev The sender is not authorized to perform the action + error DropUnauthorized(); + + /// @dev Exceeded the max token total supply + error DropExceedMaxSupply(); + + /// @dev No active claim condition + error DropNoActiveCondition(); + + /// @dev Claim condition invalid currency or price + error DropClaimInvalidTokenPrice( + address expectedCurrency, + uint256 expectedPricePerToken, + address actualCurrency, + uint256 actualExpectedPricePerToken + ); + + /// @dev Claim condition exceeded limit + error DropClaimExceedLimit(uint256 expected, uint256 actual); + + /// @dev Claim condition exceeded max supply + error DropClaimExceedMaxSupply(uint256 expected, uint256 actual); + + /// @dev Claim condition not started yet + error DropClaimNotStarted(uint256 expected, uint256 actual); /*/////////////////////////////////////////////////////////////// State variables @@ -23,16 +48,9 @@ abstract contract DropSinglePhase is IDropSinglePhase { //////////////////////////////////////////////////////////////*/ /** - * @dev Map from an account and uid for a claim condition, to the last timestamp - * at which the account claimed tokens under that claim condition. - */ - mapping(bytes32 => mapping(address => uint256)) private lastClaimTimestamp; - - /** - * @dev Map from a claim condition uid to whether an address in an allowlist - * has already claimed tokens i.e. used their place in the allowlist. + * @dev Map from a claim condition uid and account to supply claimed by account. */ - mapping(bytes32 => TWBitMaps.BitMap) private usedAllowlistSpot; + mapping(bytes32 => mapping(address => uint256)) private supplyClaimedByWallet; /*/////////////////////////////////////////////////////////////// Drop logic @@ -51,45 +69,17 @@ abstract contract DropSinglePhase is IDropSinglePhase { bytes32 activeConditionId = conditionId; - /** - * We make allowlist checks (i.e. verifyClaimMerkleProof) before verifying the claim's general - * validity (i.e. verifyClaim) because we give precedence to the check of allow list quantity - * restriction over the check of the general claim condition's quantityLimitPerTransaction - * restriction. - */ - - // Verify inclusion in allowlist. - (bool validMerkleProof, uint256 merkleProofIndex) = verifyClaimMerkleProof( - _dropMsgSender(), - _quantity, - _allowlistProof - ); - - // Verify claim validity. If not valid, revert. - // when there's allowlist present --> verifyClaimMerkleProof will verify the maxQuantityInAllowlist value with hashed leaf in the allowlist - // when there's no allowlist, this check is true --> verifyClaim will check for _quantity being equal/less than the limit - bool toVerifyMaxQuantityPerTransaction = _allowlistProof.maxQuantityInAllowlist == 0 || - claimCondition.merkleRoot == bytes32(0); - - verifyClaim(_dropMsgSender(), _quantity, _currency, _pricePerToken, toVerifyMaxQuantityPerTransaction); - - if (validMerkleProof && _allowlistProof.maxQuantityInAllowlist > 0) { - /** - * Mark the claimer's use of their position in the allowlist. A spot in an allowlist - * can be used only once. - */ - usedAllowlistSpot[activeConditionId].set(merkleProofIndex); - } + verifyClaim(_dropMsgSender(), _quantity, _currency, _pricePerToken, _allowlistProof); // Update contract state. claimCondition.supplyClaimed += _quantity; - lastClaimTimestamp[activeConditionId][_dropMsgSender()] = block.timestamp; + supplyClaimedByWallet[activeConditionId][_dropMsgSender()] += _quantity; // If there's a price, collect price. - collectPriceOnClaim(_quantity, _currency, _pricePerToken); + _collectPriceOnClaim(address(0), _quantity, _currency, _pricePerToken); // Mint the relevant NFTs to claimer. - uint256 startTokenId = transferTokensOnClaim(_receiver, _quantity); + uint256 startTokenId = _transferTokensOnClaim(_receiver, _quantity); emit TokensClaimed(_dropMsgSender(), _receiver, startTokenId, _quantity); @@ -99,7 +89,7 @@ abstract contract DropSinglePhase is IDropSinglePhase { /// @dev Lets a contract admin set claim conditions. function setClaimConditions(ClaimCondition calldata _condition, bool _resetClaimEligibility) external override { if (!_canSetClaimConditions()) { - revert("Not authorized"); + revert DropUnauthorized(); } bytes32 targetConditionId = conditionId; @@ -111,18 +101,18 @@ abstract contract DropSinglePhase is IDropSinglePhase { } if (supplyClaimedAlready > _condition.maxClaimableSupply) { - revert("max supply claimed"); + revert DropExceedMaxSupply(); } claimCondition = ClaimCondition({ startTimestamp: _condition.startTimestamp, maxClaimableSupply: _condition.maxClaimableSupply, supplyClaimed: supplyClaimedAlready, - quantityLimitPerTransaction: _condition.quantityLimitPerTransaction, - waitTimeInSecondsBetweenClaims: _condition.waitTimeInSecondsBetweenClaims, + quantityLimitPerWallet: _condition.quantityLimitPerWallet, merkleRoot: _condition.merkleRoot, pricePerToken: _condition.pricePerToken, - currency: _condition.currency + currency: _condition.currency, + metadata: _condition.metadata }); conditionId = targetConditionId; @@ -135,78 +125,69 @@ abstract contract DropSinglePhase is IDropSinglePhase { uint256 _quantity, address _currency, uint256 _pricePerToken, - bool verifyMaxQuantityPerTransaction - ) public view { + AllowlistProof calldata _allowlistProof + ) public view virtual returns (bool isOverride) { ClaimCondition memory currentClaimPhase = claimCondition; + uint256 claimLimit = currentClaimPhase.quantityLimitPerWallet; + uint256 claimPrice = currentClaimPhase.pricePerToken; + address claimCurrency = currentClaimPhase.currency; - if (_currency != currentClaimPhase.currency || _pricePerToken != currentClaimPhase.pricePerToken) { - revert("Invalid price or currency"); + /* + * Here `isOverride` implies that if the merkle proof verification fails, + * the claimer would claim through open claim limit instead of allowlisted limit. + */ + if (currentClaimPhase.merkleRoot != bytes32(0)) { + (isOverride, ) = MerkleProof.verify( + _allowlistProof.proof, + currentClaimPhase.merkleRoot, + keccak256( + abi.encodePacked( + _claimer, + _allowlistProof.quantityLimitPerWallet, + _allowlistProof.pricePerToken, + _allowlistProof.currency + ) + ) + ); } - // If we're checking for an allowlist quantity restriction, ignore the general quantity restriction. - if ( - _quantity == 0 || - (verifyMaxQuantityPerTransaction && _quantity > currentClaimPhase.quantityLimitPerTransaction) - ) { - revert("Invalid quantity"); + if (isOverride) { + claimLimit = _allowlistProof.quantityLimitPerWallet != 0 + ? _allowlistProof.quantityLimitPerWallet + : claimLimit; + claimPrice = _allowlistProof.pricePerToken != type(uint256).max + ? _allowlistProof.pricePerToken + : claimPrice; + claimCurrency = _allowlistProof.pricePerToken != type(uint256).max && _allowlistProof.currency != address(0) + ? _allowlistProof.currency + : claimCurrency; } - if (currentClaimPhase.supplyClaimed + _quantity > currentClaimPhase.maxClaimableSupply) { - revert("exceeds max supply"); - } + uint256 _supplyClaimedByWallet = supplyClaimedByWallet[conditionId][_claimer]; - (uint256 lastClaimedAt, uint256 nextValidClaimTimestamp) = getClaimTimestamp(_claimer); - if ( - currentClaimPhase.startTimestamp > block.timestamp || - (lastClaimedAt != 0 && block.timestamp < nextValidClaimTimestamp) - ) { - revert("cant claim yet"); + if (_currency != claimCurrency || _pricePerToken != claimPrice) { + revert DropClaimInvalidTokenPrice(_currency, _pricePerToken, claimCurrency, claimPrice); } - } - /// @dev Checks whether a claimer meets the claim condition's allowlist criteria. - function verifyClaimMerkleProof( - address _claimer, - uint256 _quantity, - AllowlistProof calldata _allowlistProof - ) public view returns (bool validMerkleProof, uint256 merkleProofIndex) { - ClaimCondition memory currentClaimPhase = claimCondition; + if (_quantity == 0 || (_quantity + _supplyClaimedByWallet > claimLimit)) { + revert DropClaimExceedLimit(claimLimit, _quantity + _supplyClaimedByWallet); + } - if (currentClaimPhase.merkleRoot != bytes32(0)) { - (validMerkleProof, merkleProofIndex) = MerkleProof.verify( - _allowlistProof.proof, - currentClaimPhase.merkleRoot, - keccak256(abi.encodePacked(_claimer, _allowlistProof.maxQuantityInAllowlist)) + if (currentClaimPhase.supplyClaimed + _quantity > currentClaimPhase.maxClaimableSupply) { + revert DropClaimExceedMaxSupply( + currentClaimPhase.maxClaimableSupply, + currentClaimPhase.supplyClaimed + _quantity ); - if (!validMerkleProof) { - revert("not in allowlist"); - } - - if (usedAllowlistSpot[conditionId].get(merkleProofIndex)) { - revert("proof claimed"); - } + } - if (_allowlistProof.maxQuantityInAllowlist != 0 && _quantity > _allowlistProof.maxQuantityInAllowlist) { - revert("Invalid qty proof"); - } + if (currentClaimPhase.startTimestamp > block.timestamp) { + revert DropClaimNotStarted(currentClaimPhase.startTimestamp, block.timestamp); } } - /// @dev Returns the timestamp for when a claimer is eligible for claiming NFTs again. - function getClaimTimestamp(address _claimer) - public - view - returns (uint256 lastClaimedAt, uint256 nextValidClaimTimestamp) - { - lastClaimedAt = lastClaimTimestamp[conditionId][_claimer]; - - unchecked { - nextValidClaimTimestamp = lastClaimedAt + claimCondition.waitTimeInSecondsBetweenClaims; - - if (nextValidClaimTimestamp < lastClaimedAt) { - nextValidClaimTimestamp = type(uint256).max; - } - } + /// @dev Returns the supply claimed by claimer for active conditionId. + function getSupplyClaimedByWallet(address _claimer) public view returns (uint256) { + return supplyClaimedByWallet[conditionId][_claimer]; } /*//////////////////////////////////////////////////////////////////// @@ -239,17 +220,18 @@ abstract contract DropSinglePhase is IDropSinglePhase { ) internal virtual {} /// @dev Collects and distributes the primary sale value of NFTs being claimed. - function collectPriceOnClaim( + function _collectPriceOnClaim( + address _primarySaleRecipient, uint256 _quantityToClaim, address _currency, uint256 _pricePerToken ) internal virtual; /// @dev Transfers the NFTs being claimed. - function transferTokensOnClaim(address _to, uint256 _quantityBeingClaimed) - internal - virtual - returns (uint256 startTokenId); + function _transferTokensOnClaim( + address _to, + uint256 _quantityBeingClaimed + ) internal virtual returns (uint256 startTokenId); - function _canSetClaimConditions() internal virtual returns (bool); + function _canSetClaimConditions() internal view virtual returns (bool); } diff --git a/contracts/extension/DropSinglePhase1155.sol b/contracts/extension/DropSinglePhase1155.sol new file mode 100644 index 000000000..2950ed485 --- /dev/null +++ b/contracts/extension/DropSinglePhase1155.sol @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./interface/IDropSinglePhase1155.sol"; +import "../lib/MerkleProof.sol"; + +abstract contract DropSinglePhase1155 is IDropSinglePhase1155 { + /*/////////////////////////////////////////////////////////////// + Mappings + //////////////////////////////////////////////////////////////*/ + + /// @dev Mapping from tokenId => active claim condition for the tokenId. + mapping(uint256 => ClaimCondition) public claimCondition; + + /// @dev Mapping from tokenId => active claim condition's UID. + mapping(uint256 => bytes32) private conditionId; + + /** + * @dev Map from a claim condition uid and account to supply claimed by account. + */ + mapping(bytes32 => mapping(address => uint256)) private supplyClaimedByWallet; + + /*/////////////////////////////////////////////////////////////// + Drop logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Lets an account claim tokens. + function claim( + address _receiver, + uint256 _tokenId, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof, + bytes memory _data + ) public payable virtual override { + _beforeClaim(_tokenId, _receiver, _quantity, _currency, _pricePerToken, _allowlistProof, _data); + + ClaimCondition memory condition = claimCondition[_tokenId]; + bytes32 activeConditionId = conditionId[_tokenId]; + + verifyClaim(_tokenId, _dropMsgSender(), _quantity, _currency, _pricePerToken, _allowlistProof); + + // Update contract state. + condition.supplyClaimed += _quantity; + supplyClaimedByWallet[activeConditionId][_dropMsgSender()] += _quantity; + claimCondition[_tokenId] = condition; + + // If there's a price, collect price. + _collectPriceOnClaim(address(0), _quantity, _currency, _pricePerToken); + + // Mint the relevant NFTs to claimer. + _transferTokensOnClaim(_receiver, _tokenId, _quantity); + + emit TokensClaimed(_dropMsgSender(), _receiver, _tokenId, _quantity); + + _afterClaim(_tokenId, _receiver, _quantity, _currency, _pricePerToken, _allowlistProof, _data); + } + + /// @dev Lets a contract admin set claim conditions. + function setClaimConditions( + uint256 _tokenId, + ClaimCondition calldata _condition, + bool _resetClaimEligibility + ) external override { + if (!_canSetClaimConditions()) { + revert("Not authorized"); + } + + ClaimCondition memory condition = claimCondition[_tokenId]; + bytes32 targetConditionId = conditionId[_tokenId]; + + uint256 supplyClaimedAlready = condition.supplyClaimed; + + if (targetConditionId == bytes32(0) || _resetClaimEligibility) { + supplyClaimedAlready = 0; + targetConditionId = keccak256(abi.encodePacked(_dropMsgSender(), block.number, _tokenId)); + } + + if (supplyClaimedAlready > _condition.maxClaimableSupply) { + revert("max supply claimed"); + } + + ClaimCondition memory updatedCondition = ClaimCondition({ + startTimestamp: _condition.startTimestamp, + maxClaimableSupply: _condition.maxClaimableSupply, + supplyClaimed: supplyClaimedAlready, + quantityLimitPerWallet: _condition.quantityLimitPerWallet, + merkleRoot: _condition.merkleRoot, + pricePerToken: _condition.pricePerToken, + currency: _condition.currency, + metadata: _condition.metadata + }); + + claimCondition[_tokenId] = updatedCondition; + conditionId[_tokenId] = targetConditionId; + + emit ClaimConditionUpdated(_tokenId, _condition, _resetClaimEligibility); + } + + /// @dev Checks a request to claim NFTs against the active claim condition's criteria. + function verifyClaim( + uint256 _tokenId, + address _claimer, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof + ) public view virtual returns (bool isOverride) { + ClaimCondition memory currentClaimPhase = claimCondition[_tokenId]; + uint256 claimLimit = currentClaimPhase.quantityLimitPerWallet; + uint256 claimPrice = currentClaimPhase.pricePerToken; + address claimCurrency = currentClaimPhase.currency; + + /* + * Here `isOverride` implies that if the merkle proof verification fails, + * the claimer would claim through open claim limit instead of allowlisted limit. + */ + if (currentClaimPhase.merkleRoot != bytes32(0)) { + (isOverride, ) = MerkleProof.verify( + _allowlistProof.proof, + currentClaimPhase.merkleRoot, + keccak256( + abi.encodePacked( + _claimer, + _allowlistProof.quantityLimitPerWallet, + _allowlistProof.pricePerToken, + _allowlistProof.currency + ) + ) + ); + } + + if (isOverride) { + claimLimit = _allowlistProof.quantityLimitPerWallet != 0 + ? _allowlistProof.quantityLimitPerWallet + : claimLimit; + claimPrice = _allowlistProof.pricePerToken != type(uint256).max + ? _allowlistProof.pricePerToken + : claimPrice; + claimCurrency = _allowlistProof.pricePerToken != type(uint256).max && _allowlistProof.currency != address(0) + ? _allowlistProof.currency + : claimCurrency; + } + + uint256 _supplyClaimedByWallet = supplyClaimedByWallet[conditionId[_tokenId]][_claimer]; + + if (_currency != claimCurrency || _pricePerToken != claimPrice) { + revert("!PriceOrCurrency"); + } + + if (_quantity == 0 || (_quantity + _supplyClaimedByWallet > claimLimit)) { + revert("!Qty"); + } + + if (currentClaimPhase.supplyClaimed + _quantity > currentClaimPhase.maxClaimableSupply) { + revert("!MaxSupply"); + } + + if (currentClaimPhase.startTimestamp > block.timestamp) { + revert("cant claim yet"); + } + } + + /// @dev Returns the supply claimed by claimer for active conditionId. + function getSupplyClaimedByWallet(uint256 _tokenId, address _claimer) public view returns (uint256) { + return supplyClaimedByWallet[conditionId[_tokenId]][_claimer]; + } + + /*//////////////////////////////////////////////////////////////////// + Optional hooks that can be implemented in the derived contract + ///////////////////////////////////////////////////////////////////*/ + + /// @dev Exposes the ability to override the msg sender. + function _dropMsgSender() internal virtual returns (address) { + return msg.sender; + } + + /// @dev Runs before every `claim` function call. + function _beforeClaim( + uint256 _tokenId, + address _receiver, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof, + bytes memory _data + ) internal virtual {} + + /// @dev Runs after every `claim` function call. + function _afterClaim( + uint256 _tokenId, + address _receiver, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof, + bytes memory _data + ) internal virtual {} + + /// @dev Collects and distributes the primary sale value of NFTs being claimed. + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal virtual; + + /// @dev Transfers the NFTs being claimed. + function _transferTokensOnClaim(address _to, uint256 _tokenId, uint256 _quantityBeingClaimed) internal virtual; + + function _canSetClaimConditions() internal view virtual returns (bool); +} diff --git a/contracts/extension/Initializable.sol b/contracts/extension/Initializable.sol new file mode 100644 index 000000000..b91ab4621 --- /dev/null +++ b/contracts/extension/Initializable.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.0; + +import "../lib/Address.sol"; + +/** + * @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed + * behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an + * external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer + * function so it can only be called once. The {initializer} modifier provided by this contract will have this effect. + * + * The initialization functions use a version number. Once a version number is used, it is consumed and cannot be + * reused. This mechanism prevents re-execution of each "step" but allows the creation of new initialization steps in + * case an upgrade adds a module that needs to be initialized. + * + * For example: + * + * [.hljs-theme-light.nopadding] + * ``` + * contract MyToken is ERC20Upgradeable { + * function initialize() initializer public { + * __ERC20_init("MyToken", "MTK"); + * } + * } + * contract MyTokenV2 is MyToken, ERC20PermitUpgradeable { + * function initializeV2() reinitializer(2) public { + * __ERC20Permit_init("MyToken"); + * } + * } + * ``` + * + * TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as + * possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}. + * + * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure + * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity. + * + * [CAUTION] + * ==== + * Avoid leaving a contract uninitialized. + * + * An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation + * contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke + * the {_disableInitializers} function in the constructor to automatically lock it when it is deployed: + * + * [.hljs-theme-light.nopadding] + * ``` + * /// @custom:oz-upgrades-unsafe-allow constructor + * constructor() { + * _disableInitializers(); + * } + * ``` + * ==== + */ +abstract contract Initializable { + /** + * @dev Indicates that the contract has been initialized. + * @custom:oz-retyped-from bool + */ + uint8 private _initialized; + + /** + * @dev Indicates that the contract is in the process of being initialized. + */ + bool private _initializing; + + /** + * @dev Triggered when the contract has been initialized or reinitialized. + */ + event Initialized(uint8 version); + + /** + * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope, + * `onlyInitializing` functions can be used to initialize parent contracts. Equivalent to `reinitializer(1)`. + */ + modifier initializer() { + bool isTopLevelCall = !_initializing; + require( + (isTopLevelCall && _initialized < 1) || (!Address.isContract(address(this)) && _initialized == 1), + "Initializable: contract is already initialized" + ); + _initialized = 1; + if (isTopLevelCall) { + _initializing = true; + } + _; + if (isTopLevelCall) { + _initializing = false; + emit Initialized(1); + } + } + + /** + * @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the + * contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be + * used to initialize parent contracts. + * + * `initializer` is equivalent to `reinitializer(1)`, so a reinitializer may be used after the original + * initialization step. This is essential to configure modules that are added through upgrades and that require + * initialization. + * + * Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in + * a contract, executing them in the right order is up to the developer or operator. + */ + modifier reinitializer(uint8 version) { + require(!_initializing && _initialized < version, "Initializable: contract is already initialized"); + _initialized = version; + _initializing = true; + _; + _initializing = false; + emit Initialized(version); + } + + /** + * @dev Modifier to protect an initialization function so that it can only be invoked by functions with the + * {initializer} and {reinitializer} modifiers, directly or indirectly. + */ + modifier onlyInitializing() { + require(_initializing, "Initializable: contract is not initializing"); + _; + } + + /** + * @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. + * Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized + * to any version. It is recommended to use this to lock implementation contracts that are designed to be called + * through proxies. + */ + function _disableInitializers() internal virtual { + require(!_initializing, "Initializable: contract is initializing"); + if (_initialized < type(uint8).max) { + _initialized = type(uint8).max; + emit Initialized(type(uint8).max); + } + } +} diff --git a/contracts/extension/LazyMint.sol b/contracts/extension/LazyMint.sol index 401132194..ee59abc17 100644 --- a/contracts/extension/LazyMint.sol +++ b/contracts/extension/LazyMint.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + import "./interface/ILazyMint.sol"; import "./BatchMintMetadata.sol"; @@ -11,6 +13,10 @@ import "./BatchMintMetadata.sol"; */ abstract contract LazyMint is ILazyMint, BatchMintMetadata { + /// @dev The sender is not authorized to perform the action + error LazyMintUnauthorized(); + error LazyMintInvalidAmount(); + /// @notice The tokenId assigned to the next new NFT to be lazy minted. uint256 internal nextTokenIdToLazyMint; @@ -29,11 +35,11 @@ abstract contract LazyMint is ILazyMint, BatchMintMetadata { bytes calldata _data ) public virtual override returns (uint256 batchId) { if (!_canLazyMint()) { - revert("Not authorized"); + revert LazyMintUnauthorized(); } if (_amount == 0) { - revert("Minting 0 tokens"); + revert LazyMintInvalidAmount(); } uint256 startId = nextTokenIdToLazyMint; diff --git a/contracts/extension/LazyMintWithTier.sol b/contracts/extension/LazyMintWithTier.sol new file mode 100644 index 000000000..50758b55b --- /dev/null +++ b/contracts/extension/LazyMintWithTier.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./interface/ILazyMintWithTier.sol"; +import "../extension/BatchMintMetadata.sol"; + +/** + * The `LazyMint` is a contract extension for any base NFT contract. It lets you 'lazy mint' any number of NFTs + * at once. Here, 'lazy mint' means defining the metadata for particular tokenIds of your NFT contract, without actually + * minting a non-zero balance of NFTs of those tokenIds. + */ + +abstract contract LazyMintWithTier is ILazyMintWithTier, BatchMintMetadata { + struct TokenRange { + uint256 startIdInclusive; + uint256 endIdNonInclusive; + } + + struct TierMetadata { + string tier; + TokenRange[] ranges; + string[] baseURIs; + } + + /// @notice The tokenId assigned to the next new NFT to be lazy minted. + uint256 internal nextTokenIdToLazyMint; + + /// @notice Mapping from a tier -> the token IDs grouped under that tier. + mapping(string => TokenRange[]) internal tokensInTier; + + /// @notice A list of tiers used in this contract. + string[] private tiers; + + /** + * @notice Lets an authorized address lazy mint a given amount of NFTs. + * + * @param _amount The number of NFTs to lazy mint. + * @param _baseURIForTokens The base URI for the 'n' number of NFTs being lazy minted, where the metadata for each + * of those NFTs is `${baseURIForTokens}/${tokenId}`. + * @param _data Additional bytes data to be used at the discretion of the consumer of the contract. + * @return batchId A unique integer identifier for the batch of NFTs lazy minted together. + */ + function lazyMint( + uint256 _amount, + string calldata _baseURIForTokens, + string calldata _tier, + bytes calldata _data + ) public virtual override returns (uint256 batchId) { + if (!_canLazyMint()) { + revert("Not authorized"); + } + + if (_amount == 0) { + revert("0 amt"); + } + + uint256 startId = nextTokenIdToLazyMint; + + (nextTokenIdToLazyMint, batchId) = _batchMintMetadata(startId, _amount, _baseURIForTokens); + + // Handle tier info. + if (!(tokensInTier[_tier].length > 0)) { + tiers.push(_tier); + } + tokensInTier[_tier].push(TokenRange(startId, batchId)); + + emit TokensLazyMinted(_tier, startId, startId + _amount - 1, _baseURIForTokens, _data); + + return batchId; + } + + /// @notice Returns all metadata lazy minted for the given tier. + function _getMetadataInTier( + string memory _tier + ) private view returns (TokenRange[] memory tokens, string[] memory baseURIs) { + tokens = tokensInTier[_tier]; + + uint256 len = tokens.length; + baseURIs = new string[](len); + + for (uint256 i = 0; i < len; i += 1) { + baseURIs[i] = _getBaseURI(tokens[i].startIdInclusive); + } + } + + /// @notice Returns all metadata for all tiers created on the contract. + function getMetadataForAllTiers() external view returns (TierMetadata[] memory metadataForAllTiers) { + string[] memory allTiers = tiers; + uint256 len = allTiers.length; + + metadataForAllTiers = new TierMetadata[](len); + + for (uint256 i = 0; i < len; i += 1) { + (TokenRange[] memory tokens, string[] memory baseURIs) = _getMetadataInTier(allTiers[i]); + metadataForAllTiers[i] = TierMetadata(allTiers[i], tokens, baseURIs); + } + } + + /** + * @notice Returns whether any metadata is lazy minted for the given tier. + * + * @param _tier We check whether this given tier is empty. + */ + function isTierEmpty(string memory _tier) internal view returns (bool) { + return tokensInTier[_tier].length == 0; + } + + /// @dev Returns whether lazy minting can be performed in the given execution context. + function _canLazyMint() internal view virtual returns (bool); +} diff --git a/contracts/extension/Multicall.sol b/contracts/extension/Multicall.sol index ec6ec5b67..043d6c3c0 100644 --- a/contracts/extension/Multicall.sol +++ b/contracts/extension/Multicall.sol @@ -1,9 +1,9 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.5.0) (utils/Multicall.sol) - +// SPDX-License-Identifier: Apache 2.0 pragma solidity ^0.8.0; -import "../lib/TWAddress.sol"; +/// @author thirdweb + +import "../lib/Address.sol"; import "./interface/IMulticall.sol"; /** @@ -13,13 +13,28 @@ import "./interface/IMulticall.sol"; */ contract Multicall is IMulticall { /** - * @dev Receives and executes a batch of function calls on this contract. + * @notice Receives and executes a batch of function calls on this contract. + * @dev Receives and executes a batch of function calls on this contract. + * + * @param data The bytes data that makes up the batch of function calls to execute. + * @return results The bytes data that makes up the result of the batch of function calls executed. */ - function multicall(bytes[] calldata data) external virtual override returns (bytes[] memory results) { + function multicall(bytes[] calldata data) external returns (bytes[] memory results) { results = new bytes[](data.length); + address sender = _msgSender(); + bool isForwarder = msg.sender != sender; for (uint256 i = 0; i < data.length; i++) { - results[i] = TWAddress.functionDelegateCall(address(this), data[i]); + if (isForwarder) { + results[i] = Address.functionDelegateCall(address(this), abi.encodePacked(data[i], sender)); + } else { + results[i] = Address.functionDelegateCall(address(this), data[i]); + } } return results; } + + /// @notice Returns the sender in the given execution context. + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } } diff --git a/contracts/extension/NFTMetadata.sol b/contracts/extension/NFTMetadata.sol new file mode 100644 index 000000000..3a0ded1e1 --- /dev/null +++ b/contracts/extension/NFTMetadata.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "./interface/INFTMetadata.sol"; + +abstract contract NFTMetadata is INFTMetadata { + /// @dev The sender is not authorized to perform the action + error NFTMetadataUnauthorized(); + + /// @dev Invalid token metadata url + error NFTMetadataInvalidUrl(); + + /// @dev the nft metadata is frozen + error NFTMetadataFrozen(uint256 tokenId); + + bool public uriFrozen; + + mapping(uint256 => string) internal _tokenURI; + + /// @notice Returns the metadata URI for a given NFT. + function _getTokenURI(uint256 _tokenId) internal view virtual returns (string memory) { + return _tokenURI[_tokenId]; + } + + /// @notice Sets the metadata URI for a given NFT. + function _setTokenURI(uint256 _tokenId, string memory _uri) internal virtual { + if (bytes(_uri).length == 0) { + revert NFTMetadataInvalidUrl(); + } + _tokenURI[_tokenId] = _uri; + + emit MetadataUpdate(_tokenId); + } + + /// @notice Sets the metadata URI for a given NFT. + function setTokenURI(uint256 _tokenId, string memory _uri) public virtual { + if (!_canSetMetadata()) { + revert NFTMetadataUnauthorized(); + } + if (uriFrozen) { + revert NFTMetadataFrozen(_tokenId); + } + _setTokenURI(_tokenId, _uri); + } + + function freezeMetadata() public virtual { + if (!_canFreezeMetadata()) { + revert NFTMetadataUnauthorized(); + } + uriFrozen = true; + emit MetadataFrozen(); + } + + /// @dev Returns whether metadata can be set in the given execution context. + function _canSetMetadata() internal view virtual returns (bool); + + function _canFreezeMetadata() internal view virtual returns (bool); +} diff --git a/contracts/extension/OperatorFilterToggle.sol b/contracts/extension/OperatorFilterToggle.sol new file mode 100644 index 000000000..7094026e8 --- /dev/null +++ b/contracts/extension/OperatorFilterToggle.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./interface/IOperatorFilterToggle.sol"; + +abstract contract OperatorFilterToggle is IOperatorFilterToggle { + bool public operatorRestriction; + + function setOperatorRestriction(bool _restriction) external { + require(_canSetOperatorRestriction(), "Not authorized to set operator restriction."); + _setOperatorRestriction(_restriction); + } + + function _setOperatorRestriction(bool _restriction) internal { + operatorRestriction = _restriction; + emit OperatorRestriction(_restriction); + } + + function _canSetOperatorRestriction() internal virtual returns (bool); +} diff --git a/contracts/extension/OperatorFilterer.sol b/contracts/extension/OperatorFilterer.sol new file mode 100644 index 000000000..57b3554d2 --- /dev/null +++ b/contracts/extension/OperatorFilterer.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./interface/IOperatorFilterRegistry.sol"; +import "./OperatorFilterToggle.sol"; + +/** + * @title OperatorFilterer + * @notice Abstract contract whose constructor automatically registers and optionally subscribes to or copies another + * registrant's entries in the OperatorFilterRegistry. + * @dev This smart contract is meant to be inherited by token contracts so they can use the following: + * - `onlyAllowedOperator` modifier for `transferFrom` and `safeTransferFrom` methods. + * - `onlyAllowedOperatorApproval` modifier for `approve` and `setApprovalForAll` methods. + */ + +abstract contract OperatorFilterer is OperatorFilterToggle { + error OperatorNotAllowed(address operator); + + IOperatorFilterRegistry public constant OPERATOR_FILTER_REGISTRY = + IOperatorFilterRegistry(0x000000000000AAeB6D7670E522A718067333cd4E); + + constructor(address subscriptionOrRegistrantToCopy, bool subscribe) { + // If an inheriting token contract is deployed to a network without the registry deployed, the modifier + // will not revert, but the contract will need to be registered with the registry once it is deployed in + // order for the modifier to filter addresses. + _register(subscriptionOrRegistrantToCopy, subscribe); + } + + modifier onlyAllowedOperator(address from) virtual { + // Allow spending tokens from addresses with balance + // Note that this still allows listings and marketplaces with escrow to transfer tokens if transferred + // from an EOA. + if (from != msg.sender) { + _checkFilterOperator(msg.sender); + } + _; + } + + modifier onlyAllowedOperatorApproval(address operator) virtual { + _checkFilterOperator(operator); + _; + } + + function _checkFilterOperator(address operator) internal view virtual { + // Check registry code length to facilitate testing in environments without a deployed registry. + if (operatorRestriction) { + if (address(OPERATOR_FILTER_REGISTRY).code.length > 0) { + if (!OPERATOR_FILTER_REGISTRY.isOperatorAllowed(address(this), operator)) { + revert OperatorNotAllowed(operator); + } + } + } + } + + function _register(address subscriptionOrRegistrantToCopy, bool subscribe) internal { + // Is the registry deployed? + if (address(OPERATOR_FILTER_REGISTRY).code.length > 0) { + // Is the subscription contract deployed? + if (address(subscriptionOrRegistrantToCopy).code.length > 0) { + // Do we want to subscribe? + if (subscribe) { + OPERATOR_FILTER_REGISTRY.registerAndSubscribe(address(this), subscriptionOrRegistrantToCopy); + } else { + OPERATOR_FILTER_REGISTRY.registerAndCopyEntries(address(this), subscriptionOrRegistrantToCopy); + } + } else { + OPERATOR_FILTER_REGISTRY.register(address(this)); + } + } + } +} diff --git a/contracts/extension/OperatorFiltererUpgradeable.sol b/contracts/extension/OperatorFiltererUpgradeable.sol new file mode 100644 index 000000000..a656d46ce --- /dev/null +++ b/contracts/extension/OperatorFiltererUpgradeable.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./interface/IOperatorFilterRegistry.sol"; +import "./OperatorFilterToggle.sol"; + +abstract contract OperatorFiltererUpgradeable is OperatorFilterToggle { + error OperatorNotAllowed(address operator); + + IOperatorFilterRegistry constant OPERATOR_FILTER_REGISTRY = + IOperatorFilterRegistry(0x000000000000AAeB6D7670E522A718067333cd4E); + + function __OperatorFilterer_init(address subscriptionOrRegistrantToCopy, bool subscribe) internal { + // If an inheriting token contract is deployed to a network without the registry deployed, the modifier + // will not revert, but the contract will need to be registered with the registry once it is deployed in + // order for the modifier to filter addresses. + _register(subscriptionOrRegistrantToCopy, subscribe); + } + + modifier onlyAllowedOperator(address from) virtual { + // Check registry code length to facilitate testing in environments without a deployed registry. + if (operatorRestriction) { + if (address(OPERATOR_FILTER_REGISTRY).code.length > 0) { + // Allow spending tokens from addresses with balance + // Note that this still allows listings and marketplaces with escrow to transfer tokens if transferred + // from an EOA. + if (from == msg.sender) { + _; + return; + } + if (!OPERATOR_FILTER_REGISTRY.isOperatorAllowed(address(this), msg.sender)) { + revert OperatorNotAllowed(msg.sender); + } + } + } + _; + } + + modifier onlyAllowedOperatorApproval(address operator) virtual { + // Check registry code length to facilitate testing in environments without a deployed registry. + if (operatorRestriction) { + if (address(OPERATOR_FILTER_REGISTRY).code.length > 0) { + if (!OPERATOR_FILTER_REGISTRY.isOperatorAllowed(address(this), operator)) { + revert OperatorNotAllowed(operator); + } + } + } + _; + } + + function _register(address subscriptionOrRegistrantToCopy, bool subscribe) internal { + // Is the registry deployed? + if (address(OPERATOR_FILTER_REGISTRY).code.length > 0) { + // Is the subscription contract deployed? + if (address(subscriptionOrRegistrantToCopy).code.length > 0) { + // Do we want to subscribe? + if (subscribe) { + OPERATOR_FILTER_REGISTRY.registerAndSubscribe(address(this), subscriptionOrRegistrantToCopy); + } else { + OPERATOR_FILTER_REGISTRY.registerAndCopyEntries(address(this), subscriptionOrRegistrantToCopy); + } + } else { + OPERATOR_FILTER_REGISTRY.register(address(this)); + } + } + } +} diff --git a/contracts/extension/Ownable.sol b/contracts/extension/Ownable.sol index 79f9ad7ac..bdfb98d47 100644 --- a/contracts/extension/Ownable.sol +++ b/contracts/extension/Ownable.sol @@ -1,35 +1,46 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + import "./interface/IOwnable.sol"; /** - * Thirdweb's `Ownable` is a contract extension to be used with any base contract. It exposes functions for setting and reading - * who the 'owner' of the inheriting smart contract is, and lets the inheriting contract perform conditional logic that uses - * information about who the contract's owner is. + * @title Ownable + * @notice Thirdweb's `Ownable` is a contract extension to be used with any base contract. It exposes functions for setting and reading + * who the 'owner' of the inheriting smart contract is, and lets the inheriting contract perform conditional logic that uses + * information about who the contract's owner is. */ abstract contract Ownable is IOwnable { + /// @dev The sender is not authorized to perform the action + error OwnableUnauthorized(); + /// @dev Owner of the contract (purpose: OpenSea compatibility) address private _owner; /// @dev Reverts if caller is not the owner. modifier onlyOwner() { if (msg.sender != _owner) { - revert("Not authorized"); + revert OwnableUnauthorized(); } _; } - /// @dev Returns the owner of the contract. + /** + * @notice Returns the owner of the contract. + */ function owner() public view override returns (address) { return _owner; } - /// @dev Lets a contract admin set a new owner for the contract. The new owner must be a contract admin. + /** + * @notice Lets an authorized wallet set a new owner for the contract. + * @param _newOwner The address to set as the new owner of the contract. + */ function setOwner(address _newOwner) external override { if (!_canSetOwner()) { - revert("Not authorized"); + revert OwnableUnauthorized(); } _setupOwner(_newOwner); } @@ -43,5 +54,5 @@ abstract contract Ownable is IOwnable { } /// @dev Returns whether owner can be set in the given execution context. - function _canSetOwner() internal virtual returns (bool); + function _canSetOwner() internal view virtual returns (bool); } diff --git a/contracts/extension/Permissions.sol b/contracts/extension/Permissions.sol index 5d58871e3..29362ddcf 100644 --- a/contracts/extension/Permissions.sol +++ b/contracts/extension/Permissions.sol @@ -1,24 +1,65 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + import "./interface/IPermissions.sol"; -import "../lib/TWStrings.sol"; +import "../lib/Strings.sol"; +/** + * @title Permissions + * @dev This contracts provides extending-contracts with role-based access control mechanisms + */ contract Permissions is IPermissions { + /// @dev The `account` is missing a role. + error PermissionsUnauthorizedAccount(address account, bytes32 neededRole); + + /// @dev The `account` already is a holder of `role` + error PermissionsAlreadyGranted(address account, bytes32 role); + + /// @dev Invalid priviledge to revoke + error PermissionsInvalidPermission(address expected, address actual); + + /// @dev Map from keccak256 hash of a role => a map from address => whether address has role. mapping(bytes32 => mapping(address => bool)) private _hasRole; + + /// @dev Map from keccak256 hash of a role to role admin. See {getRoleAdmin}. mapping(bytes32 => bytes32) private _getRoleAdmin; + /// @dev Default admin role for all roles. Only accounts with this role can grant/revoke other roles. bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; + /// @dev Modifier that checks if an account has the specified role; reverts otherwise. modifier onlyRole(bytes32 role) { _checkRole(role, msg.sender); _; } + /** + * @notice Checks whether an account has a particular role. + * @dev Returns `true` if `account` has been granted `role`. + * + * @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + * @param account Address of the account for which the role is being checked. + */ function hasRole(bytes32 role, address account) public view override returns (bool) { return _hasRole[role][account]; } + /** + * @notice Checks whether an account has a particular role; + * role restrictions can be swtiched on and off. + * + * @dev Returns `true` if `account` has been granted `role`. + * Role restrictions can be swtiched on and off: + * - If address(0) has ROLE, then the ROLE restrictions + * don't apply. + * - If address(0) does not have ROLE, then the ROLE + * restrictions will apply. + * + * @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + * @param account Address of the account for which the role is being checked. + */ function hasRoleWithSwitch(bytes32 role, address account) public view returns (bool) { if (!_hasRole[role][address(0)]) { return _hasRole[role][account]; @@ -27,74 +68,92 @@ contract Permissions is IPermissions { return true; } + /** + * @notice Returns the admin role that controls the specified role. + * @dev See {grantRole} and {revokeRole}. + * To change a role's admin, use {_setRoleAdmin}. + * + * @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + */ function getRoleAdmin(bytes32 role) external view override returns (bytes32) { return _getRoleAdmin[role]; } + /** + * @notice Grants a role to an account, if not previously granted. + * @dev Caller must have admin role for the `role`. + * Emits {RoleGranted Event}. + * + * @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + * @param account Address of the account to which the role is being granted. + */ function grantRole(bytes32 role, address account) public virtual override { _checkRole(_getRoleAdmin[role], msg.sender); if (_hasRole[role][account]) { - revert("Can only grant to non holders"); + revert PermissionsAlreadyGranted(account, role); } _setupRole(role, account); } + /** + * @notice Revokes role from an account. + * @dev Caller must have admin role for the `role`. + * Emits {RoleRevoked Event}. + * + * @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + * @param account Address of the account from which the role is being revoked. + */ function revokeRole(bytes32 role, address account) public virtual override { _checkRole(_getRoleAdmin[role], msg.sender); _revokeRole(role, account); } + /** + * @notice Revokes role from the account. + * @dev Caller must have the `role`, with caller being the same as `account`. + * Emits {RoleRevoked Event}. + * + * @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + * @param account Address of the account from which the role is being revoked. + */ function renounceRole(bytes32 role, address account) public virtual override { if (msg.sender != account) { - revert("Can only renounce for self"); + revert PermissionsInvalidPermission(msg.sender, account); } _revokeRole(role, account); } + /// @dev Sets `adminRole` as `role`'s admin role. function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual { bytes32 previousAdminRole = _getRoleAdmin[role]; _getRoleAdmin[role] = adminRole; emit RoleAdminChanged(role, previousAdminRole, adminRole); } + /// @dev Sets up `role` for `account` function _setupRole(bytes32 role, address account) internal virtual { _hasRole[role][account] = true; emit RoleGranted(role, account, msg.sender); } + /// @dev Revokes `role` from `account` function _revokeRole(bytes32 role, address account) internal virtual { _checkRole(role, account); delete _hasRole[role][account]; emit RoleRevoked(role, account, msg.sender); } + /// @dev Checks `role` for `account`. Reverts with a message including the required role. function _checkRole(bytes32 role, address account) internal view virtual { if (!_hasRole[role][account]) { - revert( - string( - abi.encodePacked( - "Permissions: account ", - TWStrings.toHexString(uint160(account), 20), - " is missing role ", - TWStrings.toHexString(uint256(role), 32) - ) - ) - ); + revert PermissionsUnauthorizedAccount(account, role); } } + /// @dev Checks `role` for `account`. Reverts with a message including the required role. function _checkRoleWithSwitch(bytes32 role, address account) internal view virtual { if (!hasRoleWithSwitch(role, account)) { - revert( - string( - abi.encodePacked( - "Permissions: account ", - TWStrings.toHexString(uint160(account), 20), - " is missing role ", - TWStrings.toHexString(uint256(role), 32) - ) - ) - ); + revert PermissionsUnauthorizedAccount(account, role); } } } diff --git a/contracts/extension/PermissionsEnumerable.sol b/contracts/extension/PermissionsEnumerable.sol index 81574d01d..f5480c600 100644 --- a/contracts/extension/PermissionsEnumerable.sol +++ b/contracts/extension/PermissionsEnumerable.sol @@ -1,18 +1,44 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + import "./interface/IPermissionsEnumerable.sol"; import "./Permissions.sol"; +/** + * @title PermissionsEnumerable + * @dev This contracts provides extending-contracts with role-based access control mechanisms. + * Also provides interfaces to view all members with a given role, and total count of members. + */ contract PermissionsEnumerable is IPermissionsEnumerable, Permissions { + /** + * @notice A data structure to store data of members for a given role. + * + * @param index Current index in the list of accounts that have a role. + * @param members map from index => address of account that has a role + * @param indexOf map from address => index which the account has. + */ struct RoleMembers { uint256 index; mapping(uint256 => address) members; mapping(address => uint256) indexOf; } + /// @dev map from keccak256 hash of a role to its members' data. See {RoleMembers}. mapping(bytes32 => RoleMembers) private roleMembers; + /** + * @notice Returns the role-member from a list of members for a role, + * at a given index. + * @dev Returns `member` who has `role`, at `index` of role-members list. + * See struct {RoleMembers}, and mapping {roleMembers} + * + * @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + * @param index Index in list of current members for the role. + * + * @return member Address of account that has `role` + */ function getRoleMember(bytes32 role, uint256 index) external view override returns (address member) { uint256 currentIndex = roleMembers[role].index; uint256 check; @@ -30,6 +56,15 @@ contract PermissionsEnumerable is IPermissionsEnumerable, Permissions { } } + /** + * @notice Returns total number of accounts that have a role. + * @dev Returns `count` of accounts that have `role`. + * See struct {RoleMembers}, and mapping {roleMembers} + * + * @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + * + * @return count Total number of accounts that have `role` + */ function getRoleMemberCount(bytes32 role) external view override returns (uint256 count) { uint256 currentIndex = roleMembers[role].index; @@ -43,16 +78,21 @@ contract PermissionsEnumerable is IPermissionsEnumerable, Permissions { } } + /// @dev Revokes `role` from `account`, and removes `account` from {roleMembers} + /// See {_removeMember} function _revokeRole(bytes32 role, address account) internal override { super._revokeRole(role, account); _removeMember(role, account); } + /// @dev Grants `role` to `account`, and adds `account` to {roleMembers} + /// See {_addMember} function _setupRole(bytes32 role, address account) internal override { super._setupRole(role, account); _addMember(role, account); } + /// @dev adds `account` to {roleMembers}, for `role` function _addMember(bytes32 role, address account) internal { uint256 idx = roleMembers[role].index; roleMembers[role].index += 1; @@ -61,6 +101,7 @@ contract PermissionsEnumerable is IPermissionsEnumerable, Permissions { roleMembers[role].indexOf[account] = idx; } + /// @dev removes `account` from {roleMembers}, for `role` function _removeMember(bytes32 role, address account) internal { uint256 idx = roleMembers[role].indexOf[account]; diff --git a/contracts/extension/PlatformFee.sol b/contracts/extension/PlatformFee.sol index de6811584..4c92c20cb 100644 --- a/contracts/extension/PlatformFee.sol +++ b/contracts/extension/PlatformFee.sol @@ -1,38 +1,77 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + import "./interface/IPlatformFee.sol"; /** - * Thirdweb's `PlatformFee` is a contract extension to be used with any base contract. It exposes functions for setting and reading - * the recipient of platform fee and the platform fee basis points, and lets the inheriting contract perform conditional logic - * that uses information about platform fees, if desired. + * @title Platform Fee + * @notice Thirdweb's `PlatformFee` is a contract extension to be used with any base contract. It exposes functions for setting and reading + * the recipient of platform fee and the platform fee basis points, and lets the inheriting contract perform conditional logic + * that uses information about platform fees, if desired. */ abstract contract PlatformFee is IPlatformFee { + /// @dev The sender is not authorized to perform the action + error PlatformFeeUnauthorized(); + + /// @dev The recipient is invalid + error PlatformFeeInvalidRecipient(address recipient); + + /// @dev The fee bps exceeded the max value + error PlatformFeeExceededMaxFeeBps(uint256 max, uint256 actual); + /// @dev The address that receives all platform fees from all sales. address private platformFeeRecipient; /// @dev The % of primary sales collected as platform fees. uint16 private platformFeeBps; + /// @dev Fee type variants: percentage fee and flat fee + PlatformFeeType private platformFeeType; + + /// @dev The flat amount collected by the contract as fees on primary sales. + uint256 private flatPlatformFee; + /// @dev Returns the platform fee recipient and bps. function getPlatformFeeInfo() public view override returns (address, uint16) { return (platformFeeRecipient, uint16(platformFeeBps)); } - /// @dev Lets a contract admin update the platform fee recipient and bps + /// @dev Returns the platform fee bps and recipient. + function getFlatPlatformFeeInfo() public view returns (address, uint256) { + return (platformFeeRecipient, flatPlatformFee); + } + + /// @dev Returns the platform fee type. + function getPlatformFeeType() public view returns (PlatformFeeType) { + return platformFeeType; + } + + /** + * @notice Updates the platform fee recipient and bps. + * @dev Caller should be authorized to set platform fee info. + * See {_canSetPlatformFeeInfo}. + * Emits {PlatformFeeInfoUpdated Event}; See {_setupPlatformFeeInfo}. + * + * @param _platformFeeRecipient Address to be set as new platformFeeRecipient. + * @param _platformFeeBps Updated platformFeeBps. + */ function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) external override { if (!_canSetPlatformFeeInfo()) { - revert("Not authorized"); + revert PlatformFeeUnauthorized(); } _setupPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); } - /// @dev Lets a contract admin update the platform fee recipient and bps + /// @dev Sets the platform fee recipient and bps function _setupPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) internal { if (_platformFeeBps > 10_000) { - revert("Exceeds max bps"); + revert PlatformFeeExceededMaxFeeBps(10_000, _platformFeeBps); + } + if (_platformFeeRecipient == address(0)) { + revert PlatformFeeInvalidRecipient(_platformFeeRecipient); } platformFeeBps = uint16(_platformFeeBps); @@ -41,6 +80,38 @@ abstract contract PlatformFee is IPlatformFee { emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); } + /// @notice Lets a module admin set a flat fee on primary sales. + function setFlatPlatformFeeInfo(address _platformFeeRecipient, uint256 _flatFee) external { + if (!_canSetPlatformFeeInfo()) { + revert PlatformFeeUnauthorized(); + } + + _setupFlatPlatformFeeInfo(_platformFeeRecipient, _flatFee); + } + + /// @dev Sets a flat fee on primary sales. + function _setupFlatPlatformFeeInfo(address _platformFeeRecipient, uint256 _flatFee) internal { + flatPlatformFee = _flatFee; + platformFeeRecipient = _platformFeeRecipient; + + emit FlatPlatformFeeUpdated(_platformFeeRecipient, _flatFee); + } + + /// @notice Lets a module admin set platform fee type. + function setPlatformFeeType(PlatformFeeType _feeType) external { + if (!_canSetPlatformFeeInfo()) { + revert PlatformFeeUnauthorized(); + } + _setupPlatformFeeType(_feeType); + } + + /// @dev Sets platform fee type. + function _setupPlatformFeeType(PlatformFeeType _feeType) internal { + platformFeeType = _feeType; + + emit PlatformFeeTypeUpdated(_feeType); + } + /// @dev Returns whether platform fee info can be set in the given execution context. - function _canSetPlatformFeeInfo() internal virtual returns (bool); + function _canSetPlatformFeeInfo() internal view virtual returns (bool); } diff --git a/contracts/extension/PrimarySale.sol b/contracts/extension/PrimarySale.sol index e821f410b..ca3588edd 100644 --- a/contracts/extension/PrimarySale.sol +++ b/contracts/extension/PrimarySale.sol @@ -1,36 +1,57 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + import "./interface/IPrimarySale.sol"; /** - * Thirdweb's `PrimarySale` is a contract extension to be used with any base contract. It exposes functions for setting and reading - * the recipient of primary sales, and lets the inheriting contract perform conditional logic that uses information about - * primary sales, if desired. + * @title Primary Sale + * @notice Thirdweb's `PrimarySale` is a contract extension to be used with any base contract. It exposes functions for setting and reading + * the recipient of primary sales, and lets the inheriting contract perform conditional logic that uses information about + * primary sales, if desired. */ abstract contract PrimarySale is IPrimarySale { + /// @dev The sender is not authorized to perform the action + error PrimarySaleUnauthorized(); + + /// @dev The recipient is invalid + error PrimarySaleInvalidRecipient(address recipient); + /// @dev The address that receives all primary sales value. address private recipient; + /// @dev Returns primary sale recipient address. function primarySaleRecipient() public view override returns (address) { return recipient; } - /// @dev Lets a contract admin set the recipient for all primary sales. + /** + * @notice Updates primary sale recipient. + * @dev Caller should be authorized to set primary sales info. + * See {_canSetPrimarySaleRecipient}. + * Emits {PrimarySaleRecipientUpdated Event}; See {_setupPrimarySaleRecipient}. + * + * @param _saleRecipient Address to be set as new recipient of primary sales. + */ function setPrimarySaleRecipient(address _saleRecipient) external override { if (!_canSetPrimarySaleRecipient()) { - revert("Not authorized"); + revert PrimarySaleUnauthorized(); } _setupPrimarySaleRecipient(_saleRecipient); } /// @dev Lets a contract admin set the recipient for all primary sales. function _setupPrimarySaleRecipient(address _saleRecipient) internal { + if (_saleRecipient == address(0)) { + revert PrimarySaleInvalidRecipient(_saleRecipient); + } + recipient = _saleRecipient; emit PrimarySaleRecipientUpdated(_saleRecipient); } /// @dev Returns whether primary sale recipient can be set in the given execution context. - function _canSetPrimarySaleRecipient() internal virtual returns (bool); + function _canSetPrimarySaleRecipient() internal view virtual returns (bool); } diff --git a/contracts/extension/Proxy.sol b/contracts/extension/Proxy.sol new file mode 100644 index 000000000..bb632e93d --- /dev/null +++ b/contracts/extension/Proxy.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.0; + +/** + * @dev This abstract contract provides a fallback function that delegates all calls to another contract using the EVM + * instruction `delegatecall`. We refer to the second contract as the _implementation_ behind the proxy, and it has to + * be specified by overriding the virtual {_implementation} function. + * + * Additionally, delegation to the implementation can be triggered manually through the {_fallback} function, or to a + * different contract through the {_delegate} function. + * + * The success and return data of the delegated call will be returned back to the caller of the proxy. + */ +abstract contract Proxy { + /** + * @dev Delegates the current call to `implementation`. + * + * This function does not return to its internal call site, it will return directly to the external caller. + */ + function _delegate(address implementation) internal virtual { + assembly { + // Copy msg.data. We take full control of memory in this inline assembly + // block because it will not return to Solidity code. We overwrite the + // Solidity scratch pad at memory position 0. + calldatacopy(0, 0, calldatasize()) + + // Call the implementation. + // out and outsize are 0 because we don't know the size yet. + let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) + + // Copy the returned data. + returndatacopy(0, 0, returndatasize()) + + switch result + // delegatecall returns 0 on error. + case 0 { + revert(0, returndatasize()) + } + default { + return(0, returndatasize()) + } + } + } + + /** + * @dev This is a virtual function that should be overridden so it returns the address to which the fallback function + * and {_fallback} should delegate. + */ + function _implementation() internal view virtual returns (address); + + /** + * @dev Delegates the current call to the address returned by `_implementation()`. + * + * This function does not return to its internal call site, it will return directly to the external caller. + */ + function _fallback() internal virtual { + _beforeFallback(); + _delegate(_implementation()); + } + + /** + * @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if no other + * function in the contract matches the call data. + */ + fallback() external payable virtual { + _fallback(); + } + + /** + * @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if call data + * is empty. + */ + receive() external payable virtual { + _fallback(); + } + + /** + * @dev Hook that is called before falling back to the implementation. Can happen as part of a manual `_fallback` + * call, or as part of the Solidity `fallback` or `receive` functions. + * + * If overridden should call `super._beforeFallback()`. + */ + function _beforeFallback() internal virtual {} +} diff --git a/contracts/extension/ProxyForUpgradeable.sol b/contracts/extension/ProxyForUpgradeable.sol new file mode 100644 index 000000000..dcbf4043e --- /dev/null +++ b/contracts/extension/ProxyForUpgradeable.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./Proxy.sol"; +import "../external-deps/openzeppelin/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 ProxyForUpgradeable is Proxy, ERC1967Upgrade { + /** + * @dev Initializes the upgradeable proxy with an initial implementation specified by `_logic`. + * + * If `_data` is nonempty, it's used as data in a delegate call to `_logic`. This will typically be an encoded + * function call, and allows initializing the storage of the proxy like a Solidity constructor. + */ + constructor(address _logic, bytes memory _data) payable { + _upgradeToAndCall(_logic, _data, false); + } + + /** + * @dev Returns the current implementation address. + */ + function _implementation() internal view virtual override returns (address impl) { + return ERC1967Upgrade._getImplementation(); + } +} diff --git a/contracts/extension/Royalty.sol b/contracts/extension/Royalty.sol index f73ec40ed..765fcd08f 100644 --- a/contracts/extension/Royalty.sol +++ b/contracts/extension/Royalty.sol @@ -1,17 +1,29 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + import "./interface/IRoyalty.sol"; /** - * Thirdweb's `Royalty` is a contract extension to be used with any base contract. It exposes functions for setting and reading - * the recipient of royalty fee and the royalty fee basis points, and lets the inheriting contract perform conditional logic - * that uses information about royalty fees, if desired. + * @title Royalty + * @notice Thirdweb's `Royalty` is a contract extension to be used with any base contract. It exposes functions for setting and reading + * the recipient of royalty fee and the royalty fee basis points, and lets the inheriting contract perform conditional logic + * that uses information about royalty fees, if desired. * - * The `Royalty` contract is ERC2981 compliant. + * @dev The `Royalty` contract is ERC2981 compliant. */ abstract contract Royalty is IRoyalty { + /// @dev The sender is not authorized to perform the action + error RoyaltyUnauthorized(); + + /// @dev The recipient is invalid + error RoyaltyInvalidRecipient(address recipient); + + /// @dev The fee bps exceeded the max value + error RoyaltyExceededMaxFeeBps(uint256 max, uint256 actual); + /// @dev The (default) address that receives all royalty value. address private royaltyRecipient; @@ -21,20 +33,29 @@ abstract contract Royalty is IRoyalty { /// @dev Token ID => royalty recipient and bps for token mapping(uint256 => RoyaltyInfo) private royaltyInfoForToken; - /// @dev Returns the royalty recipient and amount, given a tokenId and sale price. - function royaltyInfo(uint256 tokenId, uint256 salePrice) - external - view - virtual - override - returns (address receiver, uint256 royaltyAmount) - { + /** + * @notice View royalty info for a given token and sale price. + * @dev Returns royalty amount and recipient for `tokenId` and `salePrice`. + * @param tokenId The tokenID of the NFT for which to query royalty info. + * @param salePrice Sale price of the token. + * + * @return receiver Address of royalty recipient account. + * @return royaltyAmount Royalty amount calculated at current royaltyBps value. + */ + function royaltyInfo( + uint256 tokenId, + uint256 salePrice + ) external view virtual override returns (address receiver, uint256 royaltyAmount) { (address recipient, uint256 bps) = getRoyaltyInfoForToken(tokenId); receiver = recipient; royaltyAmount = (salePrice * bps) / 10_000; } - /// @dev Returns the royalty recipient and bps for a particular token Id. + /** + * @notice View royalty info for a given token. + * @dev Returns royalty recipient and bps for `_tokenId`. + * @param _tokenId The tokenID of the NFT for which to query royalty info. + */ function getRoyaltyInfoForToken(uint256 _tokenId) public view override returns (address, uint16) { RoyaltyInfo memory royaltyForToken = royaltyInfoForToken[_tokenId]; @@ -44,15 +65,25 @@ abstract contract Royalty is IRoyalty { : (royaltyForToken.recipient, uint16(royaltyForToken.bps)); } - /// @dev Returns the default royalty recipient and bps. + /** + * @notice Returns the defualt royalty recipient and BPS for this contract's NFTs. + */ function getDefaultRoyaltyInfo() external view override returns (address, uint16) { return (royaltyRecipient, uint16(royaltyBps)); } - /// @dev Lets a contract admin update the default royalty recipient and bps. + /** + * @notice Updates default royalty recipient and bps. + * @dev Caller should be authorized to set royalty info. + * See {_canSetRoyaltyInfo}. + * Emits {DefaultRoyalty Event}; See {_setupDefaultRoyaltyInfo}. + * + * @param _royaltyRecipient Address to be set as default royalty recipient. + * @param _royaltyBps Updated royalty bps. + */ function setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) external override { if (!_canSetRoyaltyInfo()) { - revert("Not authorized"); + revert RoyaltyUnauthorized(); } _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); @@ -61,7 +92,7 @@ abstract contract Royalty is IRoyalty { /// @dev Lets a contract admin update the default royalty recipient and bps. function _setupDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) internal { if (_royaltyBps > 10_000) { - revert("Exceeds max bps"); + revert RoyaltyExceededMaxFeeBps(10_000, _royaltyBps); } royaltyRecipient = _royaltyRecipient; @@ -70,27 +101,27 @@ abstract contract Royalty is IRoyalty { emit DefaultRoyalty(_royaltyRecipient, _royaltyBps); } - /// @dev Lets a contract admin set the royalty recipient and bps for a particular token Id. - function setRoyaltyInfoForToken( - uint256 _tokenId, - address _recipient, - uint256 _bps - ) external override { + /** + * @notice Updates default royalty recipient and bps for a particular token. + * @dev Sets royalty info for `_tokenId`. Caller should be authorized to set royalty info. + * See {_canSetRoyaltyInfo}. + * Emits {RoyaltyForToken Event}; See {_setupRoyaltyInfoForToken}. + * + * @param _recipient Address to be set as royalty recipient for given token Id. + * @param _bps Updated royalty bps for the token Id. + */ + function setRoyaltyInfoForToken(uint256 _tokenId, address _recipient, uint256 _bps) external override { if (!_canSetRoyaltyInfo()) { - revert("Not authorized"); + revert RoyaltyUnauthorized(); } _setupRoyaltyInfoForToken(_tokenId, _recipient, _bps); } /// @dev Lets a contract admin set the royalty recipient and bps for a particular token Id. - function _setupRoyaltyInfoForToken( - uint256 _tokenId, - address _recipient, - uint256 _bps - ) internal { + function _setupRoyaltyInfoForToken(uint256 _tokenId, address _recipient, uint256 _bps) internal { if (_bps > 10_000) { - revert("Exceeds max bps"); + revert RoyaltyExceededMaxFeeBps(10_000, _bps); } royaltyInfoForToken[_tokenId] = RoyaltyInfo({ recipient: _recipient, bps: _bps }); @@ -99,5 +130,5 @@ abstract contract Royalty is IRoyalty { } /// @dev Returns whether royalty info can be set in the given execution context. - function _canSetRoyaltyInfo() internal virtual returns (bool); + function _canSetRoyaltyInfo() internal view virtual returns (bool); } diff --git a/contracts/extension/SeaportEIP1271.sol b/contracts/extension/SeaportEIP1271.sol new file mode 100644 index 000000000..4705e898a --- /dev/null +++ b/contracts/extension/SeaportEIP1271.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import { ECDSA } from "solady/src/utils/ECDSA.sol"; +import { SeaportOrderParser } from "./SeaportOrderParser.sol"; +import { OrderParameters } from "seaport-types/src/lib/ConsiderationStructs.sol"; + +abstract contract SeaportEIP1271 is SeaportOrderParser { + using ECDSA for bytes32; + + bytes32 private constant ACCOUNT_MESSAGE_TYPEHASH = keccak256("AccountMessage(bytes message)"); + bytes32 private constant EIP712_TYPEHASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + bytes32 private immutable HASHED_NAME; + bytes32 private immutable HASHED_VERSION; + + /// @notice The function selector of EIP1271.isValidSignature to be returned on sucessful signature verification. + bytes4 public constant MAGICVALUE = 0x1626ba7e; + + constructor(string memory _name, string memory _version) { + HASHED_NAME = keccak256(bytes(_name)); + HASHED_VERSION = keccak256(bytes(_version)); + } + + /// @notice See EIP-1271: https://eips.ethereum.org/EIPS/eip-1271 + function isValidSignature( + bytes32 _message, + bytes memory _signature + ) public view virtual returns (bytes4 magicValue) { + bytes32 targetDigest; + bytes memory targetSig; + + // Handle OpenSea bulk order signatures that are >65 bytes in length. + if (_signature.length > 65) { + // Decode packed signature and order parameters. + (bytes memory extractedPackedSig, OrderParameters memory orderParameters, uint256 counter) = abi.decode( + _signature, + (bytes, OrderParameters, uint256) + ); + + // Verify that the original digest matches the digest built with order parameters. + bytes32 domainSeparator = _buildSeaportDomainSeparator(msg.sender); + bytes32 orderHash = _deriveOrderHash(orderParameters, counter); + + require( + _deriveEIP712Digest(domainSeparator, orderHash) == _message, + "Seaport: order hash does not match the provided message." + ); + + // Build bulk signature digest + targetDigest = _deriveEIP712Digest(domainSeparator, _computeBulkOrderProof(extractedPackedSig, orderHash)); + + // Extract the signature, which is the first 65 bytes + targetSig = new bytes(65); + for (uint256 i = 0; i < 65; i++) { + targetSig[i] = extractedPackedSig[i]; + } + } else { + targetDigest = getMessageHash(_message); + targetSig = _signature; + } + + address signer = targetDigest.recover(targetSig); + + if (_isAuthorizedSigner(signer)) { + magicValue = MAGICVALUE; + } + } + + /** + * @notice Returns the hash of message that should be signed for EIP1271 verification. + * @param _hash The message hash pre replay protection. + * @return messageHash The digest (with replay protection) to sign for EIP-1271 verification. + */ + function getMessageHash(bytes32 _hash) public view returns (bytes32) { + bytes32 messageHash = keccak256(abi.encode(_hash)); + bytes32 typedDataHash = keccak256(abi.encode(ACCOUNT_MESSAGE_TYPEHASH, messageHash)); + return keccak256(abi.encodePacked("\x19\x01", _accountDomainSeparator(), typedDataHash)); + } + + /// @notice Returns the EIP712 domain separator for the contract. + function _accountDomainSeparator() private view returns (bytes32) { + return keccak256(abi.encode(EIP712_TYPEHASH, HASHED_NAME, HASHED_VERSION, block.chainid, address(this))); + } + + /// @notice Returns whether a given signer is an authorized signer for the contract. + function _isAuthorizedSigner(address _signer) internal view virtual returns (bool); +} diff --git a/contracts/extension/SeaportOrderParser.sol b/contracts/extension/SeaportOrderParser.sol new file mode 100644 index 000000000..f76e3e337 --- /dev/null +++ b/contracts/extension/SeaportOrderParser.sol @@ -0,0 +1,550 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/* solhint-disable */ + +import { OrderParameters } from "seaport-types/src/lib/ConsiderationStructs.sol"; +import { EIP_712_PREFIX, EIP712_ConsiderationItem_size, EIP712_DigestPayload_size, EIP712_DomainSeparator_offset, EIP712_OfferItem_size, EIP712_Order_size, EIP712_OrderHash_offset, OneWord, OneWordShift, OrderParameters_consideration_head_offset, OrderParameters_counter_offset, OrderParameters_offer_head_offset, TwoWords, BulkOrderProof_keyShift, BulkOrderProof_keySize, BulkOrder_Typehash_Height_One, BulkOrder_Typehash_Height_Two, BulkOrder_Typehash_Height_Three, BulkOrder_Typehash_Height_Four, BulkOrder_Typehash_Height_Five, BulkOrder_Typehash_Height_Six, BulkOrder_Typehash_Height_Seven, BulkOrder_Typehash_Height_Eight, BulkOrder_Typehash_Height_Nine, BulkOrder_Typehash_Height_Ten, BulkOrder_Typehash_Height_Eleven, BulkOrder_Typehash_Height_Twelve, BulkOrder_Typehash_Height_Thirteen, BulkOrder_Typehash_Height_Fourteen, BulkOrder_Typehash_Height_Fifteen, BulkOrder_Typehash_Height_Sixteen, BulkOrder_Typehash_Height_Seventeen, BulkOrder_Typehash_Height_Eighteen, BulkOrder_Typehash_Height_Nineteen, BulkOrder_Typehash_Height_Twenty, BulkOrder_Typehash_Height_TwentyOne, BulkOrder_Typehash_Height_TwentyTwo, BulkOrder_Typehash_Height_TwentyThree, BulkOrder_Typehash_Height_TwentyFour, FreeMemoryPointerSlot } from "seaport-types/src/lib/ConsiderationConstants.sol"; + +contract SeaportOrderParser { + uint256 constant ECDSA_MAXLENGTH = 65; + + bytes32 private immutable _NAME_HASH; + bytes32 private immutable _VERSION_HASH; + bytes32 private immutable _EIP_712_DOMAIN_TYPEHASH; + bytes32 private immutable _OFFER_ITEM_TYPEHASH; + bytes32 private immutable _CONSIDERATION_ITEM_TYPEHASH; + bytes32 private immutable _ORDER_TYPEHASH; + + constructor() { + ( + _NAME_HASH, + _VERSION_HASH, + _EIP_712_DOMAIN_TYPEHASH, + _OFFER_ITEM_TYPEHASH, + _CONSIDERATION_ITEM_TYPEHASH, + _ORDER_TYPEHASH + ) = _deriveTypehashes(); + } + + function _nameString() internal pure virtual returns (string memory) { + // Return the name of the contract. + return "Seaport"; + } + + function _buildSeaportDomainSeparator(address _domainAddress) internal view returns (bytes32) { + return + keccak256(abi.encode(_EIP_712_DOMAIN_TYPEHASH, _NAME_HASH, _VERSION_HASH, block.chainid, _domainAddress)); + } + + function _deriveOrderHash( + OrderParameters memory orderParameters, + uint256 counter + ) internal view returns (bytes32 orderHash) { + // Get length of original consideration array and place it on the stack. + uint256 originalConsiderationLength = (orderParameters.totalOriginalConsiderationItems); + + /* + * Memory layout for an array of structs (dynamic or not) is similar + * to ABI encoding of dynamic types, with a head segment followed by + * a data segment. The main difference is that the head of an element + * is a memory pointer rather than an offset. + */ + + // Declare a variable for the derived hash of the offer array. + bytes32 offerHash; + + // Read offer item EIP-712 typehash from runtime code & place on stack. + bytes32 typeHash = _OFFER_ITEM_TYPEHASH; + + // Utilize assembly so that memory regions can be reused across hashes. + assembly { + // Retrieve the free memory pointer and place on the stack. + let hashArrPtr := mload(FreeMemoryPointerSlot) + + // Get the pointer to the offers array. + let offerArrPtr := mload(add(orderParameters, OrderParameters_offer_head_offset)) + + // Load the length. + let offerLength := mload(offerArrPtr) + + // Set the pointer to the first offer's head. + offerArrPtr := add(offerArrPtr, OneWord) + + // Iterate over the offer items. + for { + let i := 0 + } lt(i, offerLength) { + i := add(i, 1) + } { + // Read the pointer to the offer data and subtract one word + // to get typeHash pointer. + let ptr := sub(mload(offerArrPtr), OneWord) + + // Read the current value before the offer data. + let value := mload(ptr) + + // Write the type hash to the previous word. + mstore(ptr, typeHash) + + // Take the EIP712 hash and store it in the hash array. + mstore(hashArrPtr, keccak256(ptr, EIP712_OfferItem_size)) + + // Restore the previous word. + mstore(ptr, value) + + // Increment the array pointers by one word. + offerArrPtr := add(offerArrPtr, OneWord) + hashArrPtr := add(hashArrPtr, OneWord) + } + + // Derive the offer hash using the hashes of each item. + offerHash := keccak256(mload(FreeMemoryPointerSlot), shl(OneWordShift, offerLength)) + } + + // Declare a variable for the derived hash of the consideration array. + bytes32 considerationHash; + + // Read consideration item typehash from runtime code & place on stack. + typeHash = _CONSIDERATION_ITEM_TYPEHASH; + + // Utilize assembly so that memory regions can be reused across hashes. + assembly { + // Retrieve the free memory pointer and place on the stack. + let hashArrPtr := mload(FreeMemoryPointerSlot) + + // Get the pointer to the consideration array. + let considerationArrPtr := add( + mload(add(orderParameters, OrderParameters_consideration_head_offset)), + OneWord + ) + + // Iterate over the consideration items (not including tips). + for { + let i := 0 + } lt(i, originalConsiderationLength) { + i := add(i, 1) + } { + // Read the pointer to the consideration data and subtract one + // word to get typeHash pointer. + let ptr := sub(mload(considerationArrPtr), OneWord) + + // Read the current value before the consideration data. + let value := mload(ptr) + + // Write the type hash to the previous word. + mstore(ptr, typeHash) + + // Take the EIP712 hash and store it in the hash array. + mstore(hashArrPtr, keccak256(ptr, EIP712_ConsiderationItem_size)) + + // Restore the previous word. + mstore(ptr, value) + + // Increment the array pointers by one word. + considerationArrPtr := add(considerationArrPtr, OneWord) + hashArrPtr := add(hashArrPtr, OneWord) + } + + // Derive the consideration hash using the hashes of each item. + considerationHash := keccak256(mload(FreeMemoryPointerSlot), shl(OneWordShift, originalConsiderationLength)) + } + + // Read order item EIP-712 typehash from runtime code & place on stack. + typeHash = _ORDER_TYPEHASH; + + // Utilize assembly to access derived hashes & other arguments directly. + assembly { + // Retrieve pointer to the region located just behind parameters. + let typeHashPtr := sub(orderParameters, OneWord) + + // Store the value at that pointer location to restore later. + let previousValue := mload(typeHashPtr) + + // Store the order item EIP-712 typehash at the typehash location. + mstore(typeHashPtr, typeHash) + + // Retrieve the pointer for the offer array head. + let offerHeadPtr := add(orderParameters, OrderParameters_offer_head_offset) + + // Retrieve the data pointer referenced by the offer head. + let offerDataPtr := mload(offerHeadPtr) + + // Store the offer hash at the retrieved memory location. + mstore(offerHeadPtr, offerHash) + + // Retrieve the pointer for the consideration array head. + let considerationHeadPtr := add(orderParameters, OrderParameters_consideration_head_offset) + + // Retrieve the data pointer referenced by the consideration head. + let considerationDataPtr := mload(considerationHeadPtr) + + // Store the consideration hash at the retrieved memory location. + mstore(considerationHeadPtr, considerationHash) + + // Retrieve the pointer for the counter. + let counterPtr := add(orderParameters, OrderParameters_counter_offset) + + // Store the counter at the retrieved memory location. + mstore(counterPtr, counter) + + // Derive the order hash using the full range of order parameters. + orderHash := keccak256(typeHashPtr, EIP712_Order_size) + + // Restore the value previously held at typehash pointer location. + mstore(typeHashPtr, previousValue) + + // Restore offer data pointer at the offer head pointer location. + mstore(offerHeadPtr, offerDataPtr) + + // Restore consideration data pointer at the consideration head ptr. + mstore(considerationHeadPtr, considerationDataPtr) + + // Restore consideration item length at the counter pointer. + mstore(counterPtr, originalConsiderationLength) + } + } + + function _deriveTypehashes() + internal + pure + returns ( + bytes32 nameHash, + bytes32 versionHash, + bytes32 eip712DomainTypehash, + bytes32 offerItemTypehash, + bytes32 considerationItemTypehash, + bytes32 orderTypehash + ) + { + // Derive hash of the name of the contract. + nameHash = keccak256(bytes(_nameString())); + + // Derive hash of the version string of the contract. + versionHash = keccak256(bytes("1.5")); + + // Construct the OfferItem type string. + bytes memory offerItemTypeString = bytes( + "OfferItem(" + "uint8 itemType," + "address token," + "uint256 identifierOrCriteria," + "uint256 startAmount," + "uint256 endAmount" + ")" + ); + + // Construct the ConsiderationItem type string. + bytes memory considerationItemTypeString = bytes( + "ConsiderationItem(" + "uint8 itemType," + "address token," + "uint256 identifierOrCriteria," + "uint256 startAmount," + "uint256 endAmount," + "address recipient" + ")" + ); + + // Construct the OrderComponents type string, not including the above. + bytes memory orderComponentsPartialTypeString = bytes( + "OrderComponents(" + "address offerer," + "address zone," + "OfferItem[] offer," + "ConsiderationItem[] consideration," + "uint8 orderType," + "uint256 startTime," + "uint256 endTime," + "bytes32 zoneHash," + "uint256 salt," + "bytes32 conduitKey," + "uint256 counter" + ")" + ); + + // Construct the primary EIP-712 domain type string. + eip712DomainTypehash = keccak256( + bytes( + "EIP712Domain(" + "string name," + "string version," + "uint256 chainId," + "address verifyingContract" + ")" + ) + ); + + // Derive the OfferItem type hash using the corresponding type string. + offerItemTypehash = keccak256(offerItemTypeString); + + // Derive ConsiderationItem type hash using corresponding type string. + considerationItemTypehash = keccak256(considerationItemTypeString); + + bytes memory orderTypeString = bytes.concat( + orderComponentsPartialTypeString, + considerationItemTypeString, + offerItemTypeString + ); + + // Derive OrderItem type hash via combination of relevant type strings. + orderTypehash = keccak256(orderTypeString); + } + + function _computeBulkOrderProof( + bytes memory proofAndSignature, + bytes32 leaf + ) internal pure returns (bytes32 bulkOrderHash) { + // Declare arguments for the root hash and the height of the proof. + bytes32 root; + uint256 height; + + // Utilize assembly to efficiently derive the root hash using the proof. + assembly { + // Retrieve the length of the proof, key, and signature combined. + let fullLength := mload(proofAndSignature) + + // If proofAndSignature has odd length, it is a compact signature + // with 64 bytes. + let signatureLength := sub(ECDSA_MAXLENGTH, and(fullLength, 1)) + + // Derive height (or depth of tree) with signature and proof length. + height := shr(OneWordShift, sub(fullLength, signatureLength)) + + // Update the length in memory to only include the signature. + mstore(proofAndSignature, signatureLength) + + // Derive the pointer for the key using the signature length. + let keyPtr := add(proofAndSignature, add(OneWord, signatureLength)) + + // Retrieve the three-byte key using the derived pointer. + let key := shr(BulkOrderProof_keyShift, mload(keyPtr)) + + /// Retrieve pointer to first proof element by applying a constant + // for the key size to the derived key pointer. + let proof := add(keyPtr, BulkOrderProof_keySize) + + // Compute level 1. + let scratchPtr1 := shl(OneWordShift, and(key, 1)) + mstore(scratchPtr1, leaf) + mstore(xor(scratchPtr1, OneWord), mload(proof)) + + // Compute remaining proofs. + for { + let i := 1 + } lt(i, height) { + i := add(i, 1) + } { + proof := add(proof, OneWord) + let scratchPtr := shl(OneWordShift, and(shr(i, key), 1)) + mstore(scratchPtr, keccak256(0, TwoWords)) + mstore(xor(scratchPtr, OneWord), mload(proof)) + } + + // Compute root hash. + root := keccak256(0, TwoWords) + } + + // Retrieve appropriate typehash constant based on height. + bytes32 rootTypeHash = _lookupBulkOrderTypehash(height); + + // Use the typehash and the root hash to derive final bulk order hash. + assembly { + mstore(0, rootTypeHash) + mstore(OneWord, root) + bulkOrderHash := keccak256(0, TwoWords) + } + } + + function _lookupBulkOrderTypehash(uint256 _treeHeight) internal pure returns (bytes32 _typeHash) { + // Utilize assembly to efficiently retrieve correct bulk order typehash. + assembly { + // Use a Yul function to enable use of the `leave` keyword + // to stop searching once the appropriate type hash is found. + function lookupTypeHash(treeHeight) -> typeHash { + // Handle tree heights one through eight. + if lt(treeHeight, 9) { + // Handle tree heights one through four. + if lt(treeHeight, 5) { + // Handle tree heights one and two. + if lt(treeHeight, 3) { + // Utilize branchless logic to determine typehash. + typeHash := ternary( + eq(treeHeight, 1), + BulkOrder_Typehash_Height_One, + BulkOrder_Typehash_Height_Two + ) + + // Exit the function once typehash has been located. + leave + } + + // Handle height three and four via branchless logic. + typeHash := ternary( + eq(treeHeight, 3), + BulkOrder_Typehash_Height_Three, + BulkOrder_Typehash_Height_Four + ) + + // Exit the function once typehash has been located. + leave + } + + // Handle tree height five and six. + if lt(treeHeight, 7) { + // Utilize branchless logic to determine typehash. + typeHash := ternary( + eq(treeHeight, 5), + BulkOrder_Typehash_Height_Five, + BulkOrder_Typehash_Height_Six + ) + + // Exit the function once typehash has been located. + leave + } + + // Handle height seven and eight via branchless logic. + typeHash := ternary( + eq(treeHeight, 7), + BulkOrder_Typehash_Height_Seven, + BulkOrder_Typehash_Height_Eight + ) + + // Exit the function once typehash has been located. + leave + } + + // Handle tree height nine through sixteen. + if lt(treeHeight, 17) { + // Handle tree height nine through twelve. + if lt(treeHeight, 13) { + // Handle tree height nine and ten. + if lt(treeHeight, 11) { + // Utilize branchless logic to determine typehash. + typeHash := ternary( + eq(treeHeight, 9), + BulkOrder_Typehash_Height_Nine, + BulkOrder_Typehash_Height_Ten + ) + + // Exit the function once typehash has been located. + leave + } + + // Handle height eleven and twelve via branchless logic. + typeHash := ternary( + eq(treeHeight, 11), + BulkOrder_Typehash_Height_Eleven, + BulkOrder_Typehash_Height_Twelve + ) + + // Exit the function once typehash has been located. + leave + } + + // Handle tree height thirteen and fourteen. + if lt(treeHeight, 15) { + // Utilize branchless logic to determine typehash. + typeHash := ternary( + eq(treeHeight, 13), + BulkOrder_Typehash_Height_Thirteen, + BulkOrder_Typehash_Height_Fourteen + ) + + // Exit the function once typehash has been located. + leave + } + // Handle height fifteen and sixteen via branchless logic. + typeHash := ternary( + eq(treeHeight, 15), + BulkOrder_Typehash_Height_Fifteen, + BulkOrder_Typehash_Height_Sixteen + ) + + // Exit the function once typehash has been located. + leave + } + + // Handle tree height seventeen through twenty. + if lt(treeHeight, 21) { + // Handle tree height seventeen and eighteen. + if lt(treeHeight, 19) { + // Utilize branchless logic to determine typehash. + typeHash := ternary( + eq(treeHeight, 17), + BulkOrder_Typehash_Height_Seventeen, + BulkOrder_Typehash_Height_Eighteen + ) + + // Exit the function once typehash has been located. + leave + } + + // Handle height nineteen and twenty via branchless logic. + typeHash := ternary( + eq(treeHeight, 19), + BulkOrder_Typehash_Height_Nineteen, + BulkOrder_Typehash_Height_Twenty + ) + + // Exit the function once typehash has been located. + leave + } + + // Handle tree height twenty-one and twenty-two. + if lt(treeHeight, 23) { + // Utilize branchless logic to determine typehash. + typeHash := ternary( + eq(treeHeight, 21), + BulkOrder_Typehash_Height_TwentyOne, + BulkOrder_Typehash_Height_TwentyTwo + ) + + // Exit the function once typehash has been located. + leave + } + + // Handle height twenty-three & twenty-four w/ branchless logic. + typeHash := ternary( + eq(treeHeight, 23), + BulkOrder_Typehash_Height_TwentyThree, + BulkOrder_Typehash_Height_TwentyFour + ) + + // Exit the function once typehash has been located. + leave + } + + // Implement ternary conditional using branchless logic. + function ternary(cond, ifTrue, ifFalse) -> c { + c := xor(ifFalse, mul(cond, xor(ifFalse, ifTrue))) + } + + // Look up the typehash using the supplied tree height. + _typeHash := lookupTypeHash(_treeHeight) + } + } + + function _deriveEIP712Digest(bytes32 domainSeparator, bytes32 orderHash) internal pure returns (bytes32 value) { + // Leverage scratch space to perform an efficient hash. + assembly { + // Place the EIP-712 prefix at the start of scratch space. + mstore(0, EIP_712_PREFIX) + + // Place the domain separator in the next region of scratch space. + mstore(EIP712_DomainSeparator_offset, domainSeparator) + + // Place the order hash in scratch space, spilling into the first + // two bytes of the free memory pointer — this should never be set + // as memory cannot be expanded to that size, and will be zeroed out + // after the hash is performed. + mstore(EIP712_OrderHash_offset, orderHash) + + // Hash the relevant region (65 bytes). + value := keccak256(0, EIP712_DigestPayload_size) + + // Clear out the dirtied bits in the memory pointer. + mstore(EIP712_OrderHash_offset, 0) + } + } +} diff --git a/contracts/extension/SharedMetadata.sol b/contracts/extension/SharedMetadata.sol new file mode 100644 index 000000000..72d244625 --- /dev/null +++ b/contracts/extension/SharedMetadata.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.10; + +/// @author thirdweb + +import "../lib/NFTMetadataRenderer.sol"; +import "./interface/ISharedMetadata.sol"; +import "../eip/interface/IERC4906.sol"; + +abstract contract SharedMetadata is ISharedMetadata, IERC4906 { + /// @notice Token metadata information + SharedMetadataInfo public sharedMetadata; + + /// @notice Set shared metadata for NFTs + function setSharedMetadata(SharedMetadataInfo calldata _metadata) external virtual { + if (!_canSetSharedMetadata()) { + revert("Not authorized"); + } + _setSharedMetadata(_metadata); + } + + /** + * @dev Sets shared metadata for NFTs. + * @param _metadata common metadata for all tokens + */ + function _setSharedMetadata(SharedMetadataInfo calldata _metadata) internal { + sharedMetadata = SharedMetadataInfo({ + name: _metadata.name, + description: _metadata.description, + imageURI: _metadata.imageURI, + animationURI: _metadata.animationURI + }); + + emit BatchMetadataUpdate(0, type(uint256).max); + + emit SharedMetadataUpdated({ + name: _metadata.name, + description: _metadata.description, + imageURI: _metadata.imageURI, + animationURI: _metadata.animationURI + }); + } + + /** + * @dev Token URI information getter + * @param tokenId Token ID to get URI for + */ + function _getURIFromSharedMetadata(uint256 tokenId) internal view returns (string memory) { + SharedMetadataInfo memory info = sharedMetadata; + + return + NFTMetadataRenderer.createMetadataEdition({ + name: info.name, + description: info.description, + imageURI: info.imageURI, + animationURI: info.animationURI, + tokenOfEdition: tokenId + }); + } + + /// @dev Returns whether shared metadata can be set in the given execution context. + function _canSetSharedMetadata() internal view virtual returns (bool); +} diff --git a/contracts/extension/SignatureAction.sol b/contracts/extension/SignatureAction.sol new file mode 100644 index 000000000..4e857e870 --- /dev/null +++ b/contracts/extension/SignatureAction.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./interface/ISignatureAction.sol"; +import "../external-deps/openzeppelin/utils/cryptography/EIP712.sol"; + +abstract contract SignatureAction is EIP712, ISignatureAction { + using ECDSA for bytes32; + + bytes32 private constant TYPEHASH = + keccak256("GenericRequest(uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid,bytes data)"); + + /// @dev Mapping from a signed request UID => whether the request is processed. + mapping(bytes32 => bool) private executed; + + constructor() EIP712("SignatureAction", "1") {} + + /// @dev Verifies that a request is signed by an authorized account. + function verify( + GenericRequest calldata _req, + bytes calldata _signature + ) public view override returns (bool success, address signer) { + signer = _recoverAddress(_req, _signature); + success = !executed[_req.uid] && _isAuthorizedSigner(signer); + } + + /// @dev Returns whether a given address is authorized to sign requests. + function _isAuthorizedSigner(address _signer) internal view virtual returns (bool); + + /// @dev Verifies a request and marks the request as processed. + function _processRequest( + GenericRequest calldata _req, + bytes calldata _signature + ) internal returns (address signer) { + bool success; + (success, signer) = verify(_req, _signature); + + if (!success) { + revert("Invalid req"); + } + + if (_req.validityStartTimestamp > block.timestamp || block.timestamp > _req.validityEndTimestamp) { + revert("Req expired"); + } + + executed[_req.uid] = true; + } + + /// @dev Returns the address of the signer of the request. + function _recoverAddress(GenericRequest calldata _req, bytes calldata _signature) internal view returns (address) { + return _hashTypedDataV4(keccak256(_encodeRequest(_req))).recover(_signature); + } + + /// @dev Encodes a request for recovery of the signer in `recoverAddress`. + function _encodeRequest(GenericRequest calldata _req) internal pure returns (bytes memory) { + return + abi.encode( + TYPEHASH, + _req.validityStartTimestamp, + _req.validityEndTimestamp, + _req.uid, + keccak256(_req.data) + ); + } +} diff --git a/contracts/extension/SignatureActionUpgradeable.sol b/contracts/extension/SignatureActionUpgradeable.sol new file mode 100644 index 000000000..18b791151 --- /dev/null +++ b/contracts/extension/SignatureActionUpgradeable.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./interface/ISignatureAction.sol"; +import "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol"; + +abstract contract SignatureActionUpgradeable is EIP712Upgradeable, ISignatureAction { + using ECDSAUpgradeable for bytes32; + + bytes32 private constant TYPEHASH = + keccak256("GenericRequest(uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid,bytes data)"); + + /// @dev Mapping from a signed request UID => whether the request is processed. + mapping(bytes32 => bool) private executed; + + function __SignatureAction_init() internal onlyInitializing { + __EIP712_init("SignatureAction", "1"); + } + + function __SignatureAction_init_unchained() internal onlyInitializing {} + + /// @dev Verifies that a request is signed by an authorized account. + function verify( + GenericRequest calldata _req, + bytes calldata _signature + ) public view override returns (bool success, address signer) { + signer = _recoverAddress(_req, _signature); + success = !executed[_req.uid] && _isAuthorizedSigner(signer); + } + + /// @dev Returns whether a given address is authorized to sign requests. + function _isAuthorizedSigner(address _signer) internal view virtual returns (bool); + + /// @dev Verifies a request and marks the request as processed. + function _processRequest( + GenericRequest calldata _req, + bytes calldata _signature + ) internal returns (address signer) { + bool success; + (success, signer) = verify(_req, _signature); + + if (!success) { + revert("Invalid req"); + } + + if (_req.validityStartTimestamp > block.timestamp || block.timestamp > _req.validityEndTimestamp) { + revert("Req expired"); + } + + executed[_req.uid] = true; + } + + /// @dev Returns the address of the signer of the request. + function _recoverAddress(GenericRequest calldata _req, bytes calldata _signature) internal view returns (address) { + return _hashTypedDataV4(keccak256(_encodeRequest(_req))).recover(_signature); + } + + /// @dev Encodes a request for recovery of the signer in `recoverAddress`. + function _encodeRequest(GenericRequest calldata _req) internal pure returns (bytes memory) { + return + abi.encode( + TYPEHASH, + _req.validityStartTimestamp, + _req.validityEndTimestamp, + _req.uid, + keccak256(_req.data) + ); + } +} diff --git a/contracts/extension/SignatureMintERC1155.sol b/contracts/extension/SignatureMintERC1155.sol index 4f8aaef3d..0d1c7aaf3 100644 --- a/contracts/extension/SignatureMintERC1155.sol +++ b/contracts/extension/SignatureMintERC1155.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; -import "./interface/ISignatureMintERC1155.sol"; +/// @author thirdweb -import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol"; +import "./interface/ISignatureMintERC1155.sol"; +import "../external-deps/openzeppelin/utils/cryptography/EIP712.sol"; abstract contract SignatureMintERC1155 is EIP712, ISignatureMintERC1155 { using ECDSA for bytes32; @@ -20,12 +20,10 @@ abstract contract SignatureMintERC1155 is EIP712, ISignatureMintERC1155 { constructor() EIP712("SignatureMintERC1155", "1") {} /// @dev Verifies that a mint request is signed by an account holding MINTER_ROLE (at the time of the function call). - function verify(MintRequest calldata _req, bytes calldata _signature) - public - view - override - returns (bool success, address signer) - { + function verify( + MintRequest calldata _req, + bytes calldata _signature + ) public view override returns (bool success, address signer) { signer = _recoverAddress(_req, _signature); success = !minted[_req.uid] && _canSignMintRequest(signer); } @@ -43,6 +41,8 @@ abstract contract SignatureMintERC1155 is EIP712, ISignatureMintERC1155 { _req.validityStartTimestamp <= block.timestamp && block.timestamp <= _req.validityEndTimestamp, "Request expired" ); + require(_req.to != address(0), "recipient undefined"); + require(_req.quantity > 0, "0 qty"); minted[_req.uid] = true; } @@ -55,20 +55,24 @@ abstract contract SignatureMintERC1155 is EIP712, ISignatureMintERC1155 { /// @dev Resolves 'stack too deep' error in `recoverAddress`. function _encodeRequest(MintRequest calldata _req) internal pure returns (bytes memory) { return - abi.encode( - TYPEHASH, - _req.to, - _req.royaltyRecipient, - _req.royaltyBps, - _req.primarySaleRecipient, - _req.tokenId, - keccak256(bytes(_req.uri)), - _req.quantity, - _req.pricePerToken, - _req.currency, - _req.validityStartTimestamp, - _req.validityEndTimestamp, - _req.uid + bytes.concat( + abi.encode( + TYPEHASH, + _req.to, + _req.royaltyRecipient, + _req.royaltyBps, + _req.primarySaleRecipient, + _req.tokenId, + keccak256(bytes(_req.uri)) + ), + abi.encode( + _req.quantity, + _req.pricePerToken, + _req.currency, + _req.validityStartTimestamp, + _req.validityEndTimestamp, + _req.uid + ) ); } } diff --git a/contracts/extension/SignatureMintERC1155Upgradeable.sol b/contracts/extension/SignatureMintERC1155Upgradeable.sol index 5682a3413..f4e2891b5 100644 --- a/contracts/extension/SignatureMintERC1155Upgradeable.sol +++ b/contracts/extension/SignatureMintERC1155Upgradeable.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + import "./interface/ISignatureMintERC1155.sol"; import "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol"; @@ -24,12 +26,10 @@ abstract contract SignatureMintERC1155Upgradeable is Initializable, EIP712Upgrad function __SignatureMintERC1155_init_unchained() internal onlyInitializing {} /// @dev Verifies that a mint request is signed by an account holding MINTER_ROLE (at the time of the function call). - function verify(MintRequest calldata _req, bytes calldata _signature) - public - view - override - returns (bool success, address signer) - { + function verify( + MintRequest calldata _req, + bytes calldata _signature + ) public view override returns (bool success, address signer) { signer = _recoverAddress(_req, _signature); success = !minted[_req.uid] && _isAuthorizedSigner(signer); } @@ -47,6 +47,8 @@ abstract contract SignatureMintERC1155Upgradeable is Initializable, EIP712Upgrad _req.validityStartTimestamp <= block.timestamp && block.timestamp <= _req.validityEndTimestamp, "Request expired" ); + require(_req.to != address(0), "recipient undefined"); + require(_req.quantity > 0, "0 qty"); minted[_req.uid] = true; } @@ -59,20 +61,24 @@ abstract contract SignatureMintERC1155Upgradeable is Initializable, EIP712Upgrad /// @dev Resolves 'stack too deep' error in `recoverAddress`. function _encodeRequest(MintRequest calldata _req) internal pure returns (bytes memory) { return - abi.encode( - TYPEHASH, - _req.to, - _req.royaltyRecipient, - _req.royaltyBps, - _req.primarySaleRecipient, - _req.tokenId, - keccak256(bytes(_req.uri)), - _req.quantity, - _req.pricePerToken, - _req.currency, - _req.validityStartTimestamp, - _req.validityEndTimestamp, - _req.uid + bytes.concat( + abi.encode( + TYPEHASH, + _req.to, + _req.royaltyRecipient, + _req.royaltyBps, + _req.primarySaleRecipient, + _req.tokenId, + keccak256(bytes(_req.uri)) + ), + abi.encode( + _req.quantity, + _req.pricePerToken, + _req.currency, + _req.validityStartTimestamp, + _req.validityEndTimestamp, + _req.uid + ) ); } } diff --git a/contracts/extension/SignatureMintERC20.sol b/contracts/extension/SignatureMintERC20.sol index e350434fa..fb561b8b3 100644 --- a/contracts/extension/SignatureMintERC20.sol +++ b/contracts/extension/SignatureMintERC20.sol @@ -1,17 +1,17 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; -import "./interface/ISignatureMintERC20.sol"; +/// @author thirdweb -import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol"; +import "./interface/ISignatureMintERC20.sol"; +import "../external-deps/openzeppelin/utils/cryptography/EIP712.sol"; abstract contract SignatureMintERC20 is EIP712, ISignatureMintERC20 { using ECDSA for bytes32; bytes32 private constant TYPEHASH = keccak256( - "MintRequest(address to,address primarySaleRecipient,uint256 quantity,uint256 pricePerToken,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + "MintRequest(address to,address primarySaleRecipient,uint256 quantity,uint256 price,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" ); /// @dev Mapping from mint request UID => whether the mint request is processed. @@ -20,12 +20,10 @@ abstract contract SignatureMintERC20 is EIP712, ISignatureMintERC20 { constructor() EIP712("SignatureMintERC20", "1") {} /// @dev Verifies that a mint request is signed by an account holding MINTER_ROLE (at the time of the function call). - function verify(MintRequest calldata _req, bytes calldata _signature) - public - view - override - returns (bool success, address signer) - { + function verify( + MintRequest calldata _req, + bytes calldata _signature + ) public view override returns (bool success, address signer) { signer = _recoverAddress(_req, _signature); success = !minted[_req.uid] && _canSignMintRequest(signer); } @@ -43,6 +41,8 @@ abstract contract SignatureMintERC20 is EIP712, ISignatureMintERC20 { _req.validityStartTimestamp <= block.timestamp && block.timestamp <= _req.validityEndTimestamp, "Request expired" ); + require(_req.to != address(0), "recipient undefined"); + require(_req.quantity > 0, "0 qty"); minted[_req.uid] = true; } @@ -60,7 +60,7 @@ abstract contract SignatureMintERC20 is EIP712, ISignatureMintERC20 { _req.to, _req.primarySaleRecipient, _req.quantity, - _req.pricePerToken, + _req.price, _req.currency, _req.validityStartTimestamp, _req.validityEndTimestamp, diff --git a/contracts/extension/SignatureMintERC20Upgradeable.sol b/contracts/extension/SignatureMintERC20Upgradeable.sol index 132bbbc1e..d67b1eac5 100644 --- a/contracts/extension/SignatureMintERC20Upgradeable.sol +++ b/contracts/extension/SignatureMintERC20Upgradeable.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + import "./interface/ISignatureMintERC20.sol"; import "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol"; @@ -11,25 +13,24 @@ abstract contract SignatureMintERC20Upgradeable is Initializable, EIP712Upgradea bytes32 private constant TYPEHASH = keccak256( - "MintRequest(address to,address primarySaleRecipient,uint256 quantity,uint256 pricePerToken,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + "MintRequest(address to,address primarySaleRecipient,uint256 quantity,uint256 price,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" ); /// @dev Mapping from mint request UID => whether the mint request is processed. mapping(bytes32 => bool) private minted; - function __SignatureMintERC20_init() internal onlyInitializing { - __EIP712_init("SignatureMintERC20", "1"); + function __SignatureMintERC20_init(string memory _name) internal onlyInitializing { + __EIP712_init(_name, "1"); + __SignatureMintERC20_init_unchained(_name); } - function __SignatureMintERC20_init_unchained() internal onlyInitializing {} + function __SignatureMintERC20_init_unchained(string memory) internal onlyInitializing {} /// @dev Verifies that a mint request is signed by an account holding MINTER_ROLE (at the time of the function call). - function verify(MintRequest calldata _req, bytes calldata _signature) - public - view - override - returns (bool success, address signer) - { + function verify( + MintRequest calldata _req, + bytes calldata _signature + ) public view override returns (bool success, address signer) { signer = _recoverAddress(_req, _signature); success = !minted[_req.uid] && _isAuthorizedSigner(signer); } @@ -47,6 +48,8 @@ abstract contract SignatureMintERC20Upgradeable is Initializable, EIP712Upgradea _req.validityStartTimestamp <= block.timestamp && block.timestamp <= _req.validityEndTimestamp, "Request expired" ); + require(_req.to != address(0), "recipient undefined"); + require(_req.quantity > 0, "Minting zero qty"); minted[_req.uid] = true; } @@ -64,7 +67,7 @@ abstract contract SignatureMintERC20Upgradeable is Initializable, EIP712Upgradea _req.to, _req.primarySaleRecipient, _req.quantity, - _req.pricePerToken, + _req.price, _req.currency, _req.validityStartTimestamp, _req.validityEndTimestamp, diff --git a/contracts/extension/SignatureMintERC721.sol b/contracts/extension/SignatureMintERC721.sol index db1b726fc..2db421f8f 100644 --- a/contracts/extension/SignatureMintERC721.sol +++ b/contracts/extension/SignatureMintERC721.sol @@ -1,10 +1,27 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + import "./interface/ISignatureMintERC721.sol"; -import "../openzeppelin-presets/utils/cryptography/EIP712.sol"; +import "../external-deps/openzeppelin/utils/cryptography/EIP712.sol"; abstract contract SignatureMintERC721 is EIP712, ISignatureMintERC721 { + /// @dev The sender is not authorized to perform the action + error SignatureMintUnauthorized(); + + /// @dev The signer is not authorized to perform the signing action + error SignatureMintInvalidSigner(); + + /// @dev The signature is either expired or not ready to be claimed yet + error SignatureMintInvalidTime(uint256 startTime, uint256 endTime, uint256 actualTime); + + /// @dev Invalid mint recipient + error SignatureMintInvalidRecipient(); + + /// @dev Invalid mint quantity + error SignatureMintInvalidQuantity(); + using ECDSA for bytes32; bytes32 private constant TYPEHASH = @@ -18,12 +35,10 @@ abstract contract SignatureMintERC721 is EIP712, ISignatureMintERC721 { constructor() EIP712("SignatureMintERC721", "1") {} /// @dev Verifies that a mint request is signed by an authorized account. - function verify(MintRequest calldata _req, bytes calldata _signature) - public - view - override - returns (bool success, address signer) - { + function verify( + MintRequest calldata _req, + bytes calldata _signature + ) public view override returns (bool success, address signer) { signer = _recoverAddress(_req, _signature); success = !minted[_req.uid] && _canSignMintRequest(signer); } @@ -37,11 +52,19 @@ abstract contract SignatureMintERC721 is EIP712, ISignatureMintERC721 { (success, signer) = verify(_req, _signature); if (!success) { - revert("Invalid req"); + revert SignatureMintInvalidSigner(); } if (_req.validityStartTimestamp > block.timestamp || block.timestamp > _req.validityEndTimestamp) { - revert("Req expired"); + revert SignatureMintInvalidTime(_req.validityStartTimestamp, _req.validityEndTimestamp, block.timestamp); + } + + if (_req.to == address(0)) { + revert SignatureMintInvalidRecipient(); + } + + if (_req.quantity == 0) { + revert SignatureMintInvalidQuantity(); } minted[_req.uid] = true; diff --git a/contracts/extension/SignatureMintERC721Upgradeable.sol b/contracts/extension/SignatureMintERC721Upgradeable.sol index 5935ac1c6..935cd749a 100644 --- a/contracts/extension/SignatureMintERC721Upgradeable.sol +++ b/contracts/extension/SignatureMintERC721Upgradeable.sol @@ -1,12 +1,29 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + import "./interface/ISignatureMintERC721.sol"; import "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol"; abstract contract SignatureMintERC721Upgradeable is Initializable, EIP712Upgradeable, ISignatureMintERC721 { + /// @dev The sender is not authorized to perform the action + error SignatureMintUnauthorized(); + + /// @dev The signer is not authorized to perform the signing action + error SignatureMintInvalidSigner(); + + /// @dev The signature is either expired or not ready to be claimed yet + error SignatureMintInvalidTime(uint256 startTime, uint256 endTime, uint256 actualTime); + + /// @dev Invalid mint recipient + error SignatureMintInvalidRecipient(); + + /// @dev Invalid mint quantity + error SignatureMintInvalidQuantity(); + using ECDSAUpgradeable for bytes32; bytes32 private constant TYPEHASH = @@ -24,12 +41,10 @@ abstract contract SignatureMintERC721Upgradeable is Initializable, EIP712Upgrade function __SignatureMintERC721_init_unchained() internal onlyInitializing {} /// @dev Verifies that a mint request is signed by an account holding MINTER_ROLE (at the time of the function call). - function verify(MintRequest calldata _req, bytes calldata _signature) - public - view - override - returns (bool success, address signer) - { + function verify( + MintRequest calldata _req, + bytes calldata _signature + ) public view override returns (bool success, address signer) { signer = _recoverAddress(_req, _signature); success = !minted[_req.uid] && _isAuthorizedSigner(signer); } @@ -43,11 +58,19 @@ abstract contract SignatureMintERC721Upgradeable is Initializable, EIP712Upgrade (success, signer) = verify(_req, _signature); if (!success) { - revert("Invalid req"); + revert SignatureMintInvalidSigner(); } if (_req.validityStartTimestamp > block.timestamp || block.timestamp > _req.validityEndTimestamp) { - revert("Req expired"); + revert SignatureMintInvalidTime(_req.validityStartTimestamp, _req.validityEndTimestamp, block.timestamp); + } + + if (_req.to == address(0)) { + revert SignatureMintInvalidRecipient(); + } + + if (_req.quantity == 0) { + revert SignatureMintInvalidQuantity(); } minted[_req.uid] = true; diff --git a/contracts/extension/SoulboundERC721A.sol b/contracts/extension/SoulboundERC721A.sol new file mode 100644 index 000000000..7c8eba553 --- /dev/null +++ b/contracts/extension/SoulboundERC721A.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./PermissionsEnumerable.sol"; + +/** + * The `SoulboundERC721A` extension smart contract is meant to be used with ERC721A contracts as its base. It + * provides the appropriate `before transfer` hook for ERC721A, where it checks whether a given transfer is + * valid to go through or not. + * + * This contract uses the `Permissions` extension, and creates a role 'TRANSFER_ROLE'. + * - If `address(0)` holds the transfer role, then all transfers go through. + * - Else, a transfer goes through only if either the sender or recipient holds the transfe role. + */ + +abstract contract SoulboundERC721A is PermissionsEnumerable { + /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. + bytes32 public constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE"); + + event TransfersRestricted(bool isRestricted); + + /** + * @notice Restrict transfers of NFTs. + * @dev Restricting transfers means revoking the TRANSFER_ROLE from address(0). Making + * transfers unrestricted means granting the TRANSFER_ROLE to address(0). + * + * @param _toRestrict Whether to restrict transfers or not. + */ + function restrictTransfers(bool _toRestrict) public virtual { + if (_toRestrict) { + _revokeRole(TRANSFER_ROLE, address(0)); + } else { + _setupRole(TRANSFER_ROLE, address(0)); + } + } + + /// @dev Returns whether transfers can be restricted in a given execution context. + function _canRestrictTransfers() internal view virtual returns (bool); + + /// @dev See {ERC721A-_beforeTokenTransfers}. + function _beforeTokenTransfers(address from, address to, uint256, uint256) internal virtual { + // If transfers are restricted on the contract, we still want to allow burning and minting. + if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { + if (!hasRole(TRANSFER_ROLE, from) && !hasRole(TRANSFER_ROLE, to)) { + revert("!TRANSFER_ROLE"); + } + } + } +} diff --git a/contracts/extension/Staking1155.sol b/contracts/extension/Staking1155.sol new file mode 100644 index 000000000..d30885b12 --- /dev/null +++ b/contracts/extension/Staking1155.sol @@ -0,0 +1,516 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +import "../external-deps/openzeppelin/security/ReentrancyGuard.sol"; +import "../external-deps/openzeppelin/utils/math/SafeMath.sol"; +import "../eip/interface/IERC1155.sol"; + +import "./interface/IStaking1155.sol"; + +abstract contract Staking1155 is ReentrancyGuard, IStaking1155 { + /*/////////////////////////////////////////////////////////////// + State variables / Mappings + //////////////////////////////////////////////////////////////*/ + + ///@dev Address of ERC1155 contract -- staked tokens belong to this contract. + address public immutable stakingToken; + + /// @dev Flag to check direct transfers of staking tokens. + uint8 internal isStaking = 1; + + ///@dev Next staking condition Id. Tracks number of conditon updates so far. + uint64 private nextDefaultConditionId; + + ///@dev List of token-ids ever staked. + uint256[] public indexedTokens; + + ///@dev Mapping from token-id to whether it is indexed or not. + mapping(uint256 => bool) public isIndexed; + + ///@dev Mapping from default condition-id to default condition. + mapping(uint64 => StakingCondition) private defaultCondition; + + ///@dev Mapping from token-id to next staking condition Id for the token. Tracks number of conditon updates so far. + mapping(uint256 => uint64) private nextConditionId; + + ///@dev Mapping from token-id and staker address to Staker struct. See {struct IStaking1155.Staker}. + mapping(uint256 => mapping(address => Staker)) public stakers; + + ///@dev Mapping from token-id and condition Id to staking condition. See {struct IStaking1155.StakingCondition} + mapping(uint256 => mapping(uint64 => StakingCondition)) private stakingConditions; + + /// @dev Mapping from token-id to list of accounts that have staked that token-id. + mapping(uint256 => address[]) public stakersArray; + + constructor(address _stakingToken) ReentrancyGuard() { + require(address(_stakingToken) != address(0), "address 0"); + stakingToken = _stakingToken; + } + + /*/////////////////////////////////////////////////////////////// + External/Public Functions + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Stake ERC721 Tokens. + * + * @dev See {_stake}. Override that to implement custom logic. + * + * @param _tokenId ERC1155 token-id to stake. + * @param _amount Amount to stake. + */ + function stake(uint256 _tokenId, uint64 _amount) external nonReentrant { + _stake(_tokenId, _amount); + } + + /** + * @notice Withdraw staked tokens. + * + * @dev See {_withdraw}. Override that to implement custom logic. + * + * @param _tokenId ERC1155 token-id to withdraw. + * @param _amount Amount to withdraw. + */ + function withdraw(uint256 _tokenId, uint64 _amount) external nonReentrant { + _withdraw(_tokenId, _amount); + } + + /** + * @notice Claim accumulated rewards. + * + * @dev See {_claimRewards}. Override that to implement custom logic. + * See {_calculateRewards} for reward-calculation logic. + * + * @param _tokenId Staked token Id. + */ + function claimRewards(uint256 _tokenId) external nonReentrant { + _claimRewards(_tokenId); + } + + /** + * @notice Set time unit. Set as a number of seconds. + * Could be specified as -- x * 1 hours, x * 1 days, etc. + * + * @dev Only admin/authorized-account can call it. + * + * + * @param _tokenId ERC1155 token Id. + * @param _timeUnit New time unit. + */ + function setTimeUnit(uint256 _tokenId, uint80 _timeUnit) external virtual { + if (!_canSetStakeConditions()) { + revert("Not authorized"); + } + + uint64 _nextConditionId = nextConditionId[_tokenId]; + StakingCondition memory condition = _nextConditionId == 0 + ? defaultCondition[nextDefaultConditionId - 1] + : stakingConditions[_tokenId][_nextConditionId - 1]; + require(_timeUnit != condition.timeUnit, "Time-unit unchanged."); + + _setStakingCondition(_tokenId, _timeUnit, condition.rewardsPerUnitTime); + + emit UpdatedTimeUnit(_tokenId, condition.timeUnit, _timeUnit); + } + + /** + * @notice Set rewards per unit of time. + * Interpreted as x rewards per second/per day/etc based on time-unit. + * + * @dev Only admin/authorized-account can call it. + * + * + * @param _tokenId ERC1155 token Id. + * @param _rewardsPerUnitTime New rewards per unit time. + */ + function setRewardsPerUnitTime(uint256 _tokenId, uint256 _rewardsPerUnitTime) external virtual { + if (!_canSetStakeConditions()) { + revert("Not authorized"); + } + + uint64 _nextConditionId = nextConditionId[_tokenId]; + StakingCondition memory condition = _nextConditionId == 0 + ? defaultCondition[nextDefaultConditionId - 1] + : stakingConditions[_tokenId][_nextConditionId - 1]; + require(_rewardsPerUnitTime != condition.rewardsPerUnitTime, "Reward unchanged."); + + _setStakingCondition(_tokenId, condition.timeUnit, _rewardsPerUnitTime); + + emit UpdatedRewardsPerUnitTime(_tokenId, condition.rewardsPerUnitTime, _rewardsPerUnitTime); + } + + /** + * @notice Set time unit. Set as a number of seconds. + * Could be specified as -- x * 1 hours, x * 1 days, etc. + * + * @dev Only admin/authorized-account can call it. + * + * @param _defaultTimeUnit New time unit. + */ + function setDefaultTimeUnit(uint80 _defaultTimeUnit) external virtual { + if (!_canSetStakeConditions()) { + revert("Not authorized"); + } + + StakingCondition memory _defaultCondition = defaultCondition[nextDefaultConditionId - 1]; + require(_defaultTimeUnit != _defaultCondition.timeUnit, "Default time-unit unchanged."); + + _setDefaultStakingCondition(_defaultTimeUnit, _defaultCondition.rewardsPerUnitTime); + + emit UpdatedDefaultTimeUnit(_defaultCondition.timeUnit, _defaultTimeUnit); + } + + /** + * @notice Set rewards per unit of time. + * Interpreted as x rewards per second/per day/etc based on time-unit. + * + * @dev Only admin/authorized-account can call it. + * + * @param _defaultRewardsPerUnitTime New rewards per unit time. + */ + function setDefaultRewardsPerUnitTime(uint256 _defaultRewardsPerUnitTime) external virtual { + if (!_canSetStakeConditions()) { + revert("Not authorized"); + } + + StakingCondition memory _defaultCondition = defaultCondition[nextDefaultConditionId - 1]; + require(_defaultRewardsPerUnitTime != _defaultCondition.rewardsPerUnitTime, "Default reward unchanged."); + + _setDefaultStakingCondition(_defaultCondition.timeUnit, _defaultRewardsPerUnitTime); + + emit UpdatedDefaultRewardsPerUnitTime(_defaultCondition.rewardsPerUnitTime, _defaultRewardsPerUnitTime); + } + + /** + * @notice View amount staked and rewards for a user, for a given token-id. + * + * @param _staker Address for which to calculated rewards. + * @return _tokensStaked Amount of tokens staked for given token-id. + * @return _rewards Available reward amount. + */ + function getStakeInfoForToken( + uint256 _tokenId, + address _staker + ) external view virtual returns (uint256 _tokensStaked, uint256 _rewards) { + _tokensStaked = stakers[_tokenId][_staker].amountStaked; + _rewards = _availableRewards(_tokenId, _staker); + } + + /** + * @notice View all tokens staked and total rewards for a user. + * + * @param _staker Address for which to calculated rewards. + * @return _tokensStaked List of token-ids staked. + * @return _tokenAmounts Amount of each token-id staked. + * @return _totalRewards Total rewards available. + */ + function getStakeInfo( + address _staker + ) + external + view + virtual + returns (uint256[] memory _tokensStaked, uint256[] memory _tokenAmounts, uint256 _totalRewards) + { + uint256[] memory _indexedTokens = indexedTokens; + uint256[] memory _stakedAmounts = new uint256[](_indexedTokens.length); + uint256 indexedTokenCount = _indexedTokens.length; + uint256 stakerTokenCount = 0; + + for (uint256 i = 0; i < indexedTokenCount; i++) { + _stakedAmounts[i] = stakers[_indexedTokens[i]][_staker].amountStaked; + if (_stakedAmounts[i] > 0) stakerTokenCount += 1; + } + + _tokensStaked = new uint256[](stakerTokenCount); + _tokenAmounts = new uint256[](stakerTokenCount); + uint256 count = 0; + for (uint256 i = 0; i < indexedTokenCount; i++) { + if (_stakedAmounts[i] > 0) { + _tokensStaked[count] = _indexedTokens[i]; + _tokenAmounts[count] = _stakedAmounts[i]; + _totalRewards += _availableRewards(_indexedTokens[i], _staker); + count += 1; + } + } + } + + function getTimeUnit(uint256 _tokenId) public view returns (uint256 _timeUnit) { + uint64 _nextConditionId = nextConditionId[_tokenId]; + require(_nextConditionId != 0, "Time unit not set. Check default time unit."); + _timeUnit = stakingConditions[_tokenId][_nextConditionId - 1].timeUnit; + } + + function getRewardsPerUnitTime(uint256 _tokenId) public view returns (uint256 _rewardsPerUnitTime) { + uint64 _nextConditionId = nextConditionId[_tokenId]; + require(_nextConditionId != 0, "Rewards not set. Check default rewards."); + _rewardsPerUnitTime = stakingConditions[_tokenId][_nextConditionId - 1].rewardsPerUnitTime; + } + + function getDefaultTimeUnit() public view returns (uint256 _timeUnit) { + _timeUnit = defaultCondition[nextDefaultConditionId - 1].timeUnit; + } + + function getDefaultRewardsPerUnitTime() public view returns (uint256 _rewardsPerUnitTime) { + _rewardsPerUnitTime = defaultCondition[nextDefaultConditionId - 1].rewardsPerUnitTime; + } + + /*/////////////////////////////////////////////////////////////// + Internal Functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Staking logic. Override to add custom logic. + function _stake(uint256 _tokenId, uint64 _amount) internal virtual { + require(_amount != 0, "Staking 0 tokens"); + + if (stakers[_tokenId][_stakeMsgSender()].amountStaked > 0) { + _updateUnclaimedRewardsForStaker(_tokenId, _stakeMsgSender()); + } else { + stakersArray[_tokenId].push(_stakeMsgSender()); + stakers[_tokenId][_stakeMsgSender()].timeOfLastUpdate = uint80(block.timestamp); + + uint64 _conditionId = nextConditionId[_tokenId]; + unchecked { + stakers[_tokenId][_stakeMsgSender()].conditionIdOflastUpdate = _conditionId == 0 + ? nextDefaultConditionId - 1 + : _conditionId - 1; + } + } + + isStaking = 2; + IERC1155(stakingToken).safeTransferFrom(_stakeMsgSender(), address(this), _tokenId, _amount, ""); + isStaking = 1; + // stakerAddress[_tokenIds[i]] = _stakeMsgSender(); + stakers[_tokenId][_stakeMsgSender()].amountStaked += _amount; + + if (!isIndexed[_tokenId]) { + isIndexed[_tokenId] = true; + indexedTokens.push(_tokenId); + } + + emit TokensStaked(_stakeMsgSender(), _tokenId, _amount); + } + + /// @dev Withdraw logic. Override to add custom logic. + function _withdraw(uint256 _tokenId, uint64 _amount) internal virtual { + uint256 _amountStaked = stakers[_tokenId][_stakeMsgSender()].amountStaked; + require(_amount != 0, "Withdrawing 0 tokens"); + require(_amountStaked >= _amount, "Withdrawing more than staked"); + + _updateUnclaimedRewardsForStaker(_tokenId, _stakeMsgSender()); + + if (_amountStaked == _amount) { + address[] memory _stakersArray = stakersArray[_tokenId]; + for (uint256 i = 0; i < _stakersArray.length; ++i) { + if (_stakersArray[i] == _stakeMsgSender()) { + stakersArray[_tokenId][i] = _stakersArray[_stakersArray.length - 1]; + stakersArray[_tokenId].pop(); + break; + } + } + } + stakers[_tokenId][_stakeMsgSender()].amountStaked -= _amount; + + IERC1155(stakingToken).safeTransferFrom(address(this), _stakeMsgSender(), _tokenId, _amount, ""); + + emit TokensWithdrawn(_stakeMsgSender(), _tokenId, _amount); + } + + /// @dev Logic for claiming rewards. Override to add custom logic. + function _claimRewards(uint256 _tokenId) internal virtual { + uint256 rewards = stakers[_tokenId][_stakeMsgSender()].unclaimedRewards + + _calculateRewards(_tokenId, _stakeMsgSender()); + + require(rewards != 0, "No rewards"); + + stakers[_tokenId][_stakeMsgSender()].timeOfLastUpdate = uint80(block.timestamp); + stakers[_tokenId][_stakeMsgSender()].unclaimedRewards = 0; + + uint64 _conditionId = nextConditionId[_tokenId]; + + unchecked { + stakers[_tokenId][_stakeMsgSender()].conditionIdOflastUpdate = _conditionId == 0 + ? nextDefaultConditionId - 1 + : _conditionId - 1; + } + + _mintRewards(_stakeMsgSender(), rewards); + + emit RewardsClaimed(_stakeMsgSender(), rewards); + } + + /// @dev View available rewards for a user. + function _availableRewards(uint256 _tokenId, address _user) internal view virtual returns (uint256 _rewards) { + if (stakers[_tokenId][_user].amountStaked == 0) { + _rewards = stakers[_tokenId][_user].unclaimedRewards; + } else { + _rewards = stakers[_tokenId][_user].unclaimedRewards + _calculateRewards(_tokenId, _user); + } + } + + /// @dev Update unclaimed rewards for a users. Called for every state change for a user. + function _updateUnclaimedRewardsForStaker(uint256 _tokenId, address _staker) internal virtual { + uint256 rewards = _calculateRewards(_tokenId, _staker); + stakers[_tokenId][_staker].unclaimedRewards += rewards; + stakers[_tokenId][_staker].timeOfLastUpdate = uint80(block.timestamp); + + uint64 _conditionId = nextConditionId[_tokenId]; + unchecked { + stakers[_tokenId][_staker].conditionIdOflastUpdate = _conditionId == 0 + ? nextDefaultConditionId - 1 + : _conditionId - 1; + } + } + + /// @dev Set staking conditions, for a token-Id. + function _setStakingCondition(uint256 _tokenId, uint80 _timeUnit, uint256 _rewardsPerUnitTime) internal virtual { + require(_timeUnit != 0, "time-unit can't be 0"); + uint64 conditionId = nextConditionId[_tokenId]; + + if (conditionId == 0) { + uint64 _nextDefaultConditionId = nextDefaultConditionId; + for (; conditionId < _nextDefaultConditionId; conditionId += 1) { + StakingCondition memory _defaultCondition = defaultCondition[conditionId]; + + stakingConditions[_tokenId][conditionId] = StakingCondition({ + timeUnit: _defaultCondition.timeUnit, + rewardsPerUnitTime: _defaultCondition.rewardsPerUnitTime, + startTimestamp: _defaultCondition.startTimestamp, + endTimestamp: _defaultCondition.endTimestamp + }); + } + } + + stakingConditions[_tokenId][conditionId - 1].endTimestamp = uint80(block.timestamp); + + stakingConditions[_tokenId][conditionId] = StakingCondition({ + timeUnit: _timeUnit, + rewardsPerUnitTime: _rewardsPerUnitTime, + startTimestamp: uint80(block.timestamp), + endTimestamp: 0 + }); + + nextConditionId[_tokenId] = conditionId + 1; + } + + /// @dev Set default staking conditions. + function _setDefaultStakingCondition(uint80 _timeUnit, uint256 _rewardsPerUnitTime) internal virtual { + require(_timeUnit != 0, "time-unit can't be 0"); + uint64 conditionId = nextDefaultConditionId; + nextDefaultConditionId += 1; + + defaultCondition[conditionId] = StakingCondition({ + timeUnit: _timeUnit, + rewardsPerUnitTime: _rewardsPerUnitTime, + startTimestamp: uint80(block.timestamp), + endTimestamp: 0 + }); + + if (conditionId > 0) { + defaultCondition[conditionId - 1].endTimestamp = uint80(block.timestamp); + } + } + + /// @dev Reward calculation logic. Override to implement custom logic. + function _calculateRewards(uint256 _tokenId, address _staker) internal view virtual returns (uint256 _rewards) { + Staker memory staker = stakers[_tokenId][_staker]; + uint64 _stakerConditionId = staker.conditionIdOflastUpdate; + uint64 _nextConditionId = nextConditionId[_tokenId]; + + if (_nextConditionId == 0) { + _nextConditionId = nextDefaultConditionId; + + for (uint64 i = _stakerConditionId; i < _nextConditionId; i += 1) { + StakingCondition memory condition = defaultCondition[i]; + + uint256 startTime = i != _stakerConditionId ? condition.startTimestamp : staker.timeOfLastUpdate; + uint256 endTime = condition.endTimestamp != 0 ? condition.endTimestamp : block.timestamp; + + (bool noOverflowProduct, uint256 rewardsProduct) = SafeMath.tryMul( + (endTime - startTime) * staker.amountStaked, + condition.rewardsPerUnitTime + ); + (bool noOverflowSum, uint256 rewardsSum) = SafeMath.tryAdd( + _rewards, + rewardsProduct / condition.timeUnit + ); + + _rewards = noOverflowProduct && noOverflowSum ? rewardsSum : _rewards; + } + } else { + for (uint64 i = _stakerConditionId; i < _nextConditionId; i += 1) { + StakingCondition memory condition = stakingConditions[_tokenId][i]; + + uint256 startTime = i != _stakerConditionId ? condition.startTimestamp : staker.timeOfLastUpdate; + uint256 endTime = condition.endTimestamp != 0 ? condition.endTimestamp : block.timestamp; + + (bool noOverflowProduct, uint256 rewardsProduct) = SafeMath.tryMul( + (endTime - startTime) * staker.amountStaked, + condition.rewardsPerUnitTime + ); + (bool noOverflowSum, uint256 rewardsSum) = SafeMath.tryAdd( + _rewards, + rewardsProduct / condition.timeUnit + ); + + _rewards = noOverflowProduct && noOverflowSum ? rewardsSum : _rewards; + } + } + } + + /*//////////////////////////////////////////////////////////////////// + Optional hooks that can be implemented in the derived contract + ///////////////////////////////////////////////////////////////////*/ + + /// @dev Exposes the ability to override the msg sender -- support ERC2771. + function _stakeMsgSender() internal virtual returns (address) { + return msg.sender; + } + + /*/////////////////////////////////////////////////////////////// + Virtual functions to be implemented in derived contract + //////////////////////////////////////////////////////////////*/ + + /** + * @notice View total rewards available in the staking contract. + * + */ + function getRewardTokenBalance() external view virtual returns (uint256 _rewardsAvailableInContract); + + /** + * @dev Mint/Transfer ERC20 rewards to the staker. Must override. + * + * @param _staker Address for which to calculated rewards. + * @param _rewards Amount of tokens to be given out as reward. + * + * For example, override as below to mint ERC20 rewards: + * + * ``` + * function _mintRewards(address _staker, uint256 _rewards) internal override { + * + * TokenERC20(rewardTokenAddress).mintTo(_staker, _rewards); + * + * } + * ``` + */ + function _mintRewards(address _staker, uint256 _rewards) internal virtual; + + /** + * @dev Returns whether staking restrictions can be set in given execution context. + * Must override. + * + * + * For example, override as below to restrict access to admin: + * + * ``` + * function _canSetStakeConditions() internal override { + * + * return msg.sender == adminAddress; + * + * } + * ``` + */ + function _canSetStakeConditions() internal view virtual returns (bool); +} diff --git a/contracts/extension/Staking1155Upgradeable.sol b/contracts/extension/Staking1155Upgradeable.sol new file mode 100644 index 000000000..31d967efa --- /dev/null +++ b/contracts/extension/Staking1155Upgradeable.sol @@ -0,0 +1,516 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "../external-deps/openzeppelin/utils/math/SafeMath.sol"; +import "../eip/interface/IERC1155.sol"; + +import "./interface/IStaking1155.sol"; + +abstract contract Staking1155Upgradeable is ReentrancyGuardUpgradeable, IStaking1155 { + /*/////////////////////////////////////////////////////////////// + State variables / Mappings + //////////////////////////////////////////////////////////////*/ + + ///@dev Address of ERC1155 contract -- staked tokens belong to this contract. + address public stakingToken; + + /// @dev Flag to check direct transfers of staking tokens. + uint8 internal isStaking = 1; + + ///@dev Next staking condition Id. Tracks number of conditon updates so far. + uint64 private nextDefaultConditionId; + + ///@dev List of token-ids ever staked. + uint256[] public indexedTokens; + + ///@dev Mapping from token-id to whether it is indexed or not. + mapping(uint256 => bool) public isIndexed; + + ///@dev Mapping from default condition-id to default condition. + mapping(uint64 => StakingCondition) private defaultCondition; + + ///@dev Mapping from token-id to next staking condition Id for the token. Tracks number of conditon updates so far. + mapping(uint256 => uint64) private nextConditionId; + + ///@dev Mapping from token-id and staker address to Staker struct. See {struct IStaking1155.Staker}. + mapping(uint256 => mapping(address => Staker)) public stakers; + + ///@dev Mapping from token-id and condition Id to staking condition. See {struct IStaking1155.StakingCondition} + mapping(uint256 => mapping(uint64 => StakingCondition)) private stakingConditions; + + /// @dev Mapping from token-id to list of accounts that have staked that token-id. + mapping(uint256 => address[]) public stakersArray; + + function __Staking1155_init(address _stakingToken) internal onlyInitializing { + __ReentrancyGuard_init(); + + require(address(_stakingToken) != address(0), "address 0"); + stakingToken = _stakingToken; + } + + /*/////////////////////////////////////////////////////////////// + External/Public Functions + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Stake ERC721 Tokens. + * + * @dev See {_stake}. Override that to implement custom logic. + * + * @param _tokenId ERC1155 token-id to stake. + * @param _amount Amount to stake. + */ + function stake(uint256 _tokenId, uint64 _amount) external nonReentrant { + _stake(_tokenId, _amount); + } + + /** + * @notice Withdraw staked tokens. + * + * @dev See {_withdraw}. Override that to implement custom logic. + * + * @param _tokenId ERC1155 token-id to withdraw. + * @param _amount Amount to withdraw. + */ + function withdraw(uint256 _tokenId, uint64 _amount) external nonReentrant { + _withdraw(_tokenId, _amount); + } + + /** + * @notice Claim accumulated rewards. + * + * @dev See {_claimRewards}. Override that to implement custom logic. + * See {_calculateRewards} for reward-calculation logic. + * + * @param _tokenId Staked token Id. + */ + function claimRewards(uint256 _tokenId) external nonReentrant { + _claimRewards(_tokenId); + } + + /** + * @notice Set time unit. Set as a number of seconds. + * Could be specified as -- x * 1 hours, x * 1 days, etc. + * + * @dev Only admin/authorized-account can call it. + * + * + * @param _tokenId ERC1155 token Id. + * @param _timeUnit New time unit. + */ + function setTimeUnit(uint256 _tokenId, uint80 _timeUnit) external virtual { + if (!_canSetStakeConditions()) { + revert("Not authorized"); + } + + uint64 _nextConditionId = nextConditionId[_tokenId]; + StakingCondition memory condition = _nextConditionId == 0 + ? defaultCondition[nextDefaultConditionId - 1] + : stakingConditions[_tokenId][_nextConditionId - 1]; + require(_timeUnit != condition.timeUnit, "Time-unit unchanged."); + + _setStakingCondition(_tokenId, _timeUnit, condition.rewardsPerUnitTime); + + emit UpdatedTimeUnit(_tokenId, condition.timeUnit, _timeUnit); + } + + /** + * @notice Set rewards per unit of time. + * Interpreted as x rewards per second/per day/etc based on time-unit. + * + * @dev Only admin/authorized-account can call it. + * + * + * @param _tokenId ERC1155 token Id. + * @param _rewardsPerUnitTime New rewards per unit time. + */ + function setRewardsPerUnitTime(uint256 _tokenId, uint256 _rewardsPerUnitTime) external virtual { + if (!_canSetStakeConditions()) { + revert("Not authorized"); + } + + uint64 _nextConditionId = nextConditionId[_tokenId]; + StakingCondition memory condition = _nextConditionId == 0 + ? defaultCondition[nextDefaultConditionId - 1] + : stakingConditions[_tokenId][_nextConditionId - 1]; + require(_rewardsPerUnitTime != condition.rewardsPerUnitTime, "Reward unchanged."); + + _setStakingCondition(_tokenId, condition.timeUnit, _rewardsPerUnitTime); + + emit UpdatedRewardsPerUnitTime(_tokenId, condition.rewardsPerUnitTime, _rewardsPerUnitTime); + } + + /** + * @notice Set time unit. Set as a number of seconds. + * Could be specified as -- x * 1 hours, x * 1 days, etc. + * + * @dev Only admin/authorized-account can call it. + * + * @param _defaultTimeUnit New time unit. + */ + function setDefaultTimeUnit(uint80 _defaultTimeUnit) external virtual { + if (!_canSetStakeConditions()) { + revert("Not authorized"); + } + + StakingCondition memory _defaultCondition = defaultCondition[nextDefaultConditionId - 1]; + require(_defaultTimeUnit != _defaultCondition.timeUnit, "Default time-unit unchanged."); + + _setDefaultStakingCondition(_defaultTimeUnit, _defaultCondition.rewardsPerUnitTime); + + emit UpdatedDefaultTimeUnit(_defaultCondition.timeUnit, _defaultTimeUnit); + } + + /** + * @notice Set rewards per unit of time. + * Interpreted as x rewards per second/per day/etc based on time-unit. + * + * @dev Only admin/authorized-account can call it. + * + * @param _defaultRewardsPerUnitTime New rewards per unit time. + */ + function setDefaultRewardsPerUnitTime(uint256 _defaultRewardsPerUnitTime) external virtual { + if (!_canSetStakeConditions()) { + revert("Not authorized"); + } + + StakingCondition memory _defaultCondition = defaultCondition[nextDefaultConditionId - 1]; + require(_defaultRewardsPerUnitTime != _defaultCondition.rewardsPerUnitTime, "Default reward unchanged."); + + _setDefaultStakingCondition(_defaultCondition.timeUnit, _defaultRewardsPerUnitTime); + + emit UpdatedDefaultRewardsPerUnitTime(_defaultCondition.rewardsPerUnitTime, _defaultRewardsPerUnitTime); + } + + /** + * @notice View amount staked and rewards for a user, for a given token-id. + * + * @param _staker Address for which to calculated rewards. + * @return _tokensStaked Amount of tokens staked for given token-id. + * @return _rewards Available reward amount. + */ + function getStakeInfoForToken( + uint256 _tokenId, + address _staker + ) external view virtual returns (uint256 _tokensStaked, uint256 _rewards) { + _tokensStaked = stakers[_tokenId][_staker].amountStaked; + _rewards = _availableRewards(_tokenId, _staker); + } + + /** + * @notice View all tokens staked and total rewards for a user. + * + * @param _staker Address for which to calculated rewards. + * @return _tokensStaked List of token-ids staked. + * @return _tokenAmounts Amount of each token-id staked. + * @return _totalRewards Total rewards available. + */ + function getStakeInfo( + address _staker + ) + external + view + virtual + returns (uint256[] memory _tokensStaked, uint256[] memory _tokenAmounts, uint256 _totalRewards) + { + uint256[] memory _indexedTokens = indexedTokens; + uint256[] memory _stakedAmounts = new uint256[](_indexedTokens.length); + uint256 indexedTokenCount = _indexedTokens.length; + uint256 stakerTokenCount = 0; + + for (uint256 i = 0; i < indexedTokenCount; i++) { + _stakedAmounts[i] = stakers[_indexedTokens[i]][_staker].amountStaked; + if (_stakedAmounts[i] > 0) stakerTokenCount += 1; + } + + _tokensStaked = new uint256[](stakerTokenCount); + _tokenAmounts = new uint256[](stakerTokenCount); + uint256 count = 0; + for (uint256 i = 0; i < indexedTokenCount; i++) { + if (_stakedAmounts[i] > 0) { + _tokensStaked[count] = _indexedTokens[i]; + _tokenAmounts[count] = _stakedAmounts[i]; + _totalRewards += _availableRewards(_indexedTokens[i], _staker); + count += 1; + } + } + } + + function getTimeUnit(uint256 _tokenId) public view returns (uint256 _timeUnit) { + uint64 _nextConditionId = nextConditionId[_tokenId]; + require(_nextConditionId != 0, "Time unit not set. Check default time unit."); + _timeUnit = stakingConditions[_tokenId][_nextConditionId - 1].timeUnit; + } + + function getRewardsPerUnitTime(uint256 _tokenId) public view returns (uint256 _rewardsPerUnitTime) { + uint64 _nextConditionId = nextConditionId[_tokenId]; + require(_nextConditionId != 0, "Rewards not set. Check default rewards."); + _rewardsPerUnitTime = stakingConditions[_tokenId][_nextConditionId - 1].rewardsPerUnitTime; + } + + function getDefaultTimeUnit() public view returns (uint256 _timeUnit) { + _timeUnit = defaultCondition[nextDefaultConditionId - 1].timeUnit; + } + + function getDefaultRewardsPerUnitTime() public view returns (uint256 _rewardsPerUnitTime) { + _rewardsPerUnitTime = defaultCondition[nextDefaultConditionId - 1].rewardsPerUnitTime; + } + + /*/////////////////////////////////////////////////////////////// + Internal Functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Staking logic. Override to add custom logic. + function _stake(uint256 _tokenId, uint64 _amount) internal virtual { + require(_amount != 0, "Staking 0 tokens"); + + if (stakers[_tokenId][_stakeMsgSender()].amountStaked > 0) { + _updateUnclaimedRewardsForStaker(_tokenId, _stakeMsgSender()); + } else { + stakersArray[_tokenId].push(_stakeMsgSender()); + stakers[_tokenId][_stakeMsgSender()].timeOfLastUpdate = uint80(block.timestamp); + + uint64 _conditionId = nextConditionId[_tokenId]; + stakers[_tokenId][_stakeMsgSender()].conditionIdOflastUpdate = _conditionId == 0 + ? nextDefaultConditionId - 1 + : _conditionId - 1; + } + + isStaking = 2; + IERC1155(stakingToken).safeTransferFrom(_stakeMsgSender(), address(this), _tokenId, _amount, ""); + isStaking = 1; + // stakerAddress[_tokenIds[i]] = _stakeMsgSender(); + stakers[_tokenId][_stakeMsgSender()].amountStaked += _amount; + + if (!isIndexed[_tokenId]) { + isIndexed[_tokenId] = true; + indexedTokens.push(_tokenId); + } + + emit TokensStaked(_stakeMsgSender(), _tokenId, _amount); + } + + /// @dev Withdraw logic. Override to add custom logic. + function _withdraw(uint256 _tokenId, uint64 _amount) internal virtual { + uint256 _amountStaked = stakers[_tokenId][_stakeMsgSender()].amountStaked; + require(_amount != 0, "Withdrawing 0 tokens"); + require(_amountStaked >= _amount, "Withdrawing more than staked"); + + _updateUnclaimedRewardsForStaker(_tokenId, _stakeMsgSender()); + + if (_amountStaked == _amount) { + address[] memory _stakersArray = stakersArray[_tokenId]; + for (uint256 i = 0; i < _stakersArray.length; ++i) { + if (_stakersArray[i] == _stakeMsgSender()) { + stakersArray[_tokenId][i] = _stakersArray[_stakersArray.length - 1]; + stakersArray[_tokenId].pop(); + break; + } + } + } + + stakers[_tokenId][_stakeMsgSender()].amountStaked -= _amount; + + IERC1155(stakingToken).safeTransferFrom(address(this), _stakeMsgSender(), _tokenId, _amount, ""); + + emit TokensWithdrawn(_stakeMsgSender(), _tokenId, _amount); + } + + /// @dev Logic for claiming rewards. Override to add custom logic. + function _claimRewards(uint256 _tokenId) internal virtual { + uint256 rewards = stakers[_tokenId][_stakeMsgSender()].unclaimedRewards + + _calculateRewards(_tokenId, _stakeMsgSender()); + + require(rewards != 0, "No rewards"); + + stakers[_tokenId][_stakeMsgSender()].timeOfLastUpdate = uint80(block.timestamp); + stakers[_tokenId][_stakeMsgSender()].unclaimedRewards = 0; + + uint64 _conditionId = nextConditionId[_tokenId]; + unchecked { + stakers[_tokenId][_stakeMsgSender()].conditionIdOflastUpdate = _conditionId == 0 + ? nextDefaultConditionId - 1 + : _conditionId - 1; + } + + _mintRewards(_stakeMsgSender(), rewards); + + emit RewardsClaimed(_stakeMsgSender(), rewards); + } + + /// @dev View available rewards for a user. + function _availableRewards(uint256 _tokenId, address _user) internal view virtual returns (uint256 _rewards) { + if (stakers[_tokenId][_user].amountStaked == 0) { + _rewards = stakers[_tokenId][_user].unclaimedRewards; + } else { + _rewards = stakers[_tokenId][_user].unclaimedRewards + _calculateRewards(_tokenId, _user); + } + } + + /// @dev Update unclaimed rewards for a users. Called for every state change for a user. + function _updateUnclaimedRewardsForStaker(uint256 _tokenId, address _staker) internal virtual { + uint256 rewards = _calculateRewards(_tokenId, _staker); + stakers[_tokenId][_staker].unclaimedRewards += rewards; + stakers[_tokenId][_staker].timeOfLastUpdate = uint80(block.timestamp); + + uint64 _conditionId = nextConditionId[_tokenId]; + unchecked { + stakers[_tokenId][_staker].conditionIdOflastUpdate = _conditionId == 0 + ? nextDefaultConditionId - 1 + : _conditionId - 1; + } + } + + /// @dev Set staking conditions, for a token-Id. + function _setStakingCondition(uint256 _tokenId, uint80 _timeUnit, uint256 _rewardsPerUnitTime) internal virtual { + require(_timeUnit != 0, "time-unit can't be 0"); + uint64 conditionId = nextConditionId[_tokenId]; + + if (conditionId == 0) { + uint256 _nextDefaultConditionId = nextDefaultConditionId; + for (; conditionId < _nextDefaultConditionId; conditionId += 1) { + StakingCondition memory _defaultCondition = defaultCondition[conditionId]; + + stakingConditions[_tokenId][conditionId] = StakingCondition({ + timeUnit: _defaultCondition.timeUnit, + rewardsPerUnitTime: _defaultCondition.rewardsPerUnitTime, + startTimestamp: _defaultCondition.startTimestamp, + endTimestamp: _defaultCondition.endTimestamp + }); + } + } + + stakingConditions[_tokenId][conditionId - 1].endTimestamp = uint80(block.timestamp); + + stakingConditions[_tokenId][conditionId] = StakingCondition({ + timeUnit: _timeUnit, + rewardsPerUnitTime: _rewardsPerUnitTime, + startTimestamp: uint80(block.timestamp), + endTimestamp: 0 + }); + + nextConditionId[_tokenId] = conditionId + 1; + } + + /// @dev Set default staking conditions. + function _setDefaultStakingCondition(uint80 _timeUnit, uint256 _rewardsPerUnitTime) internal virtual { + require(_timeUnit != 0, "time-unit can't be 0"); + uint64 conditionId = nextDefaultConditionId; + nextDefaultConditionId += 1; + + defaultCondition[conditionId] = StakingCondition({ + timeUnit: _timeUnit, + rewardsPerUnitTime: _rewardsPerUnitTime, + startTimestamp: uint80(block.timestamp), + endTimestamp: 0 + }); + + if (conditionId > 0) { + defaultCondition[conditionId - 1].endTimestamp = uint80(block.timestamp); + } + } + + /// @dev Reward calculation logic. Override to implement custom logic. + function _calculateRewards(uint256 _tokenId, address _staker) internal view virtual returns (uint256 _rewards) { + Staker memory staker = stakers[_tokenId][_staker]; + uint64 _stakerConditionId = staker.conditionIdOflastUpdate; + uint64 _nextConditionId = nextConditionId[_tokenId]; + + if (_nextConditionId == 0) { + _nextConditionId = nextDefaultConditionId; + + for (uint64 i = _stakerConditionId; i < _nextConditionId; i += 1) { + StakingCondition memory condition = defaultCondition[i]; + + uint256 startTime = i != _stakerConditionId ? condition.startTimestamp : staker.timeOfLastUpdate; + uint256 endTime = condition.endTimestamp != 0 ? condition.endTimestamp : block.timestamp; + + (bool noOverflowProduct, uint256 rewardsProduct) = SafeMath.tryMul( + (endTime - startTime) * staker.amountStaked, + condition.rewardsPerUnitTime + ); + (bool noOverflowSum, uint256 rewardsSum) = SafeMath.tryAdd( + _rewards, + rewardsProduct / condition.timeUnit + ); + + _rewards = noOverflowProduct && noOverflowSum ? rewardsSum : _rewards; + } + } else { + for (uint64 i = _stakerConditionId; i < _nextConditionId; i += 1) { + StakingCondition memory condition = stakingConditions[_tokenId][i]; + + uint256 startTime = i != _stakerConditionId ? condition.startTimestamp : staker.timeOfLastUpdate; + uint256 endTime = condition.endTimestamp != 0 ? condition.endTimestamp : block.timestamp; + + (bool noOverflowProduct, uint256 rewardsProduct) = SafeMath.tryMul( + (endTime - startTime) * staker.amountStaked, + condition.rewardsPerUnitTime + ); + (bool noOverflowSum, uint256 rewardsSum) = SafeMath.tryAdd( + _rewards, + rewardsProduct / condition.timeUnit + ); + + _rewards = noOverflowProduct && noOverflowSum ? rewardsSum : _rewards; + } + } + } + + /*//////////////////////////////////////////////////////////////////// + Optional hooks that can be implemented in the derived contract + ///////////////////////////////////////////////////////////////////*/ + + /// @dev Exposes the ability to override the msg sender -- support ERC2771. + function _stakeMsgSender() internal virtual returns (address) { + return msg.sender; + } + + /*/////////////////////////////////////////////////////////////// + Virtual functions to be implemented in derived contract + //////////////////////////////////////////////////////////////*/ + + /** + * @notice View total rewards available in the staking contract. + * + */ + function getRewardTokenBalance() external view virtual returns (uint256 _rewardsAvailableInContract); + + /** + * @dev Mint/Transfer ERC20 rewards to the staker. Must override. + * + * @param _staker Address for which to calculated rewards. + * @param _rewards Amount of tokens to be given out as reward. + * + * For example, override as below to mint ERC20 rewards: + * + * ``` + * function _mintRewards(address _staker, uint256 _rewards) internal override { + * + * TokenERC20(rewardTokenAddress).mintTo(_staker, _rewards); + * + * } + * ``` + */ + function _mintRewards(address _staker, uint256 _rewards) internal virtual; + + /** + * @dev Returns whether staking restrictions can be set in given execution context. + * Must override. + * + * + * For example, override as below to restrict access to admin: + * + * ``` + * function _canSetStakeConditions() internal override { + * + * return msg.sender == adminAddress; + * + * } + * ``` + */ + function _canSetStakeConditions() internal view virtual returns (bool); +} diff --git a/contracts/extension/Staking20.sol b/contracts/extension/Staking20.sol new file mode 100644 index 000000000..ab766b7f4 --- /dev/null +++ b/contracts/extension/Staking20.sol @@ -0,0 +1,376 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +import "../external-deps/openzeppelin/security/ReentrancyGuard.sol"; +import "../external-deps/openzeppelin/utils/math/SafeMath.sol"; +import "../eip/interface/IERC20.sol"; +import { CurrencyTransferLib } from "../lib/CurrencyTransferLib.sol"; + +import "./interface/IStaking20.sol"; + +abstract contract Staking20 is ReentrancyGuard, IStaking20 { + /*/////////////////////////////////////////////////////////////// + State variables / Mappings + //////////////////////////////////////////////////////////////*/ + + /// @dev The address of the native token wrapper contract. + address internal immutable nativeTokenWrapper; + + ///@dev Address of ERC20 contract -- staked tokens belong to this contract. + address public immutable stakingToken; + + /// @dev Decimals of staking token. + uint16 public immutable stakingTokenDecimals; + + /// @dev Decimals of reward token. + uint16 public immutable rewardTokenDecimals; + + ///@dev Next staking condition Id. Tracks number of conditon updates so far. + uint64 private nextConditionId; + + /// @dev Total amount of tokens staked in the contract. + uint256 public stakingTokenBalance; + + /// @dev List of accounts that have staked that token-id. + address[] public stakersArray; + + ///@dev Mapping staker address to Staker struct. See {struct IStaking20.Staker}. + mapping(address => Staker) public stakers; + + ///@dev Mapping from condition Id to staking condition. See {struct IStaking721.StakingCondition} + mapping(uint256 => StakingCondition) private stakingConditions; + + constructor( + address _nativeTokenWrapper, + address _stakingToken, + uint16 _stakingTokenDecimals, + uint16 _rewardTokenDecimals + ) ReentrancyGuard() { + require(_stakingToken != address(0) && _nativeTokenWrapper != address(0), "address 0"); + require(_stakingTokenDecimals != 0 && _rewardTokenDecimals != 0, "decimals 0"); + + nativeTokenWrapper = _nativeTokenWrapper; + stakingToken = _stakingToken; + stakingTokenDecimals = _stakingTokenDecimals; + rewardTokenDecimals = _rewardTokenDecimals; + } + + /*/////////////////////////////////////////////////////////////// + External/Public Functions + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Stake ERC20 Tokens. + * + * @dev See {_stake}. Override that to implement custom logic. + * + * @param _amount Amount to stake. + */ + function stake(uint256 _amount) external payable nonReentrant { + _stake(_amount); + } + + /** + * @notice Withdraw staked ERC20 tokens. + * + * @dev See {_withdraw}. Override that to implement custom logic. + * + * @param _amount Amount to withdraw. + */ + function withdraw(uint256 _amount) external nonReentrant { + _withdraw(_amount); + } + + /** + * @notice Claim accumulated rewards. + * + * @dev See {_claimRewards}. Override that to implement custom logic. + * See {_calculateRewards} for reward-calculation logic. + */ + function claimRewards() external nonReentrant { + _claimRewards(); + } + + /** + * @notice Set time unit. Set as a number of seconds. + * Could be specified as -- x * 1 hours, x * 1 days, etc. + * + * @dev Only admin/authorized-account can call it. + * + * @param _timeUnit New time unit. + */ + function setTimeUnit(uint80 _timeUnit) external virtual { + if (!_canSetStakeConditions()) { + revert("Not authorized"); + } + + StakingCondition memory condition = stakingConditions[nextConditionId - 1]; + require(_timeUnit != condition.timeUnit, "Time-unit unchanged."); + + _setStakingCondition(_timeUnit, condition.rewardRatioNumerator, condition.rewardRatioDenominator); + + emit UpdatedTimeUnit(condition.timeUnit, _timeUnit); + } + + /** + * @notice Set rewards per unit of time. + * Interpreted as (numerator/denominator) rewards per second/per day/etc based on time-unit. + * + * For e.g., ratio of 1/20 would mean 1 reward token for every 20 tokens staked. + * + * @dev Only admin/authorized-account can call it. + * + * @param _numerator Reward ratio numerator. + * @param _denominator Reward ratio denominator. + */ + function setRewardRatio(uint256 _numerator, uint256 _denominator) external virtual { + if (!_canSetStakeConditions()) { + revert("Not authorized"); + } + + StakingCondition memory condition = stakingConditions[nextConditionId - 1]; + require( + _numerator != condition.rewardRatioNumerator || _denominator != condition.rewardRatioDenominator, + "Reward ratio unchanged." + ); + _setStakingCondition(condition.timeUnit, _numerator, _denominator); + + emit UpdatedRewardRatio( + condition.rewardRatioNumerator, + _numerator, + condition.rewardRatioDenominator, + _denominator + ); + } + + /** + * @notice View amount staked and rewards for a user. + * + * @param _staker Address for which to calculated rewards. + * @return _tokensStaked Amount of tokens staked. + * @return _rewards Available reward amount. + */ + function getStakeInfo(address _staker) external view virtual returns (uint256 _tokensStaked, uint256 _rewards) { + _tokensStaked = stakers[_staker].amountStaked; + _rewards = _availableRewards(_staker); + } + + function getTimeUnit() public view returns (uint256 _timeUnit) { + _timeUnit = stakingConditions[nextConditionId - 1].timeUnit; + } + + function getRewardRatio() public view returns (uint256 _numerator, uint256 _denominator) { + _numerator = stakingConditions[nextConditionId - 1].rewardRatioNumerator; + _denominator = stakingConditions[nextConditionId - 1].rewardRatioDenominator; + } + + /*/////////////////////////////////////////////////////////////// + Internal Functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Staking logic. Override to add custom logic. + function _stake(uint256 _amount) internal virtual { + require(_amount != 0, "Staking 0 tokens"); + + address _stakingToken; + if (stakingToken == CurrencyTransferLib.NATIVE_TOKEN) { + _stakingToken = nativeTokenWrapper; + } else { + require(msg.value == 0, "Value not 0"); + _stakingToken = stakingToken; + } + + if (stakers[_stakeMsgSender()].amountStaked > 0) { + _updateUnclaimedRewardsForStaker(_stakeMsgSender()); + } else { + stakersArray.push(_stakeMsgSender()); + stakers[_stakeMsgSender()].timeOfLastUpdate = uint80(block.timestamp); + stakers[_stakeMsgSender()].conditionIdOflastUpdate = nextConditionId - 1; + } + + uint256 balanceBefore = IERC20(_stakingToken).balanceOf(address(this)); + CurrencyTransferLib.transferCurrencyWithWrapper( + stakingToken, + _stakeMsgSender(), + address(this), + _amount, + nativeTokenWrapper + ); + uint256 actualAmount = IERC20(_stakingToken).balanceOf(address(this)) - balanceBefore; + + stakers[_stakeMsgSender()].amountStaked += actualAmount; + stakingTokenBalance += actualAmount; + + emit TokensStaked(_stakeMsgSender(), actualAmount); + } + + /// @dev Withdraw logic. Override to add custom logic. + function _withdraw(uint256 _amount) internal virtual { + uint256 _amountStaked = stakers[_stakeMsgSender()].amountStaked; + require(_amount != 0, "Withdrawing 0 tokens"); + require(_amountStaked >= _amount, "Withdrawing more than staked"); + + _updateUnclaimedRewardsForStaker(_stakeMsgSender()); + + if (_amountStaked == _amount) { + address[] memory _stakersArray = stakersArray; + for (uint256 i = 0; i < _stakersArray.length; ++i) { + if (_stakersArray[i] == _stakeMsgSender()) { + stakersArray[i] = _stakersArray[_stakersArray.length - 1]; + stakersArray.pop(); + break; + } + } + } + stakers[_stakeMsgSender()].amountStaked -= _amount; + stakingTokenBalance -= _amount; + + CurrencyTransferLib.transferCurrencyWithWrapper( + stakingToken, + address(this), + _stakeMsgSender(), + _amount, + nativeTokenWrapper + ); + + emit TokensWithdrawn(_stakeMsgSender(), _amount); + } + + /// @dev Logic for claiming rewards. Override to add custom logic. + function _claimRewards() internal virtual { + uint256 rewards = stakers[_stakeMsgSender()].unclaimedRewards + _calculateRewards(_stakeMsgSender()); + + require(rewards != 0, "No rewards"); + + stakers[_stakeMsgSender()].timeOfLastUpdate = uint80(block.timestamp); + stakers[_stakeMsgSender()].unclaimedRewards = 0; + stakers[_stakeMsgSender()].conditionIdOflastUpdate = nextConditionId - 1; + + _mintRewards(_stakeMsgSender(), rewards); + + emit RewardsClaimed(_stakeMsgSender(), rewards); + } + + /// @dev View available rewards for a user. + function _availableRewards(address _staker) internal view virtual returns (uint256 _rewards) { + if (stakers[_staker].amountStaked == 0) { + _rewards = stakers[_staker].unclaimedRewards; + } else { + _rewards = stakers[_staker].unclaimedRewards + _calculateRewards(_staker); + } + } + + /// @dev Update unclaimed rewards for a users. Called for every state change for a user. + function _updateUnclaimedRewardsForStaker(address _staker) internal virtual { + uint256 rewards = _calculateRewards(_staker); + stakers[_staker].unclaimedRewards += rewards; + stakers[_staker].timeOfLastUpdate = uint80(block.timestamp); + stakers[_staker].conditionIdOflastUpdate = nextConditionId - 1; + } + + /// @dev Set staking conditions. + function _setStakingCondition(uint80 _timeUnit, uint256 _numerator, uint256 _denominator) internal virtual { + require(_denominator != 0, "divide by 0"); + require(_timeUnit != 0, "time-unit can't be 0"); + uint256 conditionId = nextConditionId; + nextConditionId += 1; + + stakingConditions[conditionId] = StakingCondition({ + timeUnit: _timeUnit, + rewardRatioNumerator: _numerator, + rewardRatioDenominator: _denominator, + startTimestamp: uint80(block.timestamp), + endTimestamp: 0 + }); + + if (conditionId > 0) { + stakingConditions[conditionId - 1].endTimestamp = uint80(block.timestamp); + } + } + + /// @dev Calculate rewards for a staker. + function _calculateRewards(address _staker) internal view virtual returns (uint256 _rewards) { + Staker memory staker = stakers[_staker]; + + uint256 _stakerConditionId = staker.conditionIdOflastUpdate; + uint256 _nextConditionId = nextConditionId; + + for (uint256 i = _stakerConditionId; i < _nextConditionId; i += 1) { + StakingCondition memory condition = stakingConditions[i]; + + uint256 startTime = i != _stakerConditionId ? condition.startTimestamp : staker.timeOfLastUpdate; + uint256 endTime = condition.endTimestamp != 0 ? condition.endTimestamp : block.timestamp; + + (bool noOverflowProduct, uint256 rewardsProduct) = SafeMath.tryMul( + (endTime - startTime) * staker.amountStaked, + condition.rewardRatioNumerator + ); + (bool noOverflowSum, uint256 rewardsSum) = SafeMath.tryAdd( + _rewards, + (rewardsProduct / condition.timeUnit) / condition.rewardRatioDenominator + ); + + _rewards = noOverflowProduct && noOverflowSum ? rewardsSum : _rewards; + } + + (, _rewards) = SafeMath.tryMul(_rewards, 10 ** rewardTokenDecimals); + + _rewards /= (10 ** stakingTokenDecimals); + } + + /*//////////////////////////////////////////////////////////////////// + Optional hooks that can be implemented in the derived contract + ///////////////////////////////////////////////////////////////////*/ + + /// @dev Exposes the ability to override the msg sender -- support ERC2771. + function _stakeMsgSender() internal virtual returns (address) { + return msg.sender; + } + + /*/////////////////////////////////////////////////////////////// + Virtual functions to be implemented in derived contract + //////////////////////////////////////////////////////////////*/ + + /** + * @notice View total rewards available in the staking contract. + * + */ + function getRewardTokenBalance() external view virtual returns (uint256 _rewardsAvailableInContract); + + /** + * @dev Mint/Transfer ERC20 rewards to the staker. Must override. + * + * @param _staker Address for which to calculated rewards. + * @param _rewards Amount of tokens to be given out as reward. + * + * For example, override as below to mint ERC20 rewards: + * + * ``` + * function _mintRewards(address _staker, uint256 _rewards) internal override { + * + * TokenERC20(rewardTokenAddress).mintTo(_staker, _rewards); + * + * } + * ``` + */ + function _mintRewards(address _staker, uint256 _rewards) internal virtual; + + /** + * @dev Returns whether staking restrictions can be set in given execution context. + * Must override. + * + * + * For example, override as below to restrict access to admin: + * + * ``` + * function _canSetStakeConditions() internal override { + * + * return msg.sender == adminAddress; + * + * } + * ``` + */ + function _canSetStakeConditions() internal view virtual returns (bool); +} diff --git a/contracts/extension/Staking20Upgradeable.sol b/contracts/extension/Staking20Upgradeable.sol new file mode 100644 index 000000000..c83c16b1a --- /dev/null +++ b/contracts/extension/Staking20Upgradeable.sol @@ -0,0 +1,383 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "../external-deps/openzeppelin/utils/math/SafeMath.sol"; +import "../eip/interface/IERC20.sol"; +import { CurrencyTransferLib } from "../lib/CurrencyTransferLib.sol"; + +import "./interface/IStaking20.sol"; + +abstract contract Staking20Upgradeable is ReentrancyGuardUpgradeable, IStaking20 { + /*/////////////////////////////////////////////////////////////// + State variables / Mappings + //////////////////////////////////////////////////////////////*/ + + /// @dev The address of the native token wrapper contract. + address internal immutable nativeTokenWrapper; + + ///@dev Address of ERC20 contract -- staked tokens belong to this contract. + address public stakingToken; + + /// @dev Decimals of staking token. + uint16 public stakingTokenDecimals; + + /// @dev Decimals of reward token. + uint16 public rewardTokenDecimals; + + ///@dev Next staking condition Id. Tracks number of conditon updates so far. + uint64 private nextConditionId; + + /// @dev Total amount of tokens staked in the contract. + uint256 public stakingTokenBalance; + + /// @dev List of accounts that have staked that token-id. + address[] public stakersArray; + + ///@dev Mapping staker address to Staker struct. See {struct IStaking20.Staker}. + mapping(address => Staker) public stakers; + + ///@dev Mapping from condition Id to staking condition. See {struct IStaking721.StakingCondition} + mapping(uint256 => StakingCondition) private stakingConditions; + + constructor(address _nativeTokenWrapper) { + require(_nativeTokenWrapper != address(0), "address 0"); + + nativeTokenWrapper = _nativeTokenWrapper; + } + + function __Staking20_init( + address _stakingToken, + uint16 _stakingTokenDecimals, + uint16 _rewardTokenDecimals + ) internal onlyInitializing { + __ReentrancyGuard_init(); + + require(address(_stakingToken) != address(0), "token address 0"); + require(_stakingTokenDecimals != 0 && _rewardTokenDecimals != 0, "decimals 0"); + + stakingToken = _stakingToken; + stakingTokenDecimals = _stakingTokenDecimals; + rewardTokenDecimals = _rewardTokenDecimals; + } + + /*/////////////////////////////////////////////////////////////// + External/Public Functions + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Stake ERC20 Tokens. + * + * @dev See {_stake}. Override that to implement custom logic. + * + * @param _amount Amount to stake. + */ + function stake(uint256 _amount) external payable nonReentrant { + _stake(_amount); + } + + /** + * @notice Withdraw staked ERC20 tokens. + * + * @dev See {_withdraw}. Override that to implement custom logic. + * + * @param _amount Amount to withdraw. + */ + function withdraw(uint256 _amount) external nonReentrant { + _withdraw(_amount); + } + + /** + * @notice Claim accumulated rewards. + * + * @dev See {_claimRewards}. Override that to implement custom logic. + * See {_calculateRewards} for reward-calculation logic. + */ + function claimRewards() external nonReentrant { + _claimRewards(); + } + + /** + * @notice Set time unit. Set as a number of seconds. + * Could be specified as -- x * 1 hours, x * 1 days, etc. + * + * @dev Only admin/authorized-account can call it. + * + * @param _timeUnit New time unit. + */ + function setTimeUnit(uint80 _timeUnit) external virtual { + if (!_canSetStakeConditions()) { + revert("Not authorized"); + } + + StakingCondition memory condition = stakingConditions[nextConditionId - 1]; + require(_timeUnit != condition.timeUnit, "Time-unit unchanged."); + + _setStakingCondition(_timeUnit, condition.rewardRatioNumerator, condition.rewardRatioDenominator); + + emit UpdatedTimeUnit(condition.timeUnit, _timeUnit); + } + + /** + * @notice Set rewards per unit of time. + * Interpreted as (numerator/denominator) rewards per second/per day/etc based on time-unit. + * + * For e.g., ratio of 1/20 would mean 1 reward token for every 20 tokens staked. + * + * @dev Only admin/authorized-account can call it. + * + * @param _numerator Reward ratio numerator. + * @param _denominator Reward ratio denominator. + */ + function setRewardRatio(uint256 _numerator, uint256 _denominator) external virtual { + if (!_canSetStakeConditions()) { + revert("Not authorized"); + } + + StakingCondition memory condition = stakingConditions[nextConditionId - 1]; + require( + _numerator != condition.rewardRatioNumerator || _denominator != condition.rewardRatioDenominator, + "Reward ratio unchanged." + ); + _setStakingCondition(condition.timeUnit, _numerator, _denominator); + + emit UpdatedRewardRatio( + condition.rewardRatioNumerator, + _numerator, + condition.rewardRatioDenominator, + _denominator + ); + } + + /** + * @notice View amount staked and rewards for a user. + * + * @param _staker Address for which to calculated rewards. + * @return _tokensStaked Amount of tokens staked. + * @return _rewards Available reward amount. + */ + function getStakeInfo(address _staker) external view virtual returns (uint256 _tokensStaked, uint256 _rewards) { + _tokensStaked = stakers[_staker].amountStaked; + _rewards = _availableRewards(_staker); + } + + function getTimeUnit() public view returns (uint80 _timeUnit) { + _timeUnit = stakingConditions[nextConditionId - 1].timeUnit; + } + + function getRewardRatio() public view returns (uint256 _numerator, uint256 _denominator) { + _numerator = stakingConditions[nextConditionId - 1].rewardRatioNumerator; + _denominator = stakingConditions[nextConditionId - 1].rewardRatioDenominator; + } + + /*/////////////////////////////////////////////////////////////// + Internal Functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Staking logic. Override to add custom logic. + function _stake(uint256 _amount) internal virtual { + require(_amount != 0, "Staking 0 tokens"); + + address _stakingToken; + if (stakingToken == CurrencyTransferLib.NATIVE_TOKEN) { + _stakingToken = nativeTokenWrapper; + } else { + require(msg.value == 0, "Value not 0"); + _stakingToken = stakingToken; + } + + if (stakers[_stakeMsgSender()].amountStaked > 0) { + _updateUnclaimedRewardsForStaker(_stakeMsgSender()); + } else { + stakersArray.push(_stakeMsgSender()); + stakers[_stakeMsgSender()].timeOfLastUpdate = uint80(block.timestamp); + stakers[_stakeMsgSender()].conditionIdOflastUpdate = nextConditionId - 1; + } + + uint256 balanceBefore = IERC20(_stakingToken).balanceOf(address(this)); + CurrencyTransferLib.transferCurrencyWithWrapper( + stakingToken, + _stakeMsgSender(), + address(this), + _amount, + nativeTokenWrapper + ); + uint256 actualAmount = IERC20(_stakingToken).balanceOf(address(this)) - balanceBefore; + + stakers[_stakeMsgSender()].amountStaked += actualAmount; + stakingTokenBalance += actualAmount; + + emit TokensStaked(_stakeMsgSender(), actualAmount); + } + + /// @dev Withdraw logic. Override to add custom logic. + function _withdraw(uint256 _amount) internal virtual { + uint256 _amountStaked = stakers[_stakeMsgSender()].amountStaked; + require(_amount != 0, "Withdrawing 0 tokens"); + require(_amountStaked >= _amount, "Withdrawing more than staked"); + + _updateUnclaimedRewardsForStaker(_stakeMsgSender()); + + if (_amountStaked == _amount) { + address[] memory _stakersArray = stakersArray; + for (uint256 i = 0; i < _stakersArray.length; ++i) { + if (_stakersArray[i] == _stakeMsgSender()) { + stakersArray[i] = _stakersArray[_stakersArray.length - 1]; + stakersArray.pop(); + break; + } + } + } + + stakers[_stakeMsgSender()].amountStaked -= _amount; + stakingTokenBalance -= _amount; + + CurrencyTransferLib.transferCurrencyWithWrapper( + stakingToken, + address(this), + _stakeMsgSender(), + _amount, + nativeTokenWrapper + ); + + emit TokensWithdrawn(_stakeMsgSender(), _amount); + } + + /// @dev Logic for claiming rewards. Override to add custom logic. + function _claimRewards() internal virtual { + uint256 rewards = stakers[_stakeMsgSender()].unclaimedRewards + _calculateRewards(_stakeMsgSender()); + + require(rewards != 0, "No rewards"); + + stakers[_stakeMsgSender()].timeOfLastUpdate = uint80(block.timestamp); + stakers[_stakeMsgSender()].unclaimedRewards = 0; + stakers[_stakeMsgSender()].conditionIdOflastUpdate = nextConditionId - 1; + + _mintRewards(_stakeMsgSender(), rewards); + + emit RewardsClaimed(_stakeMsgSender(), rewards); + } + + /// @dev View available rewards for a user. + function _availableRewards(address _staker) internal view virtual returns (uint256 _rewards) { + if (stakers[_staker].amountStaked == 0) { + _rewards = stakers[_staker].unclaimedRewards; + } else { + _rewards = stakers[_staker].unclaimedRewards + _calculateRewards(_staker); + } + } + + /// @dev Update unclaimed rewards for a users. Called for every state change for a user. + function _updateUnclaimedRewardsForStaker(address _staker) internal virtual { + uint256 rewards = _calculateRewards(_staker); + stakers[_staker].unclaimedRewards += rewards; + stakers[_staker].timeOfLastUpdate = uint80(block.timestamp); + stakers[_staker].conditionIdOflastUpdate = nextConditionId - 1; + } + + /// @dev Set staking conditions. + function _setStakingCondition(uint80 _timeUnit, uint256 _numerator, uint256 _denominator) internal virtual { + require(_denominator != 0, "divide by 0"); + require(_timeUnit != 0, "time-unit can't be 0"); + uint256 conditionId = nextConditionId; + nextConditionId += 1; + + stakingConditions[conditionId] = StakingCondition({ + timeUnit: _timeUnit, + rewardRatioNumerator: _numerator, + rewardRatioDenominator: _denominator, + startTimestamp: uint80(block.timestamp), + endTimestamp: 0 + }); + + if (conditionId > 0) { + stakingConditions[conditionId - 1].endTimestamp = uint80(block.timestamp); + } + } + + /// @dev Calculate rewards for a staker. + function _calculateRewards(address _staker) internal view virtual returns (uint256 _rewards) { + Staker memory staker = stakers[_staker]; + + uint256 _stakerConditionId = staker.conditionIdOflastUpdate; + uint256 _nextConditionId = nextConditionId; + + for (uint256 i = _stakerConditionId; i < _nextConditionId; i += 1) { + StakingCondition memory condition = stakingConditions[i]; + + uint256 startTime = i != _stakerConditionId ? condition.startTimestamp : staker.timeOfLastUpdate; + uint256 endTime = condition.endTimestamp != 0 ? condition.endTimestamp : block.timestamp; + + (bool noOverflowProduct, uint256 rewardsProduct) = SafeMath.tryMul( + (endTime - startTime) * staker.amountStaked, + condition.rewardRatioNumerator + ); + (bool noOverflowSum, uint256 rewardsSum) = SafeMath.tryAdd( + _rewards, + (rewardsProduct / condition.timeUnit) / condition.rewardRatioDenominator + ); + + _rewards = noOverflowProduct && noOverflowSum ? rewardsSum : _rewards; + } + + (, _rewards) = SafeMath.tryMul(_rewards, 10 ** rewardTokenDecimals); + + _rewards /= (10 ** stakingTokenDecimals); + } + + /*//////////////////////////////////////////////////////////////////// + Optional hooks that can be implemented in the derived contract + ///////////////////////////////////////////////////////////////////*/ + + /// @dev Exposes the ability to override the msg sender -- support ERC2771. + function _stakeMsgSender() internal virtual returns (address) { + return msg.sender; + } + + /*/////////////////////////////////////////////////////////////// + Virtual functions to be implemented in derived contract + //////////////////////////////////////////////////////////////*/ + + /** + * @notice View total rewards available in the staking contract. + * + */ + function getRewardTokenBalance() external view virtual returns (uint256 _rewardsAvailableInContract); + + /** + * @dev Mint/Transfer ERC20 rewards to the staker. Must override. + * + * @param _staker Address for which to calculated rewards. + * @param _rewards Amount of tokens to be given out as reward. + * + * For example, override as below to mint ERC20 rewards: + * + * ``` + * function _mintRewards(address _staker, uint256 _rewards) internal override { + * + * TokenERC20(rewardTokenAddress).mintTo(_staker, _rewards); + * + * } + * ``` + */ + function _mintRewards(address _staker, uint256 _rewards) internal virtual; + + /** + * @dev Returns whether staking restrictions can be set in given execution context. + * Must override. + * + * + * For example, override as below to restrict access to admin: + * + * ``` + * function _canSetStakeConditions() internal override { + * + * return msg.sender == adminAddress; + * + * } + * ``` + */ + function _canSetStakeConditions() internal view virtual returns (bool); +} diff --git a/contracts/extension/Staking721.sol b/contracts/extension/Staking721.sol new file mode 100644 index 000000000..9d18144c5 --- /dev/null +++ b/contracts/extension/Staking721.sol @@ -0,0 +1,362 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +import "../external-deps/openzeppelin/security/ReentrancyGuard.sol"; +import "../external-deps/openzeppelin/utils/math/SafeMath.sol"; +import "../eip/interface/IERC721.sol"; + +import "./interface/IStaking721.sol"; + +abstract contract Staking721 is ReentrancyGuard, IStaking721 { + /*/////////////////////////////////////////////////////////////// + State variables / Mappings + //////////////////////////////////////////////////////////////*/ + + ///@dev Address of ERC721 NFT contract -- staked tokens belong to this contract. + address public immutable stakingToken; + + /// @dev Flag to check direct transfers of staking tokens. + uint8 internal isStaking = 1; + + ///@dev Next staking condition Id. Tracks number of conditon updates so far. + uint64 private nextConditionId; + + ///@dev List of token-ids ever staked. + uint256[] public indexedTokens; + + /// @dev List of accounts that have staked their NFTs. + address[] public stakersArray; + + ///@dev Mapping from token-id to whether it is indexed or not. + mapping(uint256 => bool) public isIndexed; + + ///@dev Mapping from staker address to Staker struct. See {struct IStaking721.Staker}. + mapping(address => Staker) public stakers; + + /// @dev Mapping from staked token-id to staker address. + mapping(uint256 => address) public stakerAddress; + + ///@dev Mapping from condition Id to staking condition. See {struct IStaking721.StakingCondition} + mapping(uint256 => StakingCondition) private stakingConditions; + + constructor(address _stakingToken) ReentrancyGuard() { + require(address(_stakingToken) != address(0), "collection address 0"); + stakingToken = _stakingToken; + } + + /*/////////////////////////////////////////////////////////////// + External/Public Functions + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Stake ERC721 Tokens. + * + * @dev See {_stake}. Override that to implement custom logic. + * + * @param _tokenIds List of tokens to stake. + */ + function stake(uint256[] calldata _tokenIds) external nonReentrant { + _stake(_tokenIds); + } + + /** + * @notice Withdraw staked tokens. + * + * @dev See {_withdraw}. Override that to implement custom logic. + * + * @param _tokenIds List of tokens to withdraw. + */ + function withdraw(uint256[] calldata _tokenIds) external nonReentrant { + _withdraw(_tokenIds); + } + + /** + * @notice Claim accumulated rewards. + * + * @dev See {_claimRewards}. Override that to implement custom logic. + * See {_calculateRewards} for reward-calculation logic. + */ + function claimRewards() external nonReentrant { + _claimRewards(); + } + + /** + * @notice Set time unit. Set as a number of seconds. + * Could be specified as -- x * 1 hours, x * 1 days, etc. + * + * @dev Only admin/authorized-account can call it. + * + * + * @param _timeUnit New time unit. + */ + function setTimeUnit(uint256 _timeUnit) external virtual { + if (!_canSetStakeConditions()) { + revert("Not authorized"); + } + + StakingCondition memory condition = stakingConditions[nextConditionId - 1]; + require(_timeUnit != condition.timeUnit, "Time-unit unchanged."); + + _setStakingCondition(_timeUnit, condition.rewardsPerUnitTime); + + emit UpdatedTimeUnit(condition.timeUnit, _timeUnit); + } + + /** + * @notice Set rewards per unit of time. + * Interpreted as x rewards per second/per day/etc based on time-unit. + * + * @dev Only admin/authorized-account can call it. + * + * + * @param _rewardsPerUnitTime New rewards per unit time. + */ + function setRewardsPerUnitTime(uint256 _rewardsPerUnitTime) external virtual { + if (!_canSetStakeConditions()) { + revert("Not authorized"); + } + + StakingCondition memory condition = stakingConditions[nextConditionId - 1]; + require(_rewardsPerUnitTime != condition.rewardsPerUnitTime, "Reward unchanged."); + + _setStakingCondition(condition.timeUnit, _rewardsPerUnitTime); + + emit UpdatedRewardsPerUnitTime(condition.rewardsPerUnitTime, _rewardsPerUnitTime); + } + + /** + * @notice View amount staked and total rewards for a user. + * + * @param _staker Address for which to calculated rewards. + * @return _tokensStaked List of token-ids staked by staker. + * @return _rewards Available reward amount. + */ + function getStakeInfo( + address _staker + ) external view virtual returns (uint256[] memory _tokensStaked, uint256 _rewards) { + uint256[] memory _indexedTokens = indexedTokens; + bool[] memory _isStakerToken = new bool[](_indexedTokens.length); + uint256 indexedTokenCount = _indexedTokens.length; + uint256 stakerTokenCount = 0; + + for (uint256 i = 0; i < indexedTokenCount; i++) { + _isStakerToken[i] = stakerAddress[_indexedTokens[i]] == _staker; + if (_isStakerToken[i]) stakerTokenCount += 1; + } + + _tokensStaked = new uint256[](stakerTokenCount); + uint256 count = 0; + for (uint256 i = 0; i < indexedTokenCount; i++) { + if (_isStakerToken[i]) { + _tokensStaked[count] = _indexedTokens[i]; + count += 1; + } + } + + _rewards = _availableRewards(_staker); + } + + function getTimeUnit() public view returns (uint256 _timeUnit) { + _timeUnit = stakingConditions[nextConditionId - 1].timeUnit; + } + + function getRewardsPerUnitTime() public view returns (uint256 _rewardsPerUnitTime) { + _rewardsPerUnitTime = stakingConditions[nextConditionId - 1].rewardsPerUnitTime; + } + + /*/////////////////////////////////////////////////////////////// + Internal Functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Staking logic. Override to add custom logic. + function _stake(uint256[] calldata _tokenIds) internal virtual { + uint64 len = uint64(_tokenIds.length); + require(len != 0, "Staking 0 tokens"); + + address _stakingToken = stakingToken; + + if (stakers[_stakeMsgSender()].amountStaked > 0) { + _updateUnclaimedRewardsForStaker(_stakeMsgSender()); + } else { + stakersArray.push(_stakeMsgSender()); + stakers[_stakeMsgSender()].timeOfLastUpdate = uint128(block.timestamp); + stakers[_stakeMsgSender()].conditionIdOflastUpdate = nextConditionId - 1; + } + for (uint256 i = 0; i < len; ++i) { + isStaking = 2; + IERC721(_stakingToken).safeTransferFrom(_stakeMsgSender(), address(this), _tokenIds[i]); + isStaking = 1; + + stakerAddress[_tokenIds[i]] = _stakeMsgSender(); + + if (!isIndexed[_tokenIds[i]]) { + isIndexed[_tokenIds[i]] = true; + indexedTokens.push(_tokenIds[i]); + } + } + stakers[_stakeMsgSender()].amountStaked += len; + + emit TokensStaked(_stakeMsgSender(), _tokenIds); + } + + /// @dev Withdraw logic. Override to add custom logic. + function _withdraw(uint256[] calldata _tokenIds) internal virtual { + uint256 _amountStaked = stakers[_stakeMsgSender()].amountStaked; + uint64 len = uint64(_tokenIds.length); + require(len != 0, "Withdrawing 0 tokens"); + require(_amountStaked >= len, "Withdrawing more than staked"); + + address _stakingToken = stakingToken; + + _updateUnclaimedRewardsForStaker(_stakeMsgSender()); + + if (_amountStaked == len) { + address[] memory _stakersArray = stakersArray; + for (uint256 i = 0; i < _stakersArray.length; ++i) { + if (_stakersArray[i] == _stakeMsgSender()) { + stakersArray[i] = _stakersArray[_stakersArray.length - 1]; + stakersArray.pop(); + break; + } + } + } + stakers[_stakeMsgSender()].amountStaked -= len; + + for (uint256 i = 0; i < len; ++i) { + require(stakerAddress[_tokenIds[i]] == _stakeMsgSender(), "Not staker"); + stakerAddress[_tokenIds[i]] = address(0); + IERC721(_stakingToken).safeTransferFrom(address(this), _stakeMsgSender(), _tokenIds[i]); + } + + emit TokensWithdrawn(_stakeMsgSender(), _tokenIds); + } + + /// @dev Logic for claiming rewards. Override to add custom logic. + function _claimRewards() internal virtual { + uint256 rewards = stakers[_stakeMsgSender()].unclaimedRewards + _calculateRewards(_stakeMsgSender()); + + require(rewards != 0, "No rewards"); + + stakers[_stakeMsgSender()].timeOfLastUpdate = uint128(block.timestamp); + stakers[_stakeMsgSender()].unclaimedRewards = 0; + stakers[_stakeMsgSender()].conditionIdOflastUpdate = nextConditionId - 1; + + _mintRewards(_stakeMsgSender(), rewards); + + emit RewardsClaimed(_stakeMsgSender(), rewards); + } + + /// @dev View available rewards for a user. + function _availableRewards(address _user) internal view virtual returns (uint256 _rewards) { + if (stakers[_user].amountStaked == 0) { + _rewards = stakers[_user].unclaimedRewards; + } else { + _rewards = stakers[_user].unclaimedRewards + _calculateRewards(_user); + } + } + + /// @dev Update unclaimed rewards for a users. Called for every state change for a user. + function _updateUnclaimedRewardsForStaker(address _staker) internal virtual { + uint256 rewards = _calculateRewards(_staker); + stakers[_staker].unclaimedRewards += rewards; + stakers[_staker].timeOfLastUpdate = uint128(block.timestamp); + stakers[_staker].conditionIdOflastUpdate = nextConditionId - 1; + } + + /// @dev Set staking conditions. + function _setStakingCondition(uint256 _timeUnit, uint256 _rewardsPerUnitTime) internal virtual { + require(_timeUnit != 0, "time-unit can't be 0"); + uint256 conditionId = nextConditionId; + nextConditionId += 1; + + stakingConditions[conditionId] = StakingCondition({ + timeUnit: _timeUnit, + rewardsPerUnitTime: _rewardsPerUnitTime, + startTimestamp: block.timestamp, + endTimestamp: 0 + }); + + if (conditionId > 0) { + stakingConditions[conditionId - 1].endTimestamp = block.timestamp; + } + } + + /// @dev Calculate rewards for a staker. + function _calculateRewards(address _staker) internal view virtual returns (uint256 _rewards) { + Staker memory staker = stakers[_staker]; + + uint256 _stakerConditionId = staker.conditionIdOflastUpdate; + uint256 _nextConditionId = nextConditionId; + + for (uint256 i = _stakerConditionId; i < _nextConditionId; i += 1) { + StakingCondition memory condition = stakingConditions[i]; + + uint256 startTime = i != _stakerConditionId ? condition.startTimestamp : staker.timeOfLastUpdate; + uint256 endTime = condition.endTimestamp != 0 ? condition.endTimestamp : block.timestamp; + + (bool noOverflowProduct, uint256 rewardsProduct) = SafeMath.tryMul( + (endTime - startTime) * staker.amountStaked, + condition.rewardsPerUnitTime + ); + (bool noOverflowSum, uint256 rewardsSum) = SafeMath.tryAdd(_rewards, rewardsProduct / condition.timeUnit); + + _rewards = noOverflowProduct && noOverflowSum ? rewardsSum : _rewards; + } + } + + /*//////////////////////////////////////////////////////////////////// + Optional hooks that can be implemented in the derived contract + ///////////////////////////////////////////////////////////////////*/ + + /// @dev Exposes the ability to override the msg sender -- support ERC2771. + function _stakeMsgSender() internal virtual returns (address) { + return msg.sender; + } + + /*/////////////////////////////////////////////////////////////// + Virtual functions to be implemented in derived contract + //////////////////////////////////////////////////////////////*/ + + /** + * @notice View total rewards available in the staking contract. + * + */ + function getRewardTokenBalance() external view virtual returns (uint256 _rewardsAvailableInContract); + + /** + * @dev Mint/Transfer ERC20 rewards to the staker. Must override. + * + * @param _staker Address for which to calculated rewards. + * @param _rewards Amount of tokens to be given out as reward. + * + * For example, override as below to mint ERC20 rewards: + * + * ``` + * function _mintRewards(address _staker, uint256 _rewards) internal override { + * + * TokenERC20(rewardTokenAddress).mintTo(_staker, _rewards); + * + * } + * ``` + */ + function _mintRewards(address _staker, uint256 _rewards) internal virtual; + + /** + * @dev Returns whether staking restrictions can be set in given execution context. + * Must override. + * + * + * For example, override as below to restrict access to admin: + * + * ``` + * function _canSetStakeConditions() internal override { + * + * return msg.sender == adminAddress; + * + * } + * ``` + */ + function _canSetStakeConditions() internal view virtual returns (bool); +} diff --git a/contracts/extension/Staking721Upgradeable.sol b/contracts/extension/Staking721Upgradeable.sol new file mode 100644 index 000000000..a5719e641 --- /dev/null +++ b/contracts/extension/Staking721Upgradeable.sol @@ -0,0 +1,364 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "../external-deps/openzeppelin/utils/math/SafeMath.sol"; +import "../eip/interface/IERC721.sol"; + +import "./interface/IStaking721.sol"; + +abstract contract Staking721Upgradeable is ReentrancyGuardUpgradeable, IStaking721 { + /*/////////////////////////////////////////////////////////////// + State variables / Mappings + //////////////////////////////////////////////////////////////*/ + + ///@dev Address of ERC721 NFT contract -- staked tokens belong to this contract. + address public stakingToken; + + /// @dev Flag to check direct transfers of staking tokens. + uint8 internal isStaking = 1; + + ///@dev Next staking condition Id. Tracks number of conditon updates so far. + uint64 private nextConditionId; + + ///@dev List of token-ids ever staked. + uint256[] public indexedTokens; + + /// @dev List of accounts that have staked their NFTs. + address[] public stakersArray; + + ///@dev Mapping from token-id to whether it is indexed or not. + mapping(uint256 => bool) public isIndexed; + + ///@dev Mapping from staker address to Staker struct. See {struct IStaking721.Staker}. + mapping(address => Staker) public stakers; + + /// @dev Mapping from staked token-id to staker address. + mapping(uint256 => address) public stakerAddress; + + ///@dev Mapping from condition Id to staking condition. See {struct IStaking721.StakingCondition} + mapping(uint256 => StakingCondition) private stakingConditions; + + function __Staking721_init(address _stakingToken) internal onlyInitializing { + __ReentrancyGuard_init(); + + require(address(_stakingToken) != address(0), "collection address 0"); + stakingToken = _stakingToken; + } + + /*/////////////////////////////////////////////////////////////// + External/Public Functions + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Stake ERC721 Tokens. + * + * @dev See {_stake}. Override that to implement custom logic. + * + * @param _tokenIds List of tokens to stake. + */ + function stake(uint256[] calldata _tokenIds) external nonReentrant { + _stake(_tokenIds); + } + + /** + * @notice Withdraw staked tokens. + * + * @dev See {_withdraw}. Override that to implement custom logic. + * + * @param _tokenIds List of tokens to withdraw. + */ + function withdraw(uint256[] calldata _tokenIds) external nonReentrant { + _withdraw(_tokenIds); + } + + /** + * @notice Claim accumulated rewards. + * + * @dev See {_claimRewards}. Override that to implement custom logic. + * See {_calculateRewards} for reward-calculation logic. + */ + function claimRewards() external nonReentrant { + _claimRewards(); + } + + /** + * @notice Set time unit. Set as a number of seconds. + * Could be specified as -- x * 1 hours, x * 1 days, etc. + * + * @dev Only admin/authorized-account can call it. + * + * + * @param _timeUnit New time unit. + */ + function setTimeUnit(uint256 _timeUnit) external virtual { + if (!_canSetStakeConditions()) { + revert("Not authorized"); + } + + StakingCondition memory condition = stakingConditions[nextConditionId - 1]; + require(_timeUnit != condition.timeUnit, "Time-unit unchanged."); + + _setStakingCondition(_timeUnit, condition.rewardsPerUnitTime); + + emit UpdatedTimeUnit(condition.timeUnit, _timeUnit); + } + + /** + * @notice Set rewards per unit of time. + * Interpreted as x rewards per second/per day/etc based on time-unit. + * + * @dev Only admin/authorized-account can call it. + * + * + * @param _rewardsPerUnitTime New rewards per unit time. + */ + function setRewardsPerUnitTime(uint256 _rewardsPerUnitTime) external virtual { + if (!_canSetStakeConditions()) { + revert("Not authorized"); + } + + StakingCondition memory condition = stakingConditions[nextConditionId - 1]; + require(_rewardsPerUnitTime != condition.rewardsPerUnitTime, "Reward unchanged."); + + _setStakingCondition(condition.timeUnit, _rewardsPerUnitTime); + + emit UpdatedRewardsPerUnitTime(condition.rewardsPerUnitTime, _rewardsPerUnitTime); + } + + /** + * @notice View amount staked and total rewards for a user. + * + * @param _staker Address for which to calculated rewards. + * @return _tokensStaked List of token-ids staked by staker. + * @return _rewards Available reward amount. + */ + function getStakeInfo( + address _staker + ) external view virtual returns (uint256[] memory _tokensStaked, uint256 _rewards) { + uint256[] memory _indexedTokens = indexedTokens; + bool[] memory _isStakerToken = new bool[](_indexedTokens.length); + uint256 indexedTokenCount = _indexedTokens.length; + uint256 stakerTokenCount = 0; + + for (uint256 i = 0; i < indexedTokenCount; i++) { + _isStakerToken[i] = stakerAddress[_indexedTokens[i]] == _staker; + if (_isStakerToken[i]) stakerTokenCount += 1; + } + + _tokensStaked = new uint256[](stakerTokenCount); + uint256 count = 0; + for (uint256 i = 0; i < indexedTokenCount; i++) { + if (_isStakerToken[i]) { + _tokensStaked[count] = _indexedTokens[i]; + count += 1; + } + } + + _rewards = _availableRewards(_staker); + } + + function getTimeUnit() public view returns (uint256 _timeUnit) { + _timeUnit = stakingConditions[nextConditionId - 1].timeUnit; + } + + function getRewardsPerUnitTime() public view returns (uint256 _rewardsPerUnitTime) { + _rewardsPerUnitTime = stakingConditions[nextConditionId - 1].rewardsPerUnitTime; + } + + /*/////////////////////////////////////////////////////////////// + Internal Functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Staking logic. Override to add custom logic. + function _stake(uint256[] calldata _tokenIds) internal virtual { + uint64 len = uint64(_tokenIds.length); + require(len != 0, "Staking 0 tokens"); + + address _stakingToken = stakingToken; + + if (stakers[_stakeMsgSender()].amountStaked > 0) { + _updateUnclaimedRewardsForStaker(_stakeMsgSender()); + } else { + stakersArray.push(_stakeMsgSender()); + stakers[_stakeMsgSender()].timeOfLastUpdate = uint128(block.timestamp); + stakers[_stakeMsgSender()].conditionIdOflastUpdate = nextConditionId - 1; + } + for (uint256 i = 0; i < len; ++i) { + isStaking = 2; + IERC721(_stakingToken).safeTransferFrom(_stakeMsgSender(), address(this), _tokenIds[i]); + isStaking = 1; + + stakerAddress[_tokenIds[i]] = _stakeMsgSender(); + + if (!isIndexed[_tokenIds[i]]) { + isIndexed[_tokenIds[i]] = true; + indexedTokens.push(_tokenIds[i]); + } + } + stakers[_stakeMsgSender()].amountStaked += len; + + emit TokensStaked(_stakeMsgSender(), _tokenIds); + } + + /// @dev Withdraw logic. Override to add custom logic. + function _withdraw(uint256[] calldata _tokenIds) internal virtual { + uint256 _amountStaked = stakers[_stakeMsgSender()].amountStaked; + uint64 len = uint64(_tokenIds.length); + require(len != 0, "Withdrawing 0 tokens"); + require(_amountStaked >= len, "Withdrawing more than staked"); + + address _stakingToken = stakingToken; + + _updateUnclaimedRewardsForStaker(_stakeMsgSender()); + + if (_amountStaked == len) { + address[] memory _stakersArray = stakersArray; + for (uint256 i = 0; i < _stakersArray.length; ++i) { + if (_stakersArray[i] == _stakeMsgSender()) { + stakersArray[i] = _stakersArray[_stakersArray.length - 1]; + stakersArray.pop(); + break; + } + } + } + stakers[_stakeMsgSender()].amountStaked -= len; + + for (uint256 i = 0; i < len; ++i) { + require(stakerAddress[_tokenIds[i]] == _stakeMsgSender(), "Not staker"); + stakerAddress[_tokenIds[i]] = address(0); + IERC721(_stakingToken).safeTransferFrom(address(this), _stakeMsgSender(), _tokenIds[i]); + } + + emit TokensWithdrawn(_stakeMsgSender(), _tokenIds); + } + + /// @dev Logic for claiming rewards. Override to add custom logic. + function _claimRewards() internal virtual { + uint256 rewards = stakers[_stakeMsgSender()].unclaimedRewards + _calculateRewards(_stakeMsgSender()); + + require(rewards != 0, "No rewards"); + + stakers[_stakeMsgSender()].timeOfLastUpdate = uint128(block.timestamp); + stakers[_stakeMsgSender()].unclaimedRewards = 0; + stakers[_stakeMsgSender()].conditionIdOflastUpdate = nextConditionId - 1; + + _mintRewards(_stakeMsgSender(), rewards); + + emit RewardsClaimed(_stakeMsgSender(), rewards); + } + + /// @dev View available rewards for a user. + function _availableRewards(address _user) internal view virtual returns (uint256 _rewards) { + if (stakers[_user].amountStaked == 0) { + _rewards = stakers[_user].unclaimedRewards; + } else { + _rewards = stakers[_user].unclaimedRewards + _calculateRewards(_user); + } + } + + /// @dev Update unclaimed rewards for a users. Called for every state change for a user. + function _updateUnclaimedRewardsForStaker(address _staker) internal virtual { + uint256 rewards = _calculateRewards(_staker); + stakers[_staker].unclaimedRewards += rewards; + stakers[_staker].timeOfLastUpdate = uint128(block.timestamp); + stakers[_staker].conditionIdOflastUpdate = nextConditionId - 1; + } + + /// @dev Set staking conditions. + function _setStakingCondition(uint256 _timeUnit, uint256 _rewardsPerUnitTime) internal virtual { + require(_timeUnit != 0, "time-unit can't be 0"); + uint256 conditionId = nextConditionId; + nextConditionId += 1; + + stakingConditions[conditionId] = StakingCondition({ + timeUnit: _timeUnit, + rewardsPerUnitTime: _rewardsPerUnitTime, + startTimestamp: block.timestamp, + endTimestamp: 0 + }); + + if (conditionId > 0) { + stakingConditions[conditionId - 1].endTimestamp = block.timestamp; + } + } + + /// @dev Calculate rewards for a staker. + function _calculateRewards(address _staker) internal view virtual returns (uint256 _rewards) { + Staker memory staker = stakers[_staker]; + + uint256 _stakerConditionId = staker.conditionIdOflastUpdate; + uint256 _nextConditionId = nextConditionId; + + for (uint256 i = _stakerConditionId; i < _nextConditionId; i += 1) { + StakingCondition memory condition = stakingConditions[i]; + + uint256 startTime = i != _stakerConditionId ? condition.startTimestamp : staker.timeOfLastUpdate; + uint256 endTime = condition.endTimestamp != 0 ? condition.endTimestamp : block.timestamp; + + (bool noOverflowProduct, uint256 rewardsProduct) = SafeMath.tryMul( + (endTime - startTime) * staker.amountStaked, + condition.rewardsPerUnitTime + ); + (bool noOverflowSum, uint256 rewardsSum) = SafeMath.tryAdd(_rewards, rewardsProduct / condition.timeUnit); + + _rewards = noOverflowProduct && noOverflowSum ? rewardsSum : _rewards; + } + } + + /*//////////////////////////////////////////////////////////////////// + Optional hooks that can be implemented in the derived contract + ///////////////////////////////////////////////////////////////////*/ + + /// @dev Exposes the ability to override the msg sender -- support ERC2771. + function _stakeMsgSender() internal virtual returns (address) { + return msg.sender; + } + + /*/////////////////////////////////////////////////////////////// + Virtual functions to be implemented in derived contract + //////////////////////////////////////////////////////////////*/ + + /** + * @notice View total rewards available in the staking contract. + * + */ + function getRewardTokenBalance() external view virtual returns (uint256 _rewardsAvailableInContract); + + /** + * @dev Mint/Transfer ERC20 rewards to the staker. Must override. + * + * @param _staker Address for which to calculated rewards. + * @param _rewards Amount of tokens to be given out as reward. + * + * For example, override as below to mint ERC20 rewards: + * + * ``` + * function _mintRewards(address _staker, uint256 _rewards) internal override { + * + * TokenERC20(rewardTokenAddress).mintTo(_staker, _rewards); + * + * } + * ``` + */ + function _mintRewards(address _staker, uint256 _rewards) internal virtual; + + /** + * @dev Returns whether staking restrictions can be set in given execution context. + * Must override. + * + * + * For example, override as below to restrict access to admin: + * + * ``` + * function _canSetStakeConditions() internal override { + * + * return msg.sender == adminAddress; + * + * } + * ``` + */ + function _canSetStakeConditions() internal view virtual returns (bool); +} diff --git a/contracts/extension/TokenBundle.sol b/contracts/extension/TokenBundle.sol index 74ed98f9b..76d773fca 100644 --- a/contracts/extension/TokenBundle.sol +++ b/contracts/extension/TokenBundle.sol @@ -1,7 +1,21 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + import "./interface/ITokenBundle.sol"; +import { CurrencyTransferLib } from "../lib/CurrencyTransferLib.sol"; + +interface IERC165 { + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} + +/** + * @title Token Bundle + * @notice `TokenBundle` contract extension allows bundling-up of ERC20/ERC721/ERC1155 and native-tokan assets + * in a data structure, and provides logic for setting/getting IDs and URIs for created bundles. + * @dev See {ITokenBundle} + */ abstract contract TokenBundle is ITokenBundle { /// @dev Mapping from bundle UID => bundle info. @@ -26,10 +40,11 @@ abstract contract TokenBundle is ITokenBundle { function _createBundle(Token[] calldata _tokensToBind, uint256 _bundleId) internal { uint256 targetCount = _tokensToBind.length; - require(targetCount > 0, "TokenBundle: no tokens to bind."); - require(bundle[_bundleId].count == 0, "TokenBundle: existent at bundleId"); + require(targetCount > 0, "!Tokens"); + require(bundle[_bundleId].count == 0, "id exists"); for (uint256 i = 0; i < targetCount; i += 1) { + _checkTokenType(_tokensToBind[i]); bundle[_bundleId].tokens[i] = _tokensToBind[i]; } @@ -37,8 +52,8 @@ abstract contract TokenBundle is ITokenBundle { } /// @dev Lets the calling contract update a bundle, by passing in a list of tokens and a unique id. - function _updateBundle(Token[] calldata _tokensToBind, uint256 _bundleId) internal { - require(_tokensToBind.length > 0, "TokenBundle: no tokens to bind."); + function _updateBundle(Token[] memory _tokensToBind, uint256 _bundleId) internal { + require(_tokensToBind.length > 0, "!Tokens"); uint256 currentCount = bundle[_bundleId].count; uint256 targetCount = _tokensToBind.length; @@ -46,6 +61,7 @@ abstract contract TokenBundle is ITokenBundle { for (uint256 i = 0; i < check; i += 1) { if (i < targetCount) { + _checkTokenType(_tokensToBind[i]); bundle[_bundleId].tokens[i] = _tokensToBind[i]; } else if (i < currentCount) { delete bundle[_bundleId].tokens[i]; @@ -57,6 +73,7 @@ abstract contract TokenBundle is ITokenBundle { /// @dev Lets the calling contract add a token to a bundle for a unique bundle id and index. function _addTokenInBundle(Token memory _tokenToBind, uint256 _bundleId) internal { + _checkTokenType(_tokenToBind); uint256 id = bundle[_bundleId].count; bundle[_bundleId].tokens[id] = _tokenToBind; @@ -64,17 +81,42 @@ abstract contract TokenBundle is ITokenBundle { } /// @dev Lets the calling contract update a token in a bundle for a unique bundle id and index. - function _updateTokenInBundle( - Token memory _tokenToBind, - uint256 _bundleId, - uint256 _index - ) internal { - require(_index < bundle[_bundleId].count, "TokenBundle: index DNE."); + function _updateTokenInBundle(Token memory _tokenToBind, uint256 _bundleId, uint256 _index) internal { + require(_index < bundle[_bundleId].count, "index DNE"); + _checkTokenType(_tokenToBind); bundle[_bundleId].tokens[_index] = _tokenToBind; } + /// @dev Checks if the type of asset-contract is same as the TokenType specified. + function _checkTokenType(Token memory _token) internal view { + if (_token.tokenType == TokenType.ERC721) { + try IERC165(_token.assetContract).supportsInterface(0x80ac58cd) returns (bool supported721) { + require(supported721, "!TokenType"); + } catch { + revert("!TokenType"); + } + } else if (_token.tokenType == TokenType.ERC1155) { + try IERC165(_token.assetContract).supportsInterface(0xd9b67a26) returns (bool supported1155) { + require(supported1155, "!TokenType"); + } catch { + revert("!TokenType"); + } + } else if (_token.tokenType == TokenType.ERC20) { + if (_token.assetContract != CurrencyTransferLib.NATIVE_TOKEN) { + // 0x36372b07 + try IERC165(_token.assetContract).supportsInterface(0x80ac58cd) returns (bool supported721) { + require(!supported721, "!TokenType"); + + try IERC165(_token.assetContract).supportsInterface(0xd9b67a26) returns (bool supported1155) { + require(!supported1155, "!TokenType"); + } catch Error(string memory) {} catch {} + } catch Error(string memory) {} catch {} + } + } + } + /// @dev Lets the calling contract set/update the uri of a particular bundle. - function _setUriOfBundle(string calldata _uri, uint256 _bundleId) internal { + function _setUriOfBundle(string memory _uri, uint256 _bundleId) internal { bundle[_bundleId].uri = _uri; } diff --git a/contracts/extension/TokenStore.sol b/contracts/extension/TokenStore.sol index 9c2726bcc..14b2042ba 100644 --- a/contracts/extension/TokenStore.sol +++ b/contracts/extension/TokenStore.sol @@ -1,23 +1,29 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + // ========== External imports ========== -import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; -import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "../eip/interface/IERC1155.sol"; +import "../eip/interface/IERC721.sol"; -import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; -import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; +import "../external-deps/openzeppelin/utils/ERC1155/ERC1155Holder.sol"; +import "../external-deps/openzeppelin/utils/ERC721/ERC721Holder.sol"; // ========== Internal imports ========== -import "./TokenBundle.sol"; -import "../lib/CurrencyTransferLib.sol"; +import { TokenBundle, ITokenBundle } from "./TokenBundle.sol"; +import { CurrencyTransferLib } from "../lib/CurrencyTransferLib.sol"; -contract TokenStore is TokenBundle, ERC721Holder, ERC1155Holder { - /// @dev The address interpreted as native token of the chain. - address public constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; +/** + * @title Token Store + * @notice `TokenStore` contract extension allows bundling-up of ERC20/ERC721/ERC1155 and native-tokan assets + * and provides logic for storing, releasing, and transferring them from the extending contract. + * @dev See {CurrencyTransferLib} + */ +contract TokenStore is TokenBundle, ERC721Holder, ERC1155Holder { /// @dev The address of the native token wrapper contract. address internal immutable nativeTokenWrapper; @@ -29,7 +35,7 @@ contract TokenStore is TokenBundle, ERC721Holder, ERC1155Holder { function _storeTokens( address _tokenOwner, Token[] calldata _tokens, - string calldata _uriForTokens, + string memory _uriForTokens, uint256 _idForTokens ) internal { _createBundle(_tokens, _idForTokens); @@ -52,11 +58,7 @@ contract TokenStore is TokenBundle, ERC721Holder, ERC1155Holder { } /// @dev Transfers an arbitrary ERC20 / ERC721 / ERC1155 token. - function _transferToken( - address _from, - address _to, - Token memory _token - ) internal { + function _transferToken(address _from, address _to, Token memory _token) internal { if (_token.tokenType == TokenType.ERC20) { CurrencyTransferLib.transferCurrencyWithWrapper( _token.assetContract, @@ -73,11 +75,7 @@ contract TokenStore is TokenBundle, ERC721Holder, ERC1155Holder { } /// @dev Transfers multiple arbitrary ERC20 / ERC721 / ERC1155 tokens. - function _transferTokenBatch( - address _from, - address _to, - Token[] memory _tokens - ) internal { + function _transferTokenBatch(address _from, address _to, Token[] memory _tokens) internal { uint256 nativeTokenValue; for (uint256 i = 0; i < _tokens.length; i += 1) { if (_tokens[i].assetContract == CurrencyTransferLib.NATIVE_TOKEN && _to == address(this)) { diff --git a/contracts/extension/Upgradeable.sol b/contracts/extension/Upgradeable.sol new file mode 100644 index 000000000..7f81db60b --- /dev/null +++ b/contracts/extension/Upgradeable.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../external-deps/openzeppelin/proxy/IERC1822Proxiable.sol"; +import "../external-deps/openzeppelin/proxy/ERC1967/ERC1967Upgrade.sol"; + +/** + * @dev An upgradeability mechanism designed for UUPS proxies. The functions included here can perform an upgrade of an + * {ERC1967Proxy}, when this contract is set as the implementation behind such a proxy. + * + * A security mechanism ensures that an upgrade does not turn off upgradeability accidentally, although this risk is + * reinstated if the upgrade retains upgradeability but removes the security mechanism, e.g. by replacing + * `UUPSUpgradeable` with a custom implementation of upgrades. + * + * The {_authorizeUpgrade} function must be overridden to include access restriction to the upgrade mechanism. + * + * _Available since v4.1._ + */ +abstract contract Upgradeable is IERC1822Proxiable, ERC1967Upgrade { + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment + address private immutable __self = address(this); + + /** + * @dev Check that the execution is being performed through a delegatecall call and that the execution context is + * a proxy contract with an implementation (as defined in ERC1967) pointing to self. This should only be the case + * for UUPS and transparent proxies that are using the current contract as their implementation. Execution of a + * function through ERC1167 minimal proxies (clones) would not normally pass this test, but is not guaranteed to + * fail. + */ + modifier onlyProxy() { + require(address(this) != __self, "Function must be called through delegatecall"); + require(_getImplementation() == __self, "Function must be called through active proxy"); + _; + } + + /** + * @dev Check that the execution is not being performed through a delegate call. This allows a function to be + * callable on the implementing contract but not through proxies. + */ + modifier notDelegated() { + require(address(this) == __self, "UUPSUpgradeable: must not be called through delegatecall"); + _; + } + + /** + * @dev Implementation of the ERC1822 {proxiableUUID} function. This returns the storage slot used by the + * implementation. It is used to validate that the this implementation remains valid after an upgrade. + * + * IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks + * bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this + * function revert if invoked through a proxy. This is guaranteed by the `notDelegated` modifier. + */ + function proxiableUUID() external view virtual override notDelegated returns (bytes32) { + return _IMPLEMENTATION_SLOT; + } + + /** + * @dev Upgrade the implementation of the proxy to `newImplementation`. + * + * Calls {_authorizeUpgrade}. + * + * Emits an {Upgraded} event. + */ + function upgradeTo(address newImplementation) external virtual onlyProxy { + _authorizeUpgrade(newImplementation); + _upgradeToAndCallUUPS(newImplementation, new bytes(0), false); + } + + /** + * @dev Upgrade the implementation of the proxy to `newImplementation`, and subsequently execute the function call + * encoded in `data`. + * + * Calls {_authorizeUpgrade}. + * + * Emits an {Upgraded} event. + */ + function upgradeToAndCall(address newImplementation, bytes memory data) external payable virtual onlyProxy { + _authorizeUpgrade(newImplementation); + _upgradeToAndCallUUPS(newImplementation, data, true); + } + + /** + * @dev Function that should revert when `msg.sender` is not authorized to upgrade the contract. Called by + * {upgradeTo} and {upgradeToAndCall}. + * + * Normally, this function will use an xref:access.adoc[access control] modifier such as {Ownable-onlyOwner}. + * + * ```solidity + * function _authorizeUpgrade(address) internal override onlyOwner {} + * ``` + */ + function _authorizeUpgrade(address newImplementation) internal virtual; +} diff --git a/contracts/extension/interface/IAccountPermissions.sol b/contracts/extension/interface/IAccountPermissions.sol new file mode 100644 index 000000000..0685a8301 --- /dev/null +++ b/contracts/extension/interface/IAccountPermissions.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +interface IAccountPermissions { + /*/////////////////////////////////////////////////////////////// + Types + //////////////////////////////////////////////////////////////*/ + + /** + * @notice The payload that must be signed by an authorized wallet to set permissions for a signer to use the smart wallet. + * + * @param signer The addres of the signer to give permissions. + * @param approvedTargets The list of approved targets that a role holder can call using the smart wallet. + * @param nativeTokenLimitPerTransaction The maximum value that can be transferred by a role holder in a single transaction. + * @param permissionStartTimestamp The UNIX timestamp at and after which a signer has permission to use the smart wallet. + * @param permissionEndTimestamp The UNIX timestamp at and after which a signer no longer has permission to use the smart wallet. + * @param reqValidityStartTimestamp The UNIX timestamp at and after which a signature is valid. + * @param reqValidityEndTimestamp The UNIX timestamp at and after which a signature is invalid/expired. + * @param uid A unique non-repeatable ID for the payload. + * @param isAdmin Whether the signer should be an admin. + */ + struct SignerPermissionRequest { + address signer; + uint8 isAdmin; + address[] approvedTargets; + uint256 nativeTokenLimitPerTransaction; + uint128 permissionStartTimestamp; + uint128 permissionEndTimestamp; + uint128 reqValidityStartTimestamp; + uint128 reqValidityEndTimestamp; + bytes32 uid; + } + + /** + * @notice The permissions that a signer has to use the smart wallet. + * + * @param signer The address of the signer. + * @param approvedTargets The list of approved targets that a role holder can call using the smart wallet. + * @param nativeTokenLimitPerTransaction The maximum value that can be transferred by a role holder in a single transaction. + * @param startTimestamp The UNIX timestamp at and after which a signer has permission to use the smart wallet. + * @param endTimestamp The UNIX timestamp at and after which a signer no longer has permission to use the smart wallet. + */ + struct SignerPermissions { + address signer; + address[] approvedTargets; + uint256 nativeTokenLimitPerTransaction; + uint128 startTimestamp; + uint128 endTimestamp; + } + + /** + * @notice Internal struct for storing permissions for a signer (without approved targets). + * + * @param nativeTokenLimitPerTransaction The maximum value that can be transferred by a role holder in a single transaction. + * @param startTimestamp The UNIX timestamp at and after which a signer has permission to use the smart wallet. + * @param endTimestamp The UNIX timestamp at and after which a signer no longer has permission to use the smart wallet. + */ + struct SignerPermissionsStatic { + uint256 nativeTokenLimitPerTransaction; + uint128 startTimestamp; + uint128 endTimestamp; + } + + /*/////////////////////////////////////////////////////////////// + Events + //////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when permissions for a signer are updated. + event SignerPermissionsUpdated( + address indexed authorizingSigner, + address indexed targetSigner, + SignerPermissionRequest permissions + ); + + /// @notice Emitted when an admin is set or removed. + event AdminUpdated(address indexed signer, bool isAdmin); + + /*/////////////////////////////////////////////////////////////// + View functions + //////////////////////////////////////////////////////////////*/ + + /// @notice Returns whether the given account is an admin. + function isAdmin(address signer) external view returns (bool); + + /// @notice Returns whether the given account is an active signer on the account. + function isActiveSigner(address signer) external view returns (bool); + + /// @notice Returns the restrictions under which a signer can use the smart wallet. + function getPermissionsForSigner(address signer) external view returns (SignerPermissions memory permissions); + + /// @notice Returns all active and inactive signers of the account. + function getAllSigners() external view returns (SignerPermissions[] memory signers); + + /// @notice Returns all signers with active permissions to use the account. + function getAllActiveSigners() external view returns (SignerPermissions[] memory signers); + + /// @notice Returns all admins of the account. + function getAllAdmins() external view returns (address[] memory admins); + + /// @dev Verifies that a request is signed by an authorized account. + function verifySignerPermissionRequest( + SignerPermissionRequest calldata req, + bytes calldata signature + ) external view returns (bool success, address signer); + + /*/////////////////////////////////////////////////////////////// + External functions + //////////////////////////////////////////////////////////////*/ + + /// @notice Sets the permissions for a given signer. + function setPermissionsForSigner(SignerPermissionRequest calldata req, bytes calldata signature) external; +} diff --git a/contracts/extension/interface/IAppURI.sol b/contracts/extension/interface/IAppURI.sol new file mode 100644 index 000000000..12875f990 --- /dev/null +++ b/contracts/extension/interface/IAppURI.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +/** + * Thirdweb's `AppURI` is a contract extension for any base contracts. It lets you set a metadata URI + * for you contract. + * + */ + +interface IAppURI { + /// @dev Returns the metadata URI of the contract. + function appURI() external view returns (string memory); + + /** + * @dev Sets contract URI for the storefront-level metadata of the contract. + * Only module admin can call this function. + */ + function setAppURI(string calldata _uri) external; + + /// @dev Emitted when the contract URI is updated. + event AppURIUpdated(string prevURI, string newURI); +} diff --git a/contracts/extension/interface/IBurnToClaim.sol b/contracts/extension/interface/IBurnToClaim.sol new file mode 100644 index 000000000..caf583d39 --- /dev/null +++ b/contracts/extension/interface/IBurnToClaim.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +interface IBurnToClaim { + /// @notice The type of assets that can be burned. + enum TokenType { + ERC721, + ERC1155 + } + + /** + * @notice Configuration for burning tokens to claim new tokens. + * + * @param originContractAddress The address of the contract that the tokens are burned from. + * @param tokenType The type of token to burn. + * @param tokenId The token ID of the token to burn. Only used if tokenType is ERC1155. + * @param mintPriceForNewToken The price to mint a new token. + * @param currency The currency to pay the mint price in. + */ + struct BurnToClaimInfo { + address originContractAddress; + TokenType tokenType; + uint256 tokenId; // used only if tokenType is ERC1155 + uint256 mintPriceForNewToken; + address currency; + } + + /// @notice Emitted when tokens are burned to claim new tokens + event TokensBurnedAndClaimed( + address indexed originContract, + address indexed tokenOwner, + uint256 indexed burnTokenId, + uint256 quantity + ); + + /** + * @notice Sets the configuration for burning tokens to claim new tokens. + * @param burnToClaimInfo The configuration for burning tokens to claim new tokens. + */ + function setBurnToClaimInfo(BurnToClaimInfo calldata burnToClaimInfo) external; +} diff --git a/contracts/extension/interface/IBurnableERC1155.sol b/contracts/extension/interface/IBurnableERC1155.sol index b5f7bcaa5..b4170c105 100644 --- a/contracts/extension/interface/IBurnableERC1155.sol +++ b/contracts/extension/interface/IBurnableERC1155.sol @@ -1,22 +1,16 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + /** * `SignatureMint1155` is an ERC 1155 contract. It lets anyone mint NFTs by producing a mint request * and a signature (produced by an account with MINTER_ROLE, signing the mint request). */ interface IBurnableERC1155 { /// @dev Lets a token owner burn the tokens they own (i.e. destroy for good) - function burn( - address account, - uint256 id, - uint256 value - ) external; + function burn(address account, uint256 id, uint256 value) external; /// @dev Lets a token owner burn multiple tokens they own at once (i.e. destroy for good) - function burnBatch( - address account, - uint256[] memory ids, - uint256[] memory values - ) external; + function burnBatch(address account, uint256[] memory ids, uint256[] memory values) external; } diff --git a/contracts/extension/interface/IBurnableERC20.sol b/contracts/extension/interface/IBurnableERC20.sol index cc73df32f..f6064f535 100644 --- a/contracts/extension/interface/IBurnableERC20.sol +++ b/contracts/extension/interface/IBurnableERC20.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + interface IBurnableERC20 { /** * @dev Destroys `amount` tokens from the caller. diff --git a/contracts/extension/interface/IBurnableERC721.sol b/contracts/extension/interface/IBurnableERC721.sol index dc69e9492..e7d92abd1 100644 --- a/contracts/extension/interface/IBurnableERC721.sol +++ b/contracts/extension/interface/IBurnableERC721.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + interface IBurnableERC721 { /** * @dev Burns `tokenId`. See {ERC721-_burn}. diff --git a/contracts/extension/interface/IClaimCondition.sol b/contracts/extension/interface/IClaimCondition.sol index ce5d7d29c..3e1f96c02 100644 --- a/contracts/extension/interface/IClaimCondition.sol +++ b/contracts/extension/interface/IClaimCondition.sol @@ -1,15 +1,13 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; -import "../../lib/TWBitMaps.sol"; +/// @author thirdweb /** - * Thirdweb's 'Drop' contracts are distribution mechanisms for tokens. + * The interface `IClaimCondition` is written for thirdweb's 'Drop' contracts, which are distribution mechanisms for tokens. * - * A contract admin (i.e. a holder of `DEFAULT_ADMIN_ROLE`) can set a series of claim conditions, - * ordered by their respective `startTimestamp`. A claim condition defines criteria under which - * accounts can mint tokens. Claim conditions can be overwritten or added to by the contract admin. - * At any moment, there is only one active claim condition. + * A claim condition defines criteria under which accounts can mint tokens. Claim conditions can be overwritten + * or added to by the contract admin. At any moment, there is only one active claim condition. */ interface IClaimCondition { @@ -26,11 +24,7 @@ interface IClaimCondition { * @param supplyClaimed At any given point, the number of tokens that have been claimed * under the claim condition. * - * @param quantityLimitPerTransaction The maximum number of tokens that can be claimed in a single - * transaction. - * - * @param waitTimeInSecondsBetweenClaims The least number of seconds an account must wait after claiming - * tokens, to be able to claim tokens again. + * @param quantityLimitPerWallet The maximum number of tokens that can be claimed by a wallet. * * @param merkleRoot The allowlist of addresses that can claim tokens under the claim * condition. @@ -38,15 +32,17 @@ interface IClaimCondition { * @param pricePerToken The price required to pay per token claimed. * * @param currency The currency in which the `pricePerToken` must be paid. + * + * @param metadata Claim condition metadata. */ struct ClaimCondition { uint256 startTimestamp; uint256 maxClaimableSupply; uint256 supplyClaimed; - uint256 quantityLimitPerTransaction; - uint256 waitTimeInSecondsBetweenClaims; + uint256 quantityLimitPerWallet; bytes32 merkleRoot; uint256 pricePerToken; address currency; + string metadata; } } diff --git a/contracts/extension/interface/IClaimConditionMultiPhase.sol b/contracts/extension/interface/IClaimConditionMultiPhase.sol index ae7b75eb5..df0d424e3 100644 --- a/contracts/extension/interface/IClaimConditionMultiPhase.sol +++ b/contracts/extension/interface/IClaimConditionMultiPhase.sol @@ -1,16 +1,16 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; -import "../../lib/TWBitMaps.sol"; +/// @author thirdweb + import "./IClaimCondition.sol"; /** - * Thirdweb's 'Drop' contracts are distribution mechanisms for tokens. + * The interface `IClaimConditionMultiPhase` is written for thirdweb's 'Drop' contracts, which are distribution mechanisms for tokens. * - * A contract admin (i.e. a holder of `DEFAULT_ADMIN_ROLE`) can set a series of claim conditions, - * ordered by their respective `startTimestamp`. A claim condition defines criteria under which - * accounts can mint tokens. Claim conditions can be overwritten or added to by the contract admin. - * At any moment, there is only one active claim condition. + * An authorized wallet can set a series of claim conditions, ordered by their respective `startTimestamp`. + * A claim condition defines criteria under which accounts can mint tokens. Claim conditions can be overwritten + * or added to by the contract admin. At any moment, there is only one active claim condition. */ interface IClaimConditionMultiPhase is IClaimCondition { @@ -28,17 +28,12 @@ interface IClaimConditionMultiPhase is IClaimCondition { * @param conditions The claim conditions at a given uid. Claim conditions * are ordered in an ascending order by their `startTimestamp`. * - * @param lastClaimTimestamp Map from an account and uid for a claim condition, to the last timestamp - * at which the account claimed tokens under that claim condition. - * - * @param usedAllowlistSpot Map from a claim condition uid to whether an address in an allowlist - * has already claimed tokens i.e. used their place in the allowlist. + * @param supplyClaimedByWallet Map from a claim condition uid and account to supply claimed by account. */ struct ClaimConditionList { uint256 currentStartId; uint256 count; mapping(uint256 => ClaimCondition) conditions; - mapping(uint256 => mapping(address => uint256)) lastClaimTimestamp; - mapping(uint256 => TWBitMaps.BitMap) usedAllowlistSpot; + mapping(uint256 => mapping(address => uint256)) supplyClaimedByWallet; } } diff --git a/contracts/extension/interface/IClaimConditionsSinglePhase.sol b/contracts/extension/interface/IClaimConditionsSinglePhase.sol index b3b571884..3b3d6d1ae 100644 --- a/contracts/extension/interface/IClaimConditionsSinglePhase.sol +++ b/contracts/extension/interface/IClaimConditionsSinglePhase.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; -import "../../lib/TWBitMaps.sol"; +/// @author thirdweb + +import "../../lib/BitMaps.sol"; import "./IClaimCondition.sol"; /** diff --git a/contracts/extension/interface/IClaimableERC1155.sol b/contracts/extension/interface/IClaimableERC1155.sol new file mode 100644 index 000000000..2afb26e29 --- /dev/null +++ b/contracts/extension/interface/IClaimableERC1155.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +interface IClaimableERC1155 { + /// @dev Emitted when tokens are claimed + event TokensClaimed( + address indexed claimer, + address indexed receiver, + uint256 indexed tokenId, + uint256 quantityClaimed + ); + + /** + * @notice Lets an address claim multiple lazy minted NFTs at once to a recipient. + * Contract creators should override this function to create custom logic for claiming, + * for e.g. price collection, allowlist, max quantity, etc. + * + * @dev The logic in the `verifyClaim` function determines whether the caller is authorized to mint NFTs. + * + * @param _receiver The recipient of the tokens to mint. + * @param _tokenId The tokenId of the lazy minted NFT to mint. + * @param _quantity The number of tokens to mint. + */ + function claim(address _receiver, uint256 _tokenId, uint256 _quantity) external payable; + + /** + * @notice Override this function to add logic for claim verification, based on conditions + * such as allowlist, price, max quantity etc. + * + * @dev Checks a request to claim NFTs against a custom condition. + * + * @param _claimer Caller of the claim function. + * @param _tokenId The tokenId of the lazy minted NFT to mint. + * @param _quantity The number of NFTs being claimed. + */ + function verifyClaim(address _claimer, uint256 _tokenId, uint256 _quantity) external view; +} diff --git a/contracts/extension/interface/IClaimableERC721.sol b/contracts/extension/interface/IClaimableERC721.sol new file mode 100644 index 000000000..d92918c71 --- /dev/null +++ b/contracts/extension/interface/IClaimableERC721.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +interface IClaimableERC721 { + /// @dev Emitted when tokens are claimed + event TokensClaimed( + address indexed claimer, + address indexed receiver, + uint256 indexed startTokenId, + uint256 quantityClaimed + ); + + /** + * @notice Lets an address claim multiple lazy minted NFTs at once to a recipient. + * Contract creators should override this function to create custom logic for claiming, + * for e.g. price collection, allowlist, max quantity, etc. + * + * @dev The logic in the `verifyClaim` function determines whether the caller is authorized to mint NFTs. + * + * @param _receiver The recipient of the NFT to mint. + * @param _quantity The number of NFTs to mint. + */ + function claim(address _receiver, uint256 _quantity) external payable; + + /** + * @notice Override this function to add logic for claim verification, based on conditions + * such as allowlist, price, max quantity etc. + * + * @dev Checks a request to claim NFTs against a custom condition. + * + * @param _claimer Caller of the claim function. + * @param _quantity The number of NFTs being claimed. + */ + function verifyClaim(address _claimer, uint256 _quantity) external view; +} diff --git a/contracts/extension/interface/IContractFactory.sol b/contracts/extension/interface/IContractFactory.sol new file mode 100644 index 000000000..7d4bcea78 --- /dev/null +++ b/contracts/extension/interface/IContractFactory.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +interface IContractFactory { + /** + * @notice Deploys a proxy that points to that points to the given implementation. + * + * @param implementation Address of the implementation to point to. + * + * @param data Additional data to pass to the proxy constructor or any other data useful during deployement. + * @param salt Salt to use for the deterministic address generation. + */ + function deployProxyByImplementation( + address implementation, + bytes memory data, + bytes32 salt + ) external returns (address); +} diff --git a/contracts/extension/interface/IContractMetadata.sol b/contracts/extension/interface/IContractMetadata.sol index b4200ea9b..b865001fd 100644 --- a/contracts/extension/interface/IContractMetadata.sol +++ b/contracts/extension/interface/IContractMetadata.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + /** * Thirdweb's `ContractMetadata` is a contract extension for any base contracts. It lets you set a metadata URI * for you contract. diff --git a/contracts/extension/interface/IDelayedReveal.sol b/contracts/extension/interface/IDelayedReveal.sol index 64e493605..74708761d 100644 --- a/contracts/extension/interface/IDelayedReveal.sol +++ b/contracts/extension/interface/IDelayedReveal.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + /** * Thirdweb's `DelayedReveal` is a contract extension for base NFT contracts. It lets you create batches of * 'delayed-reveal' NFTs. You can learn more about the usage of delayed reveal NFTs here - https://blog.thirdweb.com/delayed-reveal-nfts diff --git a/contracts/extension/interface/IDelayedRevealDeprecated.sol b/contracts/extension/interface/IDelayedRevealDeprecated.sol new file mode 100644 index 000000000..34650ae90 --- /dev/null +++ b/contracts/extension/interface/IDelayedRevealDeprecated.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +// [ DEPRECATED CONTRACT: use `contracts/extension/interface/IDelayedReveal.sol` instead ] + +/** + * Thirdweb's `DelayedReveal` is a contract extension for base NFT contracts. It lets you create batches of + * 'delayed-reveal' NFTs. You can learn more about the usage of delayed reveal NFTs here - https://blog.thirdweb.com/delayed-reveal-nfts + */ + +interface IDelayedRevealDeprecated { + /// @dev Emitted when tokens are revealed. + event TokenURIRevealed(uint256 indexed index, string revealedURI); + + /// @dev Returns the encrypted base URI associated with the given identifier. + function encryptedBaseURI(uint256 identifier) external view returns (bytes memory); + + /** + * @notice Reveals a batch of delayed reveal NFTs. + * + * @param identifier The ID for the batch of delayed-reveal NFTs to reveal. + * + * @param key The key with which the base URI for the relevant batch of NFTs was encrypted. + */ + function reveal(uint256 identifier, bytes calldata key) external returns (string memory revealedURI); + + /** + * @notice Performs XOR encryption/decryption. + * + * @param data The data to encrypt. In the case of delayed-reveal NFTs, this is the "revealed" state + * base URI of the relevant batch of NFTs. + * + * @param key The key with which to encrypt data + */ + function encryptDecrypt(bytes memory data, bytes calldata key) external pure returns (bytes memory result); +} diff --git a/contracts/extension/interface/IDrop.sol b/contracts/extension/interface/IDrop.sol index 61e7d86d2..4e466e6c3 100644 --- a/contracts/extension/interface/IDrop.sol +++ b/contracts/extension/interface/IDrop.sol @@ -1,52 +1,33 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + import "./IClaimConditionMultiPhase.sol"; +/** + * The interface `IDrop` is written for thirdweb's 'Drop' contracts, which are distribution mechanisms for tokens. + * + * An authorized wallet can set a series of claim conditions, ordered by their respective `startTimestamp`. + * A claim condition defines criteria under which accounts can mint tokens. Claim conditions can be overwritten + * or added to by the contract admin. At any moment, there is only one active claim condition. + */ + interface IDrop is IClaimConditionMultiPhase { + /** + * @param proof Proof of concerned wallet's inclusion in an allowlist. + * @param quantityLimitPerWallet The total quantity of tokens the allowlisted wallet is eligible to claim over time. + * @param pricePerToken The price per token the allowlisted wallet must pay to claim tokens. + * @param currency The currency in which the allowlisted wallet must pay the price for claiming tokens. + */ struct AllowlistProof { bytes32[] proof; - uint256 maxQuantityInAllowlist; + uint256 quantityLimitPerWallet; + uint256 pricePerToken; + address currency; } - /// @dev Emitted when an unauthorized caller tries to set claim conditions. - error Drop__NotAuthorized(); - - /// @notice Emitted when given currency or price is invalid. - error Drop__InvalidCurrencyOrPrice( - address givenCurrency, - address requiredCurrency, - uint256 givenPricePerToken, - uint256 requiredPricePerToken - ); - - /// @notice Emitted when claiming invalid quantity of tokens. - error Drop__InvalidQuantity(); - - /// @notice Emitted when claiming given quantity will exceed max claimable supply. - error Drop__ExceedMaxClaimableSupply(uint256 supplyClaimed, uint256 maxClaimableSupply); - - /// @notice Emitted when the current timestamp is invalid for claim. - error Drop__CannotClaimYet( - uint256 blockTimestamp, - uint256 startTimestamp, - uint256 lastClaimedAt, - uint256 nextValidClaimTimestamp - ); - - /// @notice Emitted when given allowlist proof is invalid. - error Drop__NotInWhitelist(); - - /// @notice Emitted when allowlist spot is already used. - error Drop__ProofClaimed(); - - /// @notice Emitted when claiming more than allowed quantity in allowlist. - error Drop__InvalidQuantityProof(uint256 maxQuantityInAllowlist); - - /// @notice Emitted when max claimable supply in given condition is less than supply claimed already. - error Drop__MaxSupplyClaimedAlready(uint256 supplyClaimedAlready); - - /// @dev Emitted when tokens are claimed via `claim`. + /// @notice Emitted when tokens are claimed via `claim`. event TokensClaimed( uint256 indexed claimConditionIndex, address indexed claimer, @@ -55,7 +36,7 @@ interface IDrop is IClaimConditionMultiPhase { uint256 quantityClaimed ); - /// @dev Emitted when the contract's claim conditions are updated. + /// @notice Emitted when the contract's claim conditions are updated. event ClaimConditionsUpdated(ClaimCondition[] claimConditions, bool resetEligibility); /** @@ -83,8 +64,8 @@ interface IDrop is IClaimConditionMultiPhase { * * @param phases Claim conditions in ascending order by `startTimestamp`. * - * @param resetClaimEligibility Whether to reset `limitLastClaimTimestamp` and `limitMerkleProofClaim` values when setting new - * claim conditions. + * @param resetClaimEligibility Whether to honor the restrictions applied to wallets who have claimed tokens in the current conditions, + * in the new claim conditions being set. * */ function setClaimConditions(ClaimCondition[] calldata phases, bool resetClaimEligibility) external; diff --git a/contracts/extension/interface/IDrop1155.sol b/contracts/extension/interface/IDrop1155.sol new file mode 100644 index 000000000..4fe60232e --- /dev/null +++ b/contracts/extension/interface/IDrop1155.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./IClaimConditionMultiPhase.sol"; + +/** + * The interface `IDrop1155` is written for thirdweb's 'Drop' contracts, which are distribution mechanisms for tokens. + * + * An authorized wallet can set a series of claim conditions, ordered by their respective `startTimestamp`. + * A claim condition defines criteria under which accounts can mint tokens. Claim conditions can be overwritten + * or added to by the contract admin. At any moment, there is only one active claim condition. + */ + +interface IDrop1155 is IClaimConditionMultiPhase { + /** + * @param proof Proof of concerned wallet's inclusion in an allowlist. + * @param quantityLimitPerWallet The total quantity of tokens the allowlisted wallet is eligible to claim over time. + * @param pricePerToken The price per token the allowlisted wallet must pay to claim tokens. + * @param currency The currency in which the allowlisted wallet must pay the price for claiming tokens. + */ + struct AllowlistProof { + bytes32[] proof; + uint256 quantityLimitPerWallet; + uint256 pricePerToken; + address currency; + } + + /// @notice Emitted when tokens are claimed. + event TokensClaimed( + uint256 indexed claimConditionIndex, + address indexed claimer, + address indexed receiver, + uint256 tokenId, + uint256 quantityClaimed + ); + + /// @notice Emitted when the contract's claim conditions are updated. + event ClaimConditionsUpdated(uint256 indexed tokenId, ClaimCondition[] claimConditions, bool resetEligibility); + + /** + * @notice Lets an account claim a given quantity of NFTs. + * + * @param receiver The receiver of the NFTs to claim. + * @param tokenId The tokenId of the NFT to claim. + * @param quantity The quantity of NFTs to claim. + * @param currency The currency in which to pay for the claim. + * @param pricePerToken The price per token to pay for the claim. + * @param allowlistProof The proof of the claimer's inclusion in the merkle root allowlist + * of the claim conditions that apply. + * @param data Arbitrary bytes data that can be leveraged in the implementation of this interface. + */ + function claim( + address receiver, + uint256 tokenId, + uint256 quantity, + address currency, + uint256 pricePerToken, + AllowlistProof calldata allowlistProof, + bytes memory data + ) external payable; + + /** + * @notice Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. + * + * @param tokenId The token ID for which to set mint conditions. + * @param phases Claim conditions in ascending order by `startTimestamp`. + * + * @param resetClaimEligibility Whether to honor the restrictions applied to wallets who have claimed tokens in the current conditions, + * in the new claim conditions being set. + * + */ + function setClaimConditions(uint256 tokenId, ClaimCondition[] calldata phases, bool resetClaimEligibility) external; +} diff --git a/contracts/extension/interface/IDropSinglePhase.sol b/contracts/extension/interface/IDropSinglePhase.sol index 82d71e69c..92717755c 100644 --- a/contracts/extension/interface/IDropSinglePhase.sol +++ b/contracts/extension/interface/IDropSinglePhase.sol @@ -1,15 +1,33 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + import "./IClaimCondition.sol"; +/** + * The interface `IDropSinglePhase` is written for thirdweb's 'DropSinglePhase' contracts, which are distribution mechanisms for tokens. + * + * An authorized wallet can set a claim condition for the distribution of the contract's tokens. + * A claim condition defines criteria under which accounts can mint tokens. Claim conditions can be overwritten + * or added to by the contract admin. At any moment, there is only one active claim condition. + */ + interface IDropSinglePhase is IClaimCondition { + /** + * @param proof Proof of concerned wallet's inclusion in an allowlist. + * @param quantityLimitPerWallet The total quantity of tokens the allowlisted wallet is eligible to claim over time. + * @param pricePerToken The price per token the allowlisted wallet must pay to claim tokens. + * @param currency The currency in which the allowlisted wallet must pay the price for claiming tokens. + */ struct AllowlistProof { bytes32[] proof; - uint256 maxQuantityInAllowlist; + uint256 quantityLimitPerWallet; + uint256 pricePerToken; + address currency; } - /// @dev Emitted when tokens are claimed via `claim`. + /// @notice Emitted when tokens are claimed via `claim`. event TokensClaimed( address indexed claimer, address indexed receiver, @@ -17,7 +35,7 @@ interface IDropSinglePhase is IClaimCondition { uint256 quantityClaimed ); - /// @dev Emitted when the contract's claim conditions are updated. + /// @notice Emitted when the contract's claim conditions are updated. event ClaimConditionUpdated(ClaimCondition condition, bool resetEligibility); /** @@ -45,8 +63,8 @@ interface IDropSinglePhase is IClaimCondition { * * @param phase Claim condition to set. * - * @param resetClaimEligibility Whether to reset `limitLastClaimTimestamp` and `limitMerkleProofClaim` values when setting new - * claim conditions. + * @param resetClaimEligibility Whether to honor the restrictions applied to wallets who have claimed tokens in the current conditions, + * in the new claim conditions being set. */ function setClaimConditions(ClaimCondition calldata phase, bool resetClaimEligibility) external; } diff --git a/contracts/extension/interface/IDropSinglePhase1155.sol b/contracts/extension/interface/IDropSinglePhase1155.sol new file mode 100644 index 000000000..8c6321095 --- /dev/null +++ b/contracts/extension/interface/IDropSinglePhase1155.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./IClaimCondition.sol"; + +/** + * The interface `IDropSinglePhase1155` is written for thirdweb's 'DropSinglePhase' contracts, which are distribution mechanisms for tokens. + * + * An authorized wallet can set a claim condition for the distribution of the contract's tokens. + * A claim condition defines criteria under which accounts can mint tokens. Claim conditions can be overwritten + * or added to by the contract admin. At any moment, there is only one active claim condition. + */ + +interface IDropSinglePhase1155 is IClaimCondition { + /** + * @param proof Proof of concerned wallet's inclusion in an allowlist. + * @param quantityLimitPerWallet The total quantity of tokens the allowlisted wallet is eligible to claim over time. + * @param pricePerToken The price per token the allowlisted wallet must pay to claim tokens. + * @param currency The currency in which the allowlisted wallet must pay the price for claiming tokens. + */ + struct AllowlistProof { + bytes32[] proof; + uint256 quantityLimitPerWallet; + uint256 pricePerToken; + address currency; + } + + /// @notice Emitted when tokens are claimed via `claim`. + event TokensClaimed( + address indexed claimer, + address indexed receiver, + uint256 indexed tokenId, + uint256 quantityClaimed + ); + + /// @notice Emitted when the contract's claim conditions are updated. + event ClaimConditionUpdated(uint256 indexed tokenId, ClaimCondition condition, bool resetEligibility); + + /** + * @notice Lets an account claim a given quantity of NFTs. + * + * @param receiver The receiver of the NFT to claim. + * @param tokenId The tokenId of the NFT to claim. + * @param quantity The quantity of the NFT to claim. + * @param currency The currency in which to pay for the claim. + * @param pricePerToken The price per token to pay for the claim. + * @param allowlistProof The proof of the claimer's inclusion in the merkle root allowlist + * of the claim conditions that apply. + * @param data Arbitrary bytes data that can be leveraged in the implementation of this interface. + */ + function claim( + address receiver, + uint256 tokenId, + uint256 quantity, + address currency, + uint256 pricePerToken, + AllowlistProof calldata allowlistProof, + bytes memory data + ) external payable; + + /** + * @notice Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. + * + * @param phase Claim condition to set. + * + * @param resetClaimEligibility Whether to honor the restrictions applied to wallets who have claimed tokens in the current conditions, + * in the new claim conditions being set. + * + * @param tokenId The tokenId for which to set the relevant claim condition. + */ + function setClaimConditions(uint256 tokenId, ClaimCondition calldata phase, bool resetClaimEligibility) external; +} diff --git a/contracts/extension/interface/IERC2771Context.sol b/contracts/extension/interface/IERC2771Context.sol new file mode 100644 index 000000000..b943d3cc8 --- /dev/null +++ b/contracts/extension/interface/IERC2771Context.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +interface IERC2771Context { + function isTrustedForwarder(address forwarder) external view returns (bool); +} diff --git a/contracts/extension/interface/ILazyMint.sol b/contracts/extension/interface/ILazyMint.sol index f058861f9..8a71f74ed 100644 --- a/contracts/extension/interface/ILazyMint.sol +++ b/contracts/extension/interface/ILazyMint.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + /** * Thirdweb's `LazyMint` is a contract extension for any base NFT contract. It lets you 'lazy mint' any number of NFTs * at once. Here, 'lazy mint' means defining the metadata for particular tokenIds of your NFT contract, without actually diff --git a/contracts/extension/interface/ILazyMintWithTier.sol b/contracts/extension/interface/ILazyMintWithTier.sol new file mode 100644 index 000000000..e3a4caa6f --- /dev/null +++ b/contracts/extension/interface/ILazyMintWithTier.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +/** + * Thirdweb's `LazyMintWithTier` is a contract extension for any base NFT contract. It lets you 'lazy mint' any number of NFTs + * at once, for a particular tier. Here, 'lazy mint' means defining the metadata for particular tokenIds of your NFT contract, + * without actually minting a non-zero balance of NFTs of those tokenIds. + */ + +interface ILazyMintWithTier { + /// @dev Emitted when tokens are lazy minted. + event TokensLazyMinted( + string indexed tier, + uint256 indexed startTokenId, + uint256 endTokenId, + string baseURI, + bytes encryptedBaseURI + ); + + /** + * @notice Lazy mints a given amount of NFTs. + * + * @param amount The number of NFTs to lazy mint. + * + * @param baseURIForTokens The base URI for the 'n' number of NFTs being lazy minted, where the metadata for each + * of those NFTs is `${baseURIForTokens}/${tokenId}`. + * + * @param tier The tier for which these tokens are being lazy mitned. Here, `tier` is a unique string label + * that is used to group together different batches of lazy minted tokens under a common category. + * + * @param extraData Additional bytes data to be used at the discretion of the consumer of the contract. + * + * @return batchId A unique integer identifier for the batch of NFTs lazy minted together. + */ + function lazyMint( + uint256 amount, + string calldata baseURIForTokens, + string calldata tier, + bytes calldata extraData + ) external returns (uint256 batchId); +} diff --git a/contracts/extension/interface/IMintableERC1155.sol b/contracts/extension/interface/IMintableERC1155.sol index b6fd4f072..7b45769f6 100644 --- a/contracts/extension/interface/IMintableERC1155.sol +++ b/contracts/extension/interface/IMintableERC1155.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + /** * `SignatureMint1155` is an ERC 1155 contract. It lets anyone mint NFTs by producing a mint request * and a signature (produced by an account with MINTER_ROLE, signing the mint request). @@ -18,10 +20,5 @@ interface IMintableERC1155 { * @param amount The number of copies of the NFT to mint. * */ - function mintTo( - address to, - uint256 tokenId, - string calldata uri, - uint256 amount - ) external; + function mintTo(address to, uint256 tokenId, string calldata uri, uint256 amount) external; } diff --git a/contracts/extension/interface/IMintableERC20.sol b/contracts/extension/interface/IMintableERC20.sol index 89ef788f1..9d42a04dc 100644 --- a/contracts/extension/interface/IMintableERC20.sol +++ b/contracts/extension/interface/IMintableERC20.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + interface IMintableERC20 { /// @dev Emitted when tokens are minted with `mintTo` event TokensMinted(address indexed mintedTo, uint256 quantityMinted); diff --git a/contracts/extension/interface/IMintableERC721.sol b/contracts/extension/interface/IMintableERC721.sol index 49fae828e..b0316c7b9 100644 --- a/contracts/extension/interface/IMintableERC721.sol +++ b/contracts/extension/interface/IMintableERC721.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + interface IMintableERC721 { /// @dev Emitted when tokens are minted via `mintTo` event TokensMinted(address indexed mintedTo, uint256 indexed tokenIdMinted, string uri); diff --git a/contracts/extension/interface/IMulticall.sol b/contracts/extension/interface/IMulticall.sol index 0b3e4e604..e96e0b85e 100644 --- a/contracts/extension/interface/IMulticall.sol +++ b/contracts/extension/interface/IMulticall.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.5.0) (utils/Multicall.sol) - pragma solidity ^0.8.0; +/// @author thirdweb + /** * @dev Provides a function to batch together multiple calls in a single external call. * diff --git a/contracts/extension/interface/INFTMetadata.sol b/contracts/extension/interface/INFTMetadata.sol new file mode 100644 index 000000000..fb8dbdfc9 --- /dev/null +++ b/contracts/extension/interface/INFTMetadata.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../eip/interface/IERC4906.sol"; + +interface INFTMetadata is IERC4906 { + /// @dev This event emits when the metadata of all tokens are frozen. + /// While not currently supported by marketplaces, this event allows + /// future indexing if desired. + event MetadataFrozen(); + + /// @notice Sets the metadata URI for a given NFT. + function setTokenURI(uint256 _tokenId, string memory _uri) external; + + /// @notice Freezes the metadata URI for a given NFT. + function freezeMetadata() external; +} diff --git a/contracts/extension/interface/IOperatorFilterRegistry.sol b/contracts/extension/interface/IOperatorFilterRegistry.sol new file mode 100644 index 000000000..4b756a17c --- /dev/null +++ b/contracts/extension/interface/IOperatorFilterRegistry.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +interface IOperatorFilterRegistry { + function isOperatorAllowed(address registrant, address operator) external view returns (bool); + + function register(address registrant) external; + + function registerAndSubscribe(address registrant, address subscription) external; + + function registerAndCopyEntries(address registrant, address registrantToCopy) external; + + function unregister(address addr) external; + + function updateOperator(address registrant, address operator, bool filtered) external; + + function updateOperators(address registrant, address[] calldata operators, bool filtered) external; + + function updateCodeHash(address registrant, bytes32 codehash, bool filtered) external; + + function updateCodeHashes(address registrant, bytes32[] calldata codeHashes, bool filtered) external; + + function subscribe(address registrant, address registrantToSubscribe) external; + + function unsubscribe(address registrant, bool copyExistingEntries) external; + + function subscriptionOf(address addr) external returns (address registrant); + + function subscribers(address registrant) external returns (address[] memory); + + function subscriberAt(address registrant, uint256 index) external returns (address); + + function copyEntriesOf(address registrant, address registrantToCopy) external; + + function isOperatorFiltered(address registrant, address operator) external returns (bool); + + function isCodeHashOfFiltered(address registrant, address operatorWithCode) external returns (bool); + + function isCodeHashFiltered(address registrant, bytes32 codeHash) external returns (bool); + + function filteredOperators(address addr) external returns (address[] memory); + + function filteredCodeHashes(address addr) external returns (bytes32[] memory); + + function filteredOperatorAt(address registrant, uint256 index) external returns (address); + + function filteredCodeHashAt(address registrant, uint256 index) external returns (bytes32); + + function isRegistered(address addr) external returns (bool); + + function codeHashOf(address addr) external returns (bytes32); +} diff --git a/contracts/extension/interface/IOperatorFilterToggle.sol b/contracts/extension/interface/IOperatorFilterToggle.sol new file mode 100644 index 000000000..65f7e23a1 --- /dev/null +++ b/contracts/extension/interface/IOperatorFilterToggle.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +interface IOperatorFilterToggle { + event OperatorRestriction(bool restriction); + + function operatorRestriction() external view returns (bool); + + function setOperatorRestriction(bool restriction) external; +} diff --git a/contracts/extension/interface/IOwnable.sol b/contracts/extension/interface/IOwnable.sol index f983e2649..e96008a07 100644 --- a/contracts/extension/interface/IOwnable.sol +++ b/contracts/extension/interface/IOwnable.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + /** * Thirdweb's `Ownable` is a contract extension to be used with any base contract. It exposes functions for setting and reading * who the 'owner' of the inheriting smart contract is, and lets the inheriting contract perform conditional logic that uses diff --git a/contracts/extension/interface/IPermissions.sol b/contracts/extension/interface/IPermissions.sol index bee59b57d..7bd6e8c8b 100644 --- a/contracts/extension/interface/IPermissions.sol +++ b/contracts/extension/interface/IPermissions.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + /** * @dev External interface of AccessControl declared to support ERC165 detection. */ diff --git a/contracts/extension/interface/IPermissionsEnumerable.sol b/contracts/extension/interface/IPermissionsEnumerable.sol index 84794fee0..977bce6d5 100644 --- a/contracts/extension/interface/IPermissionsEnumerable.sol +++ b/contracts/extension/interface/IPermissionsEnumerable.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + import "./IPermissions.sol"; /** @@ -16,7 +18,7 @@ interface IPermissionsEnumerable is IPermissions { * * WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure * you perform all queries on the same block. See the following - * https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] + * [forum post](https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296) * for more information. */ function getRoleMember(bytes32 role, uint256 index) external view returns (address); diff --git a/contracts/extension/interface/IPlatformFee.sol b/contracts/extension/interface/IPlatformFee.sol index 4b65aa30d..1a1fc778a 100644 --- a/contracts/extension/interface/IPlatformFee.sol +++ b/contracts/extension/interface/IPlatformFee.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + /** * Thirdweb's `PlatformFee` is a contract extension to be used with any base contract. It exposes functions for setting and reading * the recipient of platform fee and the platform fee basis points, and lets the inheriting contract perform conditional logic @@ -8,6 +10,12 @@ pragma solidity ^0.8.0; */ interface IPlatformFee { + /// @dev Fee type variants: percentage fee and flat fee + enum PlatformFeeType { + Bps, + Flat + } + /// @dev Returns the platform fee bps and recipient. function getPlatformFeeInfo() external view returns (address, uint16); @@ -16,4 +24,10 @@ interface IPlatformFee { /// @dev Emitted when fee on primary sales is updated. event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps); + + /// @dev Emitted when the flat platform fee is updated. + event FlatPlatformFeeUpdated(address platformFeeRecipient, uint256 flatFee); + + /// @dev Emitted when the platform fee type is updated. + event PlatformFeeTypeUpdated(PlatformFeeType feeType); } diff --git a/contracts/extension/interface/IPrimarySale.sol b/contracts/extension/interface/IPrimarySale.sol index 6a28e9e93..6ca726842 100644 --- a/contracts/extension/interface/IPrimarySale.sol +++ b/contracts/extension/interface/IPrimarySale.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + /** * Thirdweb's `Primary` is a contract extension to be used with any base contract. It exposes functions for setting and reading * the recipient of primary sales, and lets the inheriting contract perform conditional logic that uses information about diff --git a/contracts/extension/interface/IRoyalty.sol b/contracts/extension/interface/IRoyalty.sol index 0e963ca20..f87cdff9c 100644 --- a/contracts/extension/interface/IRoyalty.sol +++ b/contracts/extension/interface/IRoyalty.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + import "../../eip/interface/IERC2981.sol"; /** @@ -24,11 +26,7 @@ interface IRoyalty is IERC2981 { function setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) external; /// @dev Lets a module admin set the royalty recipient for a particular token Id. - function setRoyaltyInfoForToken( - uint256 tokenId, - address recipient, - uint256 bps - ) external; + function setRoyaltyInfoForToken(uint256 tokenId, address recipient, uint256 bps) external; /// @dev Returns the royalty recipient for a particular token Id. function getRoyaltyInfoForToken(uint256 tokenId) external view returns (address, uint16); diff --git a/contracts/extension/interface/IRoyaltyEngineV1.sol b/contracts/extension/interface/IRoyaltyEngineV1.sol new file mode 100644 index 000000000..819427c5d --- /dev/null +++ b/contracts/extension/interface/IRoyaltyEngineV1.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/// @author: manifold.xyz + +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +/** + * @dev Lookup engine interface + */ +interface IRoyaltyEngineV1 is IERC165 { + /** + * Get the royalty for a given token (address, id) and value amount. Does not cache the bps/amounts. Caches the spec for a given token address + * + * @param tokenAddress - The address of the token + * @param tokenId - The id of the token + * @param value - The value you wish to get the royalty of + * + * returns Two arrays of equal length, royalty recipients and the corresponding amount each recipient should get + */ + function getRoyalty( + address tokenAddress, + uint256 tokenId, + uint256 value + ) external returns (address payable[] memory recipients, uint256[] memory amounts); + + /** + * View only version of getRoyalty + * + * @param tokenAddress - The address of the token + * @param tokenId - The id of the token + * @param value - The value you wish to get the royalty of + * + * returns Two arrays of equal length, royalty recipients and the corresponding amount each recipient should get + */ + function getRoyaltyView( + address tokenAddress, + uint256 tokenId, + uint256 value + ) external view returns (address payable[] memory recipients, uint256[] memory amounts); +} diff --git a/contracts/extension/interface/IRoyaltyPayments.sol b/contracts/extension/interface/IRoyaltyPayments.sol new file mode 100644 index 000000000..cb7982c05 --- /dev/null +++ b/contracts/extension/interface/IRoyaltyPayments.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +/** + * @dev Read royalty info for a token. + * Supports RoyaltyEngineV1 and RoyaltyRegistry by manifold.xyz. + */ +interface IRoyaltyPayments is IERC165 { + /// @dev Emitted when the address of RoyaltyEngine is set or updated. + event RoyaltyEngineUpdated(address indexed previousAddress, address indexed newAddress); + + /** + * Get the royalty for a given token (address, id) and value amount. + * + * @param tokenAddress - The address of the token + * @param tokenId - The id of the token + * @param value - The value you wish to get the royalty of + * + * returns Two arrays of equal length, royalty recipients and the corresponding amount each recipient should get + */ + function getRoyalty( + address tokenAddress, + uint256 tokenId, + uint256 value + ) external returns (address payable[] memory recipients, uint256[] memory amounts); + + /** + * Set or override RoyaltyEngine address + * + * @param _royaltyEngineAddress - RoyaltyEngineV1 address + */ + function setRoyaltyEngine(address _royaltyEngineAddress) external; +} diff --git a/contracts/extension/interface/IRulesEngine.sol b/contracts/extension/interface/IRulesEngine.sol new file mode 100644 index 000000000..3c19c6558 --- /dev/null +++ b/contracts/extension/interface/IRulesEngine.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +interface IRulesEngine { + enum TokenType { + ERC20, + ERC721, + ERC1155 + } + + enum RuleType { + Threshold, + Multiplicative + } + + struct RuleTypeThreshold { + address token; + TokenType tokenType; + uint256 tokenId; + uint256 balance; + uint256 score; + } + + struct RuleTypeMultiplicative { + address token; + TokenType tokenType; + uint256 tokenId; + uint256 scorePerOwnedToken; + } + + struct RuleWithId { + bytes32 ruleId; + address token; + TokenType tokenType; + uint256 tokenId; + uint256 balance; + uint256 score; + RuleType ruleType; + } + + event RuleCreated(bytes32 indexed ruleId, RuleWithId rule); + event RuleDeleted(bytes32 indexed ruleId); + event RulesEngineOverriden(address indexed newRulesEngine); + + function getScore(address _tokenOwner) external view returns (uint256 score); + + function getAllRules() external view returns (RuleWithId[] memory rules); + + function getRulesEngineOverride() external view returns (address rulesEngineAddress); + + function createRuleMultiplicative(RuleTypeMultiplicative memory rule) external returns (bytes32 ruleId); + + function createRuleThreshold(RuleTypeThreshold memory rule) external returns (bytes32 ruleId); + + function deleteRule(bytes32 ruleId) external; + + function setRulesEngineOverride(address _rulesEngineAddress) external; +} diff --git a/contracts/extension/interface/ISharedMetadata.sol b/contracts/extension/interface/ISharedMetadata.sol new file mode 100644 index 000000000..5b816d2a1 --- /dev/null +++ b/contracts/extension/interface/ISharedMetadata.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.10; + +/// @author thirdweb + +interface ISharedMetadata { + /// @notice Emitted when shared metadata is lazy minted. + event SharedMetadataUpdated(string name, string description, string imageURI, string animationURI); + + /** + * @notice Structure for metadata shared across all tokens + * + * @param name Shared name of NFT in metadata + * @param description Shared description of NFT in metadata + * @param imageURI Shared URI of image to render for NFTs + * @param animationURI Shared URI of animation to render for NFTs + */ + struct SharedMetadataInfo { + string name; + string description; + string imageURI; + string animationURI; + } + + /** + * @notice Set shared metadata for NFTs + * @param _metadata common metadata for all tokens + */ + function setSharedMetadata(SharedMetadataInfo calldata _metadata) external; +} diff --git a/contracts/extension/interface/ISharedMetadataBatch.sol b/contracts/extension/interface/ISharedMetadataBatch.sol new file mode 100644 index 000000000..e6bc915c0 --- /dev/null +++ b/contracts/extension/interface/ISharedMetadataBatch.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.10; + +/// @author thirdweb + +interface ISharedMetadataBatch { + /// @notice Emitted when shared metadata is lazy minted. + event SharedMetadataUpdated( + bytes32 indexed id, + string name, + string description, + string imageURI, + string animationURI + ); + + /// @notice Emitted when shared metadata is deleted. + event SharedMetadataDeleted(bytes32 indexed id); + + /** + * @notice Structure for metadata shared across all tokens + * + * @param name Shared name of NFT in metadata + * @param description Shared description of NFT in metadata + * @param imageURI Shared URI of image to render for NFTs + * @param animationURI Shared URI of animation to render for NFTs + */ + struct SharedMetadataInfo { + string name; + string description; + string imageURI; + string animationURI; + } + + struct SharedMetadataWithId { + bytes32 id; + SharedMetadataInfo metadata; + } + + /** + * @notice Set shared metadata for NFTs + * @param metadata common metadata for all tokens + * @param id UID for the metadata + */ + function setSharedMetadata(SharedMetadataInfo calldata metadata, bytes32 id) external; + + /** + * @notice Delete shared metadata for NFTs + * @param id UID for the metadata + */ + function deleteSharedMetadata(bytes32 id) external; + + /** + * @notice Get all shared metadata + * @return metadata array of all shared metadata + */ + function getAllSharedMetadata() external view returns (SharedMetadataWithId[] memory metadata); +} diff --git a/contracts/extension/interface/ISignatureAction.sol b/contracts/extension/interface/ISignatureAction.sol new file mode 100644 index 000000000..dd98f5d49 --- /dev/null +++ b/contracts/extension/interface/ISignatureAction.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +/** + * thirdweb's `SignatureAction` extension smart contract can be used with any base smart contract. It provides a generic + * payload struct that can be signed by an authorized wallet and verified by the contract. The bytes `data` field provided + * in the payload can be abi encoded <-> decoded to use `SignatureContract` for any authorized signature action. + */ + +interface ISignatureAction { + /** + * @notice The payload that must be signed by an authorized wallet. + * + * @param validityStartTimestamp The UNIX timestamp at and after which a signature is valid. + * @param validityEndTimestamp The UNIX timestamp at and after which a signature is invalid/expired. + * @param uid A unique non-repeatable ID for the payload. + * @param data Arbitrary bytes data to be used at the discretion of the contract. + */ + struct GenericRequest { + uint128 validityStartTimestamp; + uint128 validityEndTimestamp; + bytes32 uid; + bytes data; + } + + /// @notice Emitted when a payload is verified and executed. + event RequestExecuted(address indexed user, address indexed signer, GenericRequest _req); + + /** + * @notice Verfies that a payload is signed by an authorized wallet. + * + * @param req The payload signed by the authorized wallet. + * @param signature The signature produced by the authorized wallet signing the given payload. + * + * @return success Whether the payload is signed by the authorized wallet. + * @return signer The address of the signer. + */ + function verify( + GenericRequest calldata req, + bytes calldata signature + ) external view returns (bool success, address signer); +} diff --git a/contracts/extension/interface/ISignatureMintERC1155.sol b/contracts/extension/interface/ISignatureMintERC1155.sol index a08f2c534..9f283eff0 100644 --- a/contracts/extension/interface/ISignatureMintERC1155.sol +++ b/contracts/extension/interface/ISignatureMintERC1155.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + /** * The 'signature minting' mechanism used in thirdweb Token smart contracts is a way for a contract admin to authorize an external party's * request to mint tokens on the admin's contract. @@ -57,10 +59,10 @@ interface ISignatureMintERC1155 { * * returns (success, signer) Result of verification and the recovered address. */ - function verify(MintRequest calldata req, bytes calldata signature) - external - view - returns (bool success, address signer); + function verify( + MintRequest calldata req, + bytes calldata signature + ) external view returns (bool success, address signer); /** * @notice Mints tokens according to the provided mint request. @@ -68,8 +70,8 @@ interface ISignatureMintERC1155 { * @param req The payload / mint request. * @param signature The signature produced by an account signing the mint request. */ - function mintWithSignature(MintRequest calldata req, bytes calldata signature) - external - payable - returns (address signer); + function mintWithSignature( + MintRequest calldata req, + bytes calldata signature + ) external payable returns (address signer); } diff --git a/contracts/extension/interface/ISignatureMintERC20.sol b/contracts/extension/interface/ISignatureMintERC20.sol index 80922d46e..63aabbc33 100644 --- a/contracts/extension/interface/ISignatureMintERC20.sol +++ b/contracts/extension/interface/ISignatureMintERC20.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + /** * The 'signature minting' mechanism used in thirdweb Token smart contracts is a way for a contract admin to authorize an external party's * request to mint tokens on the admin's contract. @@ -25,7 +27,7 @@ interface ISignatureMintERC20 { address to; address primarySaleRecipient; uint256 quantity; - uint256 pricePerToken; + uint256 price; address currency; uint128 validityStartTimestamp; uint128 validityEndTimestamp; @@ -44,10 +46,10 @@ interface ISignatureMintERC20 { * * returns (success, signer) Result of verification and the recovered address. */ - function verify(MintRequest calldata req, bytes calldata signature) - external - view - returns (bool success, address signer); + function verify( + MintRequest calldata req, + bytes calldata signature + ) external view returns (bool success, address signer); /** * @notice Mints tokens according to the provided mint request. @@ -55,8 +57,8 @@ interface ISignatureMintERC20 { * @param req The payload / mint request. * @param signature The signature produced by an account signing the mint request. */ - function mintWithSignature(MintRequest calldata req, bytes calldata signature) - external - payable - returns (address signer); + function mintWithSignature( + MintRequest calldata req, + bytes calldata signature + ) external payable returns (address signer); } diff --git a/contracts/extension/interface/ISignatureMintERC721.sol b/contracts/extension/interface/ISignatureMintERC721.sol index 0edfd9769..0478adae1 100644 --- a/contracts/extension/interface/ISignatureMintERC721.sol +++ b/contracts/extension/interface/ISignatureMintERC721.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + /** * The 'signature minting' mechanism used in thirdweb Token smart contracts is a way for a contract admin to authorize an external party's * request to mint tokens on the admin's contract. @@ -55,10 +57,10 @@ interface ISignatureMintERC721 { * * returns (success, signer) Result of verification and the recovered address. */ - function verify(MintRequest calldata req, bytes calldata signature) - external - view - returns (bool success, address signer); + function verify( + MintRequest calldata req, + bytes calldata signature + ) external view returns (bool success, address signer); /** * @notice Mints tokens according to the provided mint request. @@ -66,8 +68,8 @@ interface ISignatureMintERC721 { * @param req The payload / mint request. * @param signature The signature produced by an account signing the mint request. */ - function mintWithSignature(MintRequest calldata req, bytes calldata signature) - external - payable - returns (address signer); + function mintWithSignature( + MintRequest calldata req, + bytes calldata signature + ) external payable returns (address signer); } diff --git a/contracts/extension/interface/IStaking1155.sol b/contracts/extension/interface/IStaking1155.sol new file mode 100644 index 000000000..27351e332 --- /dev/null +++ b/contracts/extension/interface/IStaking1155.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +interface IStaking1155 { + /// @dev Emitted when tokens are staked. + event TokensStaked(address indexed staker, uint256 indexed tokenId, uint256 amount); + + /// @dev Emitted when a set of staked token-ids are withdrawn. + event TokensWithdrawn(address indexed staker, uint256 indexed tokenId, uint256 amount); + + /// @dev Emitted when a staker claims staking rewards. + event RewardsClaimed(address indexed staker, uint256 rewardAmount); + + /// @dev Emitted when contract admin updates timeUnit. + event UpdatedTimeUnit(uint256 indexed _tokenId, uint256 oldTimeUnit, uint256 newTimeUnit); + + /// @dev Emitted when contract admin updates rewardsPerUnitTime. + event UpdatedRewardsPerUnitTime( + uint256 indexed _tokenId, + uint256 oldRewardsPerUnitTime, + uint256 newRewardsPerUnitTime + ); + + /// @dev Emitted when contract admin updates timeUnit. + event UpdatedDefaultTimeUnit(uint256 oldTimeUnit, uint256 newTimeUnit); + + /// @dev Emitted when contract admin updates rewardsPerUnitTime. + event UpdatedDefaultRewardsPerUnitTime(uint256 oldRewardsPerUnitTime, uint256 newRewardsPerUnitTime); + + /** + * @notice Staker Info. + * + * @param amountStaked Total number of tokens staked by the staker. + * + * @param timeOfLastUpdate Last reward-update timestamp. + * + * @param unclaimedRewards Rewards accumulated but not claimed by user yet. + * + * @param conditionIdOflastUpdate Condition-Id when rewards were last updated for user. + */ + struct Staker { + uint64 conditionIdOflastUpdate; + uint64 amountStaked; + uint128 timeOfLastUpdate; + uint256 unclaimedRewards; + } + + /** + * @notice Staking Condition. + * + * @param timeUnit Unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc. + * + * @param rewardsPerUnitTime Rewards accumulated per unit of time. + * + * @param startTimestamp Condition start timestamp. + * + * @param endTimestamp Condition end timestamp. + */ + struct StakingCondition { + uint80 timeUnit; + uint80 startTimestamp; + uint80 endTimestamp; + uint256 rewardsPerUnitTime; + } + + /** + * @notice Stake ERC721 Tokens. + * + * @param tokenId ERC1155 token-id to stake. + * @param amount Amount to stake. + */ + function stake(uint256 tokenId, uint64 amount) external; + + /** + * @notice Withdraw staked tokens. + * + * @param tokenId ERC1155 token-id to withdraw. + * @param amount Amount to withdraw. + */ + function withdraw(uint256 tokenId, uint64 amount) external; + + /** + * @notice Claim accumulated rewards. + * + * @param tokenId Staked token Id. + */ + function claimRewards(uint256 tokenId) external; + + /** + * @notice View amount staked and total rewards for a user. + * + * @param tokenId Staked token Id. + * @param staker Address for which to calculated rewards. + */ + function getStakeInfoForToken( + uint256 tokenId, + address staker + ) external view returns (uint256 _tokensStaked, uint256 _rewards); + + /** + * @notice View amount staked and total rewards for a user. + * + * @param staker Address for which to calculated rewards. + */ + function getStakeInfo( + address staker + ) external view returns (uint256[] memory _tokensStaked, uint256[] memory _tokenAmounts, uint256 _totalRewards); +} diff --git a/contracts/extension/interface/IStaking20.sol b/contracts/extension/interface/IStaking20.sol new file mode 100644 index 000000000..494a0d0a5 --- /dev/null +++ b/contracts/extension/interface/IStaking20.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +interface IStaking20 { + /// @dev Emitted when tokens are staked. + event TokensStaked(address indexed staker, uint256 amount); + + /// @dev Emitted when a tokens are withdrawn. + event TokensWithdrawn(address indexed staker, uint256 amount); + + /// @dev Emitted when a staker claims staking rewards. + event RewardsClaimed(address indexed staker, uint256 rewardAmount); + + /// @dev Emitted when contract admin updates timeUnit. + event UpdatedTimeUnit(uint256 oldTimeUnit, uint256 newTimeUnit); + + /// @dev Emitted when contract admin updates rewardsPerUnitTime. + event UpdatedRewardRatio( + uint256 oldNumerator, + uint256 newNumerator, + uint256 oldDenominator, + uint256 newDenominator + ); + + /// @dev Emitted when contract admin updates minimum staking amount. + event UpdatedMinStakeAmount(uint256 oldAmount, uint256 newAmount); + + /** + * @notice Staker Info. + * + * @param amountStaked Total number of tokens staked by the staker. + * + * @param timeOfLastUpdate Last reward-update timestamp. + * + * @param unclaimedRewards Rewards accumulated but not claimed by user yet. + * + * @param conditionIdOflastUpdate Condition-Id when rewards were last updated for user. + */ + struct Staker { + uint128 timeOfLastUpdate; + uint64 conditionIdOflastUpdate; + uint256 amountStaked; + uint256 unclaimedRewards; + } + + /** + * @notice Staking Condition. + * + * @param timeUnit Unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc. + * + * @param rewardRatioNumerator Rewards ratio is the number of reward tokens for a number of staked tokens, + * per unit of time. + * + * @param rewardRatioDenominator Rewards ratio is the number of reward tokens for a number of staked tokens, + * per unit of time. + * + * @param startTimestamp Condition start timestamp. + * + * @param endTimestamp Condition end timestamp. + */ + struct StakingCondition { + uint80 timeUnit; + uint80 startTimestamp; + uint80 endTimestamp; + uint256 rewardRatioNumerator; + uint256 rewardRatioDenominator; + } + + /** + * @notice Stake ERC721 Tokens. + * + * @param amount Amount to stake. + */ + function stake(uint256 amount) external payable; + + /** + * @notice Withdraw staked tokens. + * + * @param amount Amount to withdraw. + */ + function withdraw(uint256 amount) external; + + /** + * @notice Claim accumulated rewards. + * + */ + function claimRewards() external; + + /** + * @notice View amount staked and total rewards for a user. + * + * @param staker Address for which to calculated rewards. + */ + function getStakeInfo(address staker) external view returns (uint256 _tokensStaked, uint256 _rewards); +} diff --git a/contracts/extension/interface/IStaking721.sol b/contracts/extension/interface/IStaking721.sol new file mode 100644 index 000000000..21ef88b38 --- /dev/null +++ b/contracts/extension/interface/IStaking721.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +interface IStaking721 { + /// @dev Emitted when a set of token-ids are staked. + event TokensStaked(address indexed staker, uint256[] indexed tokenIds); + + /// @dev Emitted when a set of staked token-ids are withdrawn. + event TokensWithdrawn(address indexed staker, uint256[] indexed tokenIds); + + /// @dev Emitted when a staker claims staking rewards. + event RewardsClaimed(address indexed staker, uint256 rewardAmount); + + /// @dev Emitted when contract admin updates timeUnit. + event UpdatedTimeUnit(uint256 oldTimeUnit, uint256 newTimeUnit); + + /// @dev Emitted when contract admin updates rewardsPerUnitTime. + event UpdatedRewardsPerUnitTime(uint256 oldRewardsPerUnitTime, uint256 newRewardsPerUnitTime); + + /** + * @notice Staker Info. + * + * @param amountStaked Total number of tokens staked by the staker. + * + * @param timeOfLastUpdate Last reward-update timestamp. + * + * @param unclaimedRewards Rewards accumulated but not claimed by user yet. + * + * @param conditionIdOflastUpdate Condition-Id when rewards were last updated for user. + */ + struct Staker { + uint64 amountStaked; + uint64 conditionIdOflastUpdate; + uint128 timeOfLastUpdate; + uint256 unclaimedRewards; + } + + /** + * @notice Staking Condition. + * + * @param timeUnit Unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc. + * + * @param rewardsPerUnitTime Rewards accumulated per unit of time. + * + * @param startTimestamp Condition start timestamp. + * + * @param endTimestamp Condition end timestamp. + */ + struct StakingCondition { + uint256 timeUnit; + uint256 rewardsPerUnitTime; + uint256 startTimestamp; + uint256 endTimestamp; + } + + /** + * @notice Stake ERC721 Tokens. + * + * @param tokenIds List of tokens to stake. + */ + function stake(uint256[] calldata tokenIds) external; + + /** + * @notice Withdraw staked tokens. + * + * @param tokenIds List of tokens to withdraw. + */ + function withdraw(uint256[] calldata tokenIds) external; + + /** + * @notice Claim accumulated rewards. + */ + function claimRewards() external; + + /** + * @notice View amount staked and total rewards for a user. + * + * @param staker Address for which to calculated rewards. + */ + function getStakeInfo(address staker) external view returns (uint256[] memory _tokensStaked, uint256 _rewards); +} diff --git a/contracts/extension/interface/ITokenBundle.sol b/contracts/extension/interface/ITokenBundle.sol index 667968885..ee63551f5 100644 --- a/contracts/extension/interface/ITokenBundle.sol +++ b/contracts/extension/interface/ITokenBundle.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +/// @author thirdweb + /** * Group together arbitrary ERC20, ERC721 and ERC1155 tokens into a single bundle. * diff --git a/contracts/extension/interface/plugin/IContext.sol b/contracts/extension/interface/plugin/IContext.sol new file mode 100644 index 000000000..96da340a1 --- /dev/null +++ b/contracts/extension/interface/plugin/IContext.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +interface IContext { + function _msgSender() external view returns (address sender); + + function _msgData() external view returns (bytes calldata); +} diff --git a/contracts/extension/interface/plugin/IPluginMap.sol b/contracts/extension/interface/plugin/IPluginMap.sol new file mode 100644 index 000000000..9053a06e6 --- /dev/null +++ b/contracts/extension/interface/plugin/IPluginMap.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +interface IPluginMap { + /** + * @notice An interface to describe a plug-in. + * + * @param functionSelector 4-byte function selector. + * @param functionSignature Function representation as a string. E.g. "transfer(address,address,uint256)" + * @param pluginAddress Address of the contract containing the function. + */ + struct Plugin { + bytes4 functionSelector; + string functionSignature; + address pluginAddress; + } + + /// @dev Emitted when a function selector is mapped to a particular plug-in smart contract, during construction of Map. + event PluginSet(bytes4 indexed functionSelector, string indexed functionSignature, address indexed pluginAddress); + + /// @dev Returns the plug-in contract for a given function. + function getPluginForFunction(bytes4 functionSelector) external view returns (address); + + /// @dev Returns all functions that are mapped to the given plug-in contract. + function getAllFunctionsOfPlugin(address pluginAddress) external view returns (bytes4[] memory); + + /// @dev Returns all plug-ins known by Map. + function getAllPlugins() external view returns (Plugin[] memory); +} diff --git a/contracts/extension/interface/plugin/IRouter.sol b/contracts/extension/interface/plugin/IRouter.sol new file mode 100644 index 000000000..0fe7a5670 --- /dev/null +++ b/contracts/extension/interface/plugin/IRouter.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +import "./IPluginMap.sol"; + +interface IRouter is IPluginMap { + /// @dev Emitted when a functionality is added, or plugged-in. + event PluginAdded(bytes4 indexed functionSelector, address indexed pluginAddress); + + /// @dev Emitted when a functionality is updated or overridden. + event PluginUpdated( + bytes4 indexed functionSelector, + address indexed oldPluginAddress, + address indexed newPluginAddress + ); + + /// @dev Emitted when a functionality is removed. + event PluginRemoved(bytes4 indexed functionSelector, address indexed pluginAddress); + + /// @dev Add a new plugin to the contract. + function addPlugin(Plugin memory plugin) external; + + /// @dev Update / override an existing plugin. + function updatePlugin(Plugin memory plugin) external; + + /// @dev Remove an existing plugin from the contract. + function removePlugin(bytes4 functionSelector) external; +} diff --git a/contracts/extension/plugin/ContractMetadataLogic.sol b/contracts/extension/plugin/ContractMetadataLogic.sol new file mode 100644 index 000000000..724aeb936 --- /dev/null +++ b/contracts/extension/plugin/ContractMetadataLogic.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./ContractMetadataStorage.sol"; +import "../interface/IContractMetadata.sol"; + +/** + * @author thirdweb.com + * + * @title Contract Metadata + * @notice Thirdweb's `ContractMetadata` is a contract extension for any base contracts. It lets you set a metadata URI + * for you contract. + * Additionally, `ContractMetadata` is necessary for NFT contracts that want royalties to get distributed on OpenSea. + */ + +abstract contract ContractMetadataLogic is IContractMetadata { + /// @dev Returns the metadata URI of the contract. + function contractURI() public view returns (string memory) { + ContractMetadataStorage.Data storage data = ContractMetadataStorage.contractMetadataStorage(); + return data.contractURI; + } + + /** + * @notice Lets a contract admin set the URI for contract-level metadata. + * @dev Caller should be authorized to setup contractURI, e.g. contract admin. + * See {_canSetContractURI}. + * Emits {ContractURIUpdated Event}. + * + * @param _uri keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + */ + function setContractURI(string memory _uri) external override { + if (!_canSetContractURI()) { + revert("Not authorized"); + } + + _setupContractURI(_uri); + } + + /// @dev Lets a contract admin set the URI for contract-level metadata. + function _setupContractURI(string memory _uri) internal { + ContractMetadataStorage.Data storage data = ContractMetadataStorage.contractMetadataStorage(); + string memory prevURI = data.contractURI; + data.contractURI = _uri; + + emit ContractURIUpdated(prevURI, _uri); + } + + /// @dev Returns whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view virtual returns (bool); +} diff --git a/contracts/extension/plugin/ContractMetadataStorage.sol b/contracts/extension/plugin/ContractMetadataStorage.sol new file mode 100644 index 000000000..c6d9bfb85 --- /dev/null +++ b/contracts/extension/plugin/ContractMetadataStorage.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +/** + * @author thirdweb.com + */ +library ContractMetadataStorage { + /// @custom:storage-location erc7201:contract.metadata.storage + /// @dev keccak256(abi.encode(uint256(keccak256("contract.metadata.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant CONTRACT_METADATA_STORAGE_POSITION = + 0x4bc804ba64359c0e35e5ed5d90ee596ecaa49a3a930ddcb1470ea0dd625da900; + + struct Data { + string contractURI; + } + + function contractMetadataStorage() internal pure returns (Data storage contractMetadataData) { + bytes32 position = CONTRACT_METADATA_STORAGE_POSITION; + assembly { + contractMetadataData.slot := position + } + } +} diff --git a/contracts/extension/plugin/ERC2771ContextConsumer.sol b/contracts/extension/plugin/ERC2771ContextConsumer.sol new file mode 100644 index 000000000..6e9c236c5 --- /dev/null +++ b/contracts/extension/plugin/ERC2771ContextConsumer.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./ERC2771ContextLogic.sol"; + +interface IERC2771Context { + function isTrustedForwarder(address forwarder) external view returns (bool); +} + +/** + * @dev Context variant with ERC2771 support. + */ +abstract contract ERC2771ContextConsumer { + function _msgSender() public view virtual returns (address sender) { + if (IERC2771Context(address(this)).isTrustedForwarder(msg.sender)) { + // The assembly code is more direct than the Solidity version using `abi.decode`. + assembly { + sender := shr(96, calldataload(sub(calldatasize(), 20))) + } + } else { + return msg.sender; + } + } + + function _msgData() public view virtual returns (bytes calldata) { + if (IERC2771Context(address(this)).isTrustedForwarder(msg.sender)) { + return msg.data[:msg.data.length - 20]; + } else { + return msg.data; + } + } +} diff --git a/contracts/extension/plugin/ERC2771ContextLogic.sol b/contracts/extension/plugin/ERC2771ContextLogic.sol new file mode 100644 index 000000000..c87d1bd6a --- /dev/null +++ b/contracts/extension/plugin/ERC2771ContextLogic.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./ERC2771ContextStorage.sol"; + +/** + * @dev Context variant with ERC2771 support. + */ +abstract contract ERC2771ContextLogic { + constructor(address[] memory trustedForwarder) { + ERC2771ContextStorage.Data storage data = ERC2771ContextStorage.erc2771ContextStorage(); + + for (uint256 i = 0; i < trustedForwarder.length; i++) { + data._trustedForwarder[trustedForwarder[i]] = true; + } + } + + function isTrustedForwarder(address forwarder) public view virtual returns (bool) { + ERC2771ContextStorage.Data storage data = ERC2771ContextStorage.erc2771ContextStorage(); + return data._trustedForwarder[forwarder]; + } + + function _msgSender() internal view virtual returns (address sender) { + if (isTrustedForwarder(msg.sender)) { + // The assembly code is more direct than the Solidity version using `abi.decode`. + assembly { + sender := shr(96, calldataload(sub(calldatasize(), 20))) + } + } else { + return msg.sender; + } + } + + function _msgData() internal view virtual returns (bytes calldata) { + if (isTrustedForwarder(msg.sender)) { + return msg.data[:msg.data.length - 20]; + } else { + return msg.data; + } + } +} diff --git a/contracts/extension/plugin/ERC2771ContextStorage.sol b/contracts/extension/plugin/ERC2771ContextStorage.sol new file mode 100644 index 000000000..884d8e728 --- /dev/null +++ b/contracts/extension/plugin/ERC2771ContextStorage.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +library ERC2771ContextStorage { + /// @custom:storage-location erc7201:erc2771.context.storage + /// @dev keccak256(abi.encode(uint256(keccak256("erc2771.context.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant ERC2771_CONTEXT_STORAGE_POSITION = + 0x82aadcdf5bea62fd30615b6c0754b644e71b6c1e8c55b71bb927ad005b504f00; + + struct Data { + mapping(address => bool) _trustedForwarder; + } + + function erc2771ContextStorage() internal pure returns (Data storage erc2771ContextData) { + bytes32 position = ERC2771_CONTEXT_STORAGE_POSITION; + assembly { + erc2771ContextData.slot := position + } + } +} diff --git a/contracts/extension/plugin/ERC2771ContextUpgradeableLogic.sol b/contracts/extension/plugin/ERC2771ContextUpgradeableLogic.sol new file mode 100644 index 000000000..f07deb38d --- /dev/null +++ b/contracts/extension/plugin/ERC2771ContextUpgradeableLogic.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./ERC2771ContextStorage.sol"; + +/** + * @dev Context variant with ERC2771 support. + */ +abstract contract ERC2771ContextUpgradeableLogic { + function __ERC2771Context_init(address[] memory trustedForwarder) internal { + __ERC2771Context_init_unchained(trustedForwarder); + } + + function __ERC2771Context_init_unchained(address[] memory trustedForwarder) internal { + ERC2771ContextStorage.Data storage data = ERC2771ContextStorage.erc2771ContextStorage(); + + for (uint256 i = 0; i < trustedForwarder.length; i++) { + data._trustedForwarder[trustedForwarder[i]] = true; + } + } + + function isTrustedForwarder(address forwarder) public view virtual returns (bool) { + ERC2771ContextStorage.Data storage data = ERC2771ContextStorage.erc2771ContextStorage(); + return data._trustedForwarder[forwarder]; + } + + function _msgSender() internal view virtual returns (address sender) { + if (isTrustedForwarder(msg.sender)) { + // The assembly code is more direct than the Solidity version using `abi.decode`. + assembly { + sender := shr(96, calldataload(sub(calldatasize(), 20))) + } + } else { + return msg.sender; + } + } + + function _msgData() internal view virtual returns (bytes calldata) { + if (isTrustedForwarder(msg.sender)) { + return msg.data[:msg.data.length - 20]; + } else { + return msg.data; + } + } +} diff --git a/contracts/extension/plugin/ERC2771ContextUpgradeableStorage.sol b/contracts/extension/plugin/ERC2771ContextUpgradeableStorage.sol new file mode 100644 index 000000000..f77bc4df8 --- /dev/null +++ b/contracts/extension/plugin/ERC2771ContextUpgradeableStorage.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +library ERC2771ContextUpgradeableStorage { + bytes32 public constant ERC2771_CONTEXT_UPGRADEABLE_STORAGE_POSITION = + keccak256("erc2771.context.upgradeable.storage"); + + struct Data { + mapping(address => bool) _trustedForwarder; + } + + function erc2771ContextUpgradeableStorage() internal pure returns (Data storage erc2771ContextData) { + bytes32 position = ERC2771_CONTEXT_UPGRADEABLE_STORAGE_POSITION; + assembly { + erc2771ContextData.slot := position + } + } +} diff --git a/contracts/extension/plugin/PermissionsEnumerableLogic.sol b/contracts/extension/plugin/PermissionsEnumerableLogic.sol new file mode 100644 index 000000000..c307d5c8a --- /dev/null +++ b/contracts/extension/plugin/PermissionsEnumerableLogic.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./PermissionsEnumerableStorage.sol"; +import "./PermissionsLogic.sol"; + +/** + * @author thirdweb.com + * + * @title PermissionsEnumerable + * @dev This contracts provides extending-contracts with role-based access control mechanisms. + * Also provides interfaces to view all members with a given role, and total count of members. + */ +contract PermissionsEnumerableLogic is IPermissionsEnumerable, PermissionsLogic { + /** + * @notice Returns the role-member from a list of members for a role, + * at a given index. + * @dev Returns `member` who has `role`, at `index` of role-members list. + * See struct {RoleMembers}, and mapping {roleMembers} + * + * @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + * @param index Index in list of current members for the role. + * + * @return member Address of account that has `role` + */ + function getRoleMember(bytes32 role, uint256 index) external view override returns (address member) { + PermissionsEnumerableStorage.Data storage data = PermissionsEnumerableStorage.permissionsEnumerableStorage(); + uint256 currentIndex = data.roleMembers[role].index; + uint256 check; + + for (uint256 i = 0; i < currentIndex; i += 1) { + if (data.roleMembers[role].members[i] != address(0)) { + if (check == index) { + member = data.roleMembers[role].members[i]; + return member; + } + check += 1; + } else if (hasRole(role, address(0)) && i == data.roleMembers[role].indexOf[address(0)]) { + check += 1; + } + } + } + + /** + * @notice Returns total number of accounts that have a role. + * @dev Returns `count` of accounts that have `role`. + * See struct {RoleMembers}, and mapping {roleMembers} + * + * @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + * + * @return count Total number of accounts that have `role` + */ + function getRoleMemberCount(bytes32 role) external view override returns (uint256 count) { + PermissionsEnumerableStorage.Data storage data = PermissionsEnumerableStorage.permissionsEnumerableStorage(); + uint256 currentIndex = data.roleMembers[role].index; + + for (uint256 i = 0; i < currentIndex; i += 1) { + if (data.roleMembers[role].members[i] != address(0)) { + count += 1; + } + } + if (hasRole(role, address(0))) { + count += 1; + } + } + + /// @dev Revokes `role` from `account`, and removes `account` from {roleMembers} + /// See {_removeMember} + function _revokeRole(bytes32 role, address account) internal override { + super._revokeRole(role, account); + _removeMember(role, account); + } + + /// @dev Grants `role` to `account`, and adds `account` to {roleMembers} + /// See {_addMember} + function _setupRole(bytes32 role, address account) internal override { + super._setupRole(role, account); + _addMember(role, account); + } + + /// @dev adds `account` to {roleMembers}, for `role` + function _addMember(bytes32 role, address account) internal { + PermissionsEnumerableStorage.Data storage data = PermissionsEnumerableStorage.permissionsEnumerableStorage(); + uint256 idx = data.roleMembers[role].index; + data.roleMembers[role].index += 1; + + data.roleMembers[role].members[idx] = account; + data.roleMembers[role].indexOf[account] = idx; + } + + /// @dev removes `account` from {roleMembers}, for `role` + function _removeMember(bytes32 role, address account) internal { + PermissionsEnumerableStorage.Data storage data = PermissionsEnumerableStorage.permissionsEnumerableStorage(); + uint256 idx = data.roleMembers[role].indexOf[account]; + + delete data.roleMembers[role].members[idx]; + delete data.roleMembers[role].indexOf[account]; + } +} diff --git a/contracts/extension/plugin/PermissionsEnumerableStorage.sol b/contracts/extension/plugin/PermissionsEnumerableStorage.sol new file mode 100644 index 000000000..e8c889e3d --- /dev/null +++ b/contracts/extension/plugin/PermissionsEnumerableStorage.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../interface/IPermissionsEnumerable.sol"; + +/** + * @author thirdweb.com + */ +library PermissionsEnumerableStorage { + /// @custom:storage-location erc7201:permissions.enumerable.storage + /// @dev keccak256(abi.encode(uint256(keccak256("permissions.enumerable.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant PERMISSIONS_ENUMERABLE_STORAGE_POSITION = + 0x1ea2ed6cf13bfad376ba49bede85b663fef0b40eac197c5ac8e6f92ec4076100; + + /** + * @notice A data structure to store data of members for a given role. + * + * @param index Current index in the list of accounts that have a role. + * @param members map from index => address of account that has a role + * @param indexOf map from address => index which the account has. + */ + struct RoleMembers { + uint256 index; + mapping(uint256 => address) members; + mapping(address => uint256) indexOf; + } + + struct Data { + /// @dev map from keccak256 hash of a role to its members' data. See {RoleMembers}. + mapping(bytes32 => RoleMembers) roleMembers; + } + + function permissionsEnumerableStorage() internal pure returns (Data storage permissionsEnumerableData) { + bytes32 position = PERMISSIONS_ENUMERABLE_STORAGE_POSITION; + assembly { + permissionsEnumerableData.slot := position + } + } +} diff --git a/contracts/extension/plugin/PermissionsLogic.sol b/contracts/extension/plugin/PermissionsLogic.sol new file mode 100644 index 000000000..9206870bd --- /dev/null +++ b/contracts/extension/plugin/PermissionsLogic.sol @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../interface/IPermissions.sol"; +import "./PermissionsStorage.sol"; +import "../../lib/Strings.sol"; + +/** + * @author thirdweb.com + * + * @title Permissions + * @dev This contracts provides extending-contracts with role-based access control mechanisms + */ +contract PermissionsLogic is IPermissions { + /// @dev Default admin role for all roles. Only accounts with this role can grant/revoke other roles. + bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; + + /// @dev Modifier that checks if an account has the specified role; reverts otherwise. + modifier onlyRole(bytes32 role) { + _checkRole(role, _msgSender()); + _; + } + + /** + * @notice Checks whether an account has a particular role. + * @dev Returns `true` if `account` has been granted `role`. + * + * @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + * @param account Address of the account for which the role is being checked. + */ + function hasRole(bytes32 role, address account) public view override returns (bool) { + PermissionsStorage.Data storage data = PermissionsStorage.permissionsStorage(); + return data._hasRole[role][account]; + } + + /** + * @notice Checks whether an account has a particular role; + * role restrictions can be swtiched on and off. + * + * @dev Returns `true` if `account` has been granted `role`. + * Role restrictions can be swtiched on and off: + * - If address(0) has ROLE, then the ROLE restrictions + * don't apply. + * - If address(0) does not have ROLE, then the ROLE + * restrictions will apply. + * + * @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + * @param account Address of the account for which the role is being checked. + */ + function hasRoleWithSwitch(bytes32 role, address account) public view returns (bool) { + PermissionsStorage.Data storage data = PermissionsStorage.permissionsStorage(); + if (!data._hasRole[role][address(0)]) { + return data._hasRole[role][account]; + } + + return true; + } + + /** + * @notice Returns the admin role that controls the specified role. + * @dev See {grantRole} and {revokeRole}. + * To change a role's admin, use {_setRoleAdmin}. + * + * @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + */ + function getRoleAdmin(bytes32 role) external view override returns (bytes32) { + PermissionsStorage.Data storage data = PermissionsStorage.permissionsStorage(); + return data._getRoleAdmin[role]; + } + + /** + * @notice Grants a role to an account, if not previously granted. + * @dev Caller must have admin role for the `role`. + * Emits {RoleGranted Event}. + * + * @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + * @param account Address of the account to which the role is being granted. + */ + function grantRole(bytes32 role, address account) public virtual override { + PermissionsStorage.Data storage data = PermissionsStorage.permissionsStorage(); + _checkRole(data._getRoleAdmin[role], _msgSender()); + if (data._hasRole[role][account]) { + revert("Can only grant to non holders"); + } + _setupRole(role, account); + } + + /** + * @notice Revokes role from an account. + * @dev Caller must have admin role for the `role`. + * Emits {RoleRevoked Event}. + * + * @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + * @param account Address of the account from which the role is being revoked. + */ + function revokeRole(bytes32 role, address account) public virtual override { + PermissionsStorage.Data storage data = PermissionsStorage.permissionsStorage(); + _checkRole(data._getRoleAdmin[role], _msgSender()); + _revokeRole(role, account); + } + + /** + * @notice Revokes role from the account. + * @dev Caller must have the `role`, with caller being the same as `account`. + * Emits {RoleRevoked Event}. + * + * @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + * @param account Address of the account from which the role is being revoked. + */ + function renounceRole(bytes32 role, address account) public virtual override { + if (_msgSender() != account) { + revert("Can only renounce for self"); + } + _revokeRole(role, account); + } + + /// @dev Sets `adminRole` as `role`'s admin role. + function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual { + PermissionsStorage.Data storage data = PermissionsStorage.permissionsStorage(); + bytes32 previousAdminRole = data._getRoleAdmin[role]; + data._getRoleAdmin[role] = adminRole; + emit RoleAdminChanged(role, previousAdminRole, adminRole); + } + + /// @dev Sets up `role` for `account` + function _setupRole(bytes32 role, address account) internal virtual { + PermissionsStorage.Data storage data = PermissionsStorage.permissionsStorage(); + data._hasRole[role][account] = true; + emit RoleGranted(role, account, _msgSender()); + } + + /// @dev Revokes `role` from `account` + function _revokeRole(bytes32 role, address account) internal virtual { + PermissionsStorage.Data storage data = PermissionsStorage.permissionsStorage(); + _checkRole(role, account); + delete data._hasRole[role][account]; + emit RoleRevoked(role, account, _msgSender()); + } + + /// @dev Checks `role` for `account`. Reverts with a message including the required role. + function _checkRole(bytes32 role, address account) internal view virtual { + PermissionsStorage.Data storage data = PermissionsStorage.permissionsStorage(); + if (!data._hasRole[role][account]) { + revert( + string( + abi.encodePacked( + "Permissions: account ", + Strings.toHexString(uint160(account), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ) + ); + } + } + + /// @dev Checks `role` for `account`. Reverts with a message including the required role. + function _checkRoleWithSwitch(bytes32 role, address account) internal view virtual { + if (!hasRoleWithSwitch(role, account)) { + revert( + string( + abi.encodePacked( + "Permissions: account ", + Strings.toHexString(uint160(account), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ) + ); + } + } + + function _msgSender() internal view virtual returns (address sender) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } +} diff --git a/contracts/extension/plugin/PermissionsStorage.sol b/contracts/extension/plugin/PermissionsStorage.sol new file mode 100644 index 000000000..bedc341c0 --- /dev/null +++ b/contracts/extension/plugin/PermissionsStorage.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +/** + * @author thirdweb.com + */ +library PermissionsStorage { + /// @custom:storage-location erc7201:permissions.storage + /// @dev keccak256(abi.encode(uint256(keccak256("permissions.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant PERMISSIONS_STORAGE_POSITION = + 0x0a7b0f5c59907924802379ebe98cdc23e2ee7820f63d30126e10b3752010e500; + + struct Data { + /// @dev Map from keccak256 hash of a role => a map from address => whether address has role. + mapping(bytes32 => mapping(address => bool)) _hasRole; + /// @dev Map from keccak256 hash of a role to role admin. See {getRoleAdmin}. + mapping(bytes32 => bytes32) _getRoleAdmin; + } + + function permissionsStorage() internal pure returns (Data storage permissionsData) { + bytes32 position = PERMISSIONS_STORAGE_POSITION; + assembly { + permissionsData.slot := position + } + } +} diff --git a/contracts/extension/plugin/PlatformFeeLogic.sol b/contracts/extension/plugin/PlatformFeeLogic.sol new file mode 100644 index 000000000..99dfe9aef --- /dev/null +++ b/contracts/extension/plugin/PlatformFeeLogic.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./PlatformFeeStorage.sol"; +import "../interface/IPlatformFee.sol"; + +/** + * @author thirdweb.com + * + * @title Platform Fee + * @notice Thirdweb's `PlatformFee` is a contract extension to be used with any base contract. It exposes functions for setting and reading + * the recipient of platform fee and the platform fee basis points, and lets the inheriting contract perform conditional logic + * that uses information about platform fees, if desired. + */ + +abstract contract PlatformFeeLogic is IPlatformFee { + /// @dev Returns the platform fee recipient and bps. + function getPlatformFeeInfo() public view override returns (address, uint16) { + PlatformFeeStorage.Data storage data = PlatformFeeStorage.platformFeeStorage(); + return (data.platformFeeRecipient, uint16(data.platformFeeBps)); + } + + /** + * @notice Updates the platform fee recipient and bps. + * @dev Caller should be authorized to set platform fee info. + * See {_canSetPlatformFeeInfo}. + * Emits {PlatformFeeInfoUpdated Event}; See {_setupPlatformFeeInfo}. + * + * @param _platformFeeRecipient Address to be set as new platformFeeRecipient. + * @param _platformFeeBps Updated platformFeeBps. + */ + function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) external override { + if (!_canSetPlatformFeeInfo()) { + revert("Not authorized"); + } + _setupPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } + + /// @dev Lets a contract admin update the platform fee recipient and bps + function _setupPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) internal { + PlatformFeeStorage.Data storage data = PlatformFeeStorage.platformFeeStorage(); + if (_platformFeeBps > 10_000) { + revert("Exceeds max bps"); + } + + data.platformFeeBps = uint16(_platformFeeBps); + data.platformFeeRecipient = _platformFeeRecipient; + + emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); + } + + /// @dev Returns whether platform fee info can be set in the given execution context. + function _canSetPlatformFeeInfo() internal view virtual returns (bool); +} diff --git a/contracts/extension/plugin/PlatformFeeStorage.sol b/contracts/extension/plugin/PlatformFeeStorage.sol new file mode 100644 index 000000000..93395a481 --- /dev/null +++ b/contracts/extension/plugin/PlatformFeeStorage.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +/** + * @author thirdweb.com + */ +library PlatformFeeStorage { + /// @custom:storage-location erc7201:platform.fee.storage + /// @dev keccak256(abi.encode(uint256(keccak256("platform.fee.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant PLATFORM_FEE_STORAGE_POSITION = + 0xc0c34308b4a2f4c5ee9af8ba82541cfb3c33b076d1fd05c65f9ce7060c64c400; + + struct Data { + /// @dev The address that receives all platform fees from all sales. + address platformFeeRecipient; + /// @dev The % of primary sales collected as platform fees. + uint16 platformFeeBps; + } + + function platformFeeStorage() internal pure returns (Data storage platformFeeData) { + bytes32 position = PLATFORM_FEE_STORAGE_POSITION; + assembly { + platformFeeData.slot := position + } + } +} diff --git a/contracts/extension/plugin/PluginMap.sol b/contracts/extension/plugin/PluginMap.sol new file mode 100644 index 000000000..d4b89be3b --- /dev/null +++ b/contracts/extension/plugin/PluginMap.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../interface/plugin/IPluginMap.sol"; +import "../../external-deps/openzeppelin/utils/EnumerableSet.sol"; + +/** + * @author thirdweb.com + */ +contract PluginMap is IPluginMap { + using EnumerableSet for EnumerableSet.Bytes32Set; + + EnumerableSet.Bytes32Set private allSelectors; + + mapping(address => EnumerableSet.Bytes32Set) private selectorsForPlugin; + mapping(bytes4 => Plugin) private pluginForSelector; + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + constructor(Plugin[] memory _pluginsToAdd) { + uint256 len = _pluginsToAdd.length; + for (uint256 i = 0; i < len; i += 1) { + _setPlugin(_pluginsToAdd[i]); + } + } + + /*/////////////////////////////////////////////////////////////// + View functions + //////////////////////////////////////////////////////////////*/ + + /// @dev View address of the plugged-in functionality contract for a given function signature. + function getPluginForFunction(bytes4 _selector) public view returns (address) { + address _pluginAddress = pluginForSelector[_selector].pluginAddress; + require(_pluginAddress != address(0), "Map: No plugin available for selector"); + + return _pluginAddress; + } + + /// @dev View all funtionality as list of function signatures. + function getAllFunctionsOfPlugin(address _pluginAddress) external view returns (bytes4[] memory registered) { + uint256 len = selectorsForPlugin[_pluginAddress].length(); + registered = new bytes4[](len); + + for (uint256 i = 0; i < len; i += 1) { + registered[i] = bytes4(selectorsForPlugin[_pluginAddress].at(i)); + } + } + + /// @dev View all funtionality existing on the contract. + function getAllPlugins() external view returns (Plugin[] memory _plugins) { + uint256 len = allSelectors.length(); + _plugins = new Plugin[](len); + + for (uint256 i = 0; i < len; i += 1) { + bytes4 selector = bytes4(allSelectors.at(i)); + _plugins[i] = pluginForSelector[selector]; + } + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Add functionality to the contract. + function _setPlugin(Plugin memory _plugin) internal { + require(allSelectors.add(bytes32(_plugin.functionSelector)), "Map: Selector exists"); + require( + _plugin.functionSelector == bytes4(keccak256(abi.encodePacked(_plugin.functionSignature))), + "Map: Incorrect selector" + ); + + pluginForSelector[_plugin.functionSelector] = _plugin; + selectorsForPlugin[_plugin.pluginAddress].add(bytes32(_plugin.functionSelector)); + + emit PluginSet(_plugin.functionSelector, _plugin.functionSignature, _plugin.pluginAddress); + } +} diff --git a/contracts/extension/plugin/ReentrancyGuardLogic.sol b/contracts/extension/plugin/ReentrancyGuardLogic.sol new file mode 100644 index 000000000..a0936c67a --- /dev/null +++ b/contracts/extension/plugin/ReentrancyGuardLogic.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./ReentrancyGuardStorage.sol"; + +/** + * @dev Contract module that helps prevent reentrant calls to a function. + * + * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier + * available, which can be applied to functions to make sure there are no nested + * (reentrant) calls to them. + * + * Note that because there is a single `nonReentrant` guard, functions marked as + * `nonReentrant` may not call one another. This can be worked around by making + * those functions `private`, and then adding `external` `nonReentrant` entry + * points to them. + * + * TIP: If you would like to learn more about reentrancy and alternative ways + * to protect against it, check out our blog post + * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. + */ +abstract contract ReentrancyGuardLogic { + // Booleans are more expensive than uint256 or any type that takes up a full + // word because each write operation emits an extra SLOAD to first read the + // slot's contents, replace the bits taken up by the boolean, and then write + // back. This is the compiler's defense against contract upgrades and + // pointer aliasing, and it cannot be disabled. + + // The values being non-zero value makes deployment a bit more expensive, + // but in exchange the refund on every call to nonReentrant will be lower in + // amount. Since refunds are capped to a percentage of the total + // transaction's gas, it is best to keep them low in cases like this one, to + // increase the likelihood of the full refund coming into effect. + uint256 private constant _NOT_ENTERED = 1; + uint256 private constant _ENTERED = 2; + + function __ReentrancyGuard_init() internal { + __ReentrancyGuard_init_unchained(); + } + + function __ReentrancyGuard_init_unchained() internal { + ReentrancyGuardStorage.Data storage data = ReentrancyGuardStorage.reentrancyGuardStorage(); + data._status = _NOT_ENTERED; + } + + /** + * @dev Prevents a contract from calling itself, directly or indirectly. + * Calling a `nonReentrant` function from another `nonReentrant` + * function is not supported. It is possible to prevent this from happening + * by making the `nonReentrant` function external, and making it call a + * `private` function that does the actual work. + */ + modifier nonReentrant() { + ReentrancyGuardStorage.Data storage data = ReentrancyGuardStorage.reentrancyGuardStorage(); + // On the first call to nonReentrant, _notEntered will be true + require(data._status != _ENTERED, "ReentrancyGuard: reentrant call"); + + // Any calls to nonReentrant after this point will fail + data._status = _ENTERED; + + _; + + // By storing the original value once again, a refund is triggered (see + // https://eips.ethereum.org/EIPS/eip-2200) + data._status = _NOT_ENTERED; + } +} diff --git a/contracts/extension/plugin/ReentrancyGuardStorage.sol b/contracts/extension/plugin/ReentrancyGuardStorage.sol new file mode 100644 index 000000000..866747c05 --- /dev/null +++ b/contracts/extension/plugin/ReentrancyGuardStorage.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +library ReentrancyGuardStorage { + /// @custom:storage-location erc7201:reentrancy.guard.storage + /// @dev keccak256(abi.encode(uint256(keccak256("reentrancy.guard.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant REENTRANCY_GUARD_STORAGE_POSITION = + 0x1d281c488dae143b6ea4122e80c65059929950b9c32f17fc57be22089d9c3b00; + + struct Data { + uint256 _status; + } + + function reentrancyGuardStorage() internal pure returns (Data storage reentrancyGuardData) { + bytes32 position = REENTRANCY_GUARD_STORAGE_POSITION; + assembly { + reentrancyGuardData.slot := position + } + } +} diff --git a/contracts/extension/plugin/Router.sol b/contracts/extension/plugin/Router.sol new file mode 100644 index 000000000..301db0803 --- /dev/null +++ b/contracts/extension/plugin/Router.sol @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../interface/plugin/IRouter.sol"; +import "../Multicall.sol"; +import "../../eip/ERC165.sol"; +import "../../external-deps/openzeppelin/utils/EnumerableSet.sol"; + +/** + * @author thirdweb.com + */ +library RouterStorage { + /// @custom:storage-location erc7201:router.storage + /// @dev keccak256(abi.encode(uint256(keccak256("router.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant ROUTER_STORAGE_POSITION = + 0x012ef321094c8c682aa635dfdfcd754624a7473f08ad6ac415bb7f35eb12a100; + + struct Data { + EnumerableSet.Bytes32Set allSelectors; + mapping(address => EnumerableSet.Bytes32Set) selectorsForPlugin; + mapping(bytes4 => IPluginMap.Plugin) pluginForSelector; + } + + function routerStorage() internal pure returns (Data storage routerData) { + bytes32 position = ROUTER_STORAGE_POSITION; + assembly { + routerData.slot := position + } + } +} + +abstract contract Router is Multicall, ERC165, IRouter { + using EnumerableSet for EnumerableSet.Bytes32Set; + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + address public immutable pluginMap; + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + constructor(address _pluginMap) { + pluginMap = _pluginMap; + } + + /*/////////////////////////////////////////////////////////////// + ERC 165 + //////////////////////////////////////////////////////////////*/ + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == type(IRouter).interfaceId || super.supportsInterface(interfaceId); + } + + /*/////////////////////////////////////////////////////////////// + Generic contract logic + //////////////////////////////////////////////////////////////*/ + + fallback() external payable virtual { + address _pluginAddress = _getPluginForFunction(msg.sig); + if (_pluginAddress == address(0)) { + _pluginAddress = IPluginMap(pluginMap).getPluginForFunction(msg.sig); + } + _delegate(_pluginAddress); + } + + receive() external payable {} + + function _delegate(address implementation) internal virtual { + assembly { + // Copy msg.data. We take full control of memory in this inline assembly + // block because it will not return to Solidity code. We overwrite the + // Solidity scratch pad at memory position 0. + calldatacopy(0, 0, calldatasize()) + + // Call the implementation. + // out and outsize are 0 because we don't know the size yet. + let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) + + // Copy the returned data. + returndatacopy(0, 0, returndatasize()) + + switch result + // delegatecall returns 0 on error. + case 0 { + revert(0, returndatasize()) + } + default { + return(0, returndatasize()) + } + } + } + + /*/////////////////////////////////////////////////////////////// + External functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Add functionality to the contract. + function addPlugin(Plugin memory _plugin) external { + require(_canSetPlugin(), "Router: Not authorized"); + + _addPlugin(_plugin); + } + + /// @dev Update or override existing functionality. + function updatePlugin(Plugin memory _plugin) external { + require(_canSetPlugin(), "Map: Not authorized"); + + _updatePlugin(_plugin); + } + + /// @dev Remove existing functionality from the contract. + function removePlugin(bytes4 _selector) external { + require(_canSetPlugin(), "Map: Not authorized"); + + _removePlugin(_selector); + } + + /*/////////////////////////////////////////////////////////////// + View functions + //////////////////////////////////////////////////////////////*/ + + /// @dev View address of the plugged-in functionality contract for a given function signature. + function getPluginForFunction(bytes4 _selector) public view returns (address) { + address pluginAddress = _getPluginForFunction(_selector); + + return pluginAddress != address(0) ? pluginAddress : IPluginMap(pluginMap).getPluginForFunction(_selector); + } + + /// @dev View all funtionality as list of function signatures. + function getAllFunctionsOfPlugin(address _pluginAddress) external view returns (bytes4[] memory registered) { + RouterStorage.Data storage data = RouterStorage.routerStorage(); + + EnumerableSet.Bytes32Set storage selectorsForPlugin = data.selectorsForPlugin[_pluginAddress]; + bytes4[] memory defaultSelectors = IPluginMap(pluginMap).getAllFunctionsOfPlugin(_pluginAddress); + + uint256 len = defaultSelectors.length; + uint256 count = selectorsForPlugin.length() + defaultSelectors.length; + + for (uint256 i = 0; i < len; i += 1) { + if (selectorsForPlugin.contains(defaultSelectors[i])) { + count -= 1; + defaultSelectors[i] = bytes4(0); + } + } + + registered = new bytes4[](count); + uint256 index; + + for (uint256 i = 0; i < len; i += 1) { + if (defaultSelectors[i] != bytes4(0)) { + registered[index++] = defaultSelectors[i]; + } + } + + len = selectorsForPlugin.length(); + for (uint256 i = 0; i < len; i += 1) { + registered[index++] = bytes4(data.selectorsForPlugin[_pluginAddress].at(i)); + } + } + + /// @dev View all funtionality existing on the contract. + function getAllPlugins() external view returns (Plugin[] memory registered) { + RouterStorage.Data storage data = RouterStorage.routerStorage(); + + EnumerableSet.Bytes32Set storage overrideSelectors = data.allSelectors; + Plugin[] memory defaultPlugins = IPluginMap(pluginMap).getAllPlugins(); + + uint256 overrideSelectorsLen = overrideSelectors.length(); + uint256 defaultPluginsLen = defaultPlugins.length; + + uint256 totalCount = overrideSelectorsLen + defaultPluginsLen; + + for (uint256 i = 0; i < overrideSelectorsLen; i += 1) { + for (uint256 j = 0; j < defaultPluginsLen; j += 1) { + if (bytes4(overrideSelectors.at(i)) == defaultPlugins[j].functionSelector) { + totalCount -= 1; + defaultPlugins[j].functionSelector = bytes4(0); + } + } + } + + registered = new Plugin[](totalCount); + uint256 index; + + for (uint256 i = 0; i < defaultPluginsLen; i += 1) { + if (defaultPlugins[i].functionSelector != bytes4(0)) { + registered[index] = defaultPlugins[i]; + index += 1; + } + } + + for (uint256 i = 0; i < overrideSelectorsLen; i += 1) { + registered[index] = data.pluginForSelector[bytes4(overrideSelectors.at(i))]; + index += 1; + } + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev View address of the plugged-in functionality contract for a given function signature. + function _getPluginForFunction(bytes4 _selector) public view returns (address) { + RouterStorage.Data storage data = RouterStorage.routerStorage(); + address _pluginAddress = data.pluginForSelector[_selector].pluginAddress; + + return _pluginAddress; + } + + /// @dev Add functionality to the contract. + function _addPlugin(Plugin memory _plugin) internal { + RouterStorage.Data storage data = RouterStorage.routerStorage(); + + // Revert: default plugin exists for function; use updatePlugin instead. + try IPluginMap(pluginMap).getPluginForFunction(_plugin.functionSelector) returns (address) { + revert("Router: default plugin exists for function."); + } catch { + require(data.allSelectors.add(bytes32(_plugin.functionSelector)), "Router: plugin exists for function."); + } + + require( + _plugin.functionSelector == bytes4(keccak256(abi.encodePacked(_plugin.functionSignature))), + "Router: fn selector and signature mismatch." + ); + + data.pluginForSelector[_plugin.functionSelector] = _plugin; + data.selectorsForPlugin[_plugin.pluginAddress].add(bytes32(_plugin.functionSelector)); + + emit PluginAdded(_plugin.functionSelector, _plugin.pluginAddress); + } + + /// @dev Update or override existing functionality. + function _updatePlugin(Plugin memory _plugin) internal { + address currentPlugin = getPluginForFunction(_plugin.functionSelector); + require( + _plugin.functionSelector == bytes4(keccak256(abi.encodePacked(_plugin.functionSignature))), + "Router: fn selector and signature mismatch." + ); + + RouterStorage.Data storage data = RouterStorage.routerStorage(); + data.allSelectors.add(bytes32(_plugin.functionSelector)); + data.pluginForSelector[_plugin.functionSelector] = _plugin; + data.selectorsForPlugin[currentPlugin].remove(bytes32(_plugin.functionSelector)); + data.selectorsForPlugin[_plugin.pluginAddress].add(bytes32(_plugin.functionSelector)); + + emit PluginUpdated(_plugin.functionSelector, currentPlugin, _plugin.pluginAddress); + } + + /// @dev Remove existing functionality from the contract. + function _removePlugin(bytes4 _selector) internal { + RouterStorage.Data storage data = RouterStorage.routerStorage(); + address currentPlugin = _getPluginForFunction(_selector); + require(currentPlugin != address(0), "Router: No plugin available for selector"); + + delete data.pluginForSelector[_selector]; + data.allSelectors.remove(_selector); + data.selectorsForPlugin[currentPlugin].remove(bytes32(_selector)); + + emit PluginRemoved(_selector, currentPlugin); + } + + function _canSetPlugin() internal view virtual returns (bool); +} diff --git a/contracts/extension/plugin/RouterImmutable.sol b/contracts/extension/plugin/RouterImmutable.sol new file mode 100644 index 000000000..b2e326eeb --- /dev/null +++ b/contracts/extension/plugin/RouterImmutable.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./Router.sol"; + +/** + * @author thirdweb.com + */ +contract RouterImmutable is Router { + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + constructor(address _pluginMap) Router(_pluginMap) {} + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns whether plug-in can be set in the given execution context. + function _canSetPlugin() internal pure override returns (bool) { + return false; + } +} diff --git a/contracts/extension/plugin/RoyaltyPayments.sol b/contracts/extension/plugin/RoyaltyPayments.sol new file mode 100644 index 000000000..fc353376c --- /dev/null +++ b/contracts/extension/plugin/RoyaltyPayments.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../interface/IRoyaltyPayments.sol"; +import "../interface/IRoyaltyEngineV1.sol"; +import { IERC2981 } from "../../eip/interface/IERC2981.sol"; + +library RoyaltyPaymentsStorage { + /// @custom:storage-location erc7201:royalty.payments.storage + /// @dev keccak256(abi.encode(uint256(keccak256("royalty.payments.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant ROYALTY_PAYMENTS_STORAGE_POSITION = + 0xc802b338f3fb784853cf3c808df5ff08335200e394ea2c687d12571a91045000; + + struct Data { + /// @dev The address of RoyaltyEngineV1, replacing the one set during construction. + address royaltyEngineAddressOverride; + } + + function royaltyPaymentsStorage() internal pure returns (Data storage royaltyPaymentsData) { + bytes32 position = ROYALTY_PAYMENTS_STORAGE_POSITION; + assembly { + royaltyPaymentsData.slot := position + } + } +} + +/** + * @author thirdweb.com + * + * @title Royalty Payments + * @notice Thirdweb's `RoyaltyPayments` is a contract extension to be used with a marketplace contract. + * It exposes functions for fetching royalty settings for a token. + * It Supports RoyaltyEngineV1 and RoyaltyRegistry by manifold.xyz. + */ + +abstract contract RoyaltyPaymentsLogic is IRoyaltyPayments { + // solhint-disable-next-line var-name-mixedcase + address immutable ROYALTY_ENGINE_ADDRESS; + + constructor(address _royaltyEngineAddress) { + // allow address(0) in case RoyaltyEngineV1 not present on a network + require( + _royaltyEngineAddress == address(0) || + IERC165(_royaltyEngineAddress).supportsInterface(type(IRoyaltyEngineV1).interfaceId), + "Doesn't support IRoyaltyEngineV1 interface" + ); + + ROYALTY_ENGINE_ADDRESS = _royaltyEngineAddress; + } + + /** + * Get the royalty for a given token (address, id) and value amount. Does not cache the bps/amounts. Caches the spec for a given token address + * + * @param tokenAddress - The address of the token + * @param tokenId - The id of the token + * @param value - The value you wish to get the royalty of + * + * returns Two arrays of equal length, royalty recipients and the corresponding amount each recipient should get + */ + function getRoyalty( + address tokenAddress, + uint256 tokenId, + uint256 value + ) external returns (address payable[] memory recipients, uint256[] memory amounts) { + address royaltyEngineAddress = getRoyaltyEngineAddress(); + + if (royaltyEngineAddress == address(0)) { + try IERC2981(tokenAddress).royaltyInfo(tokenId, value) returns (address recipient, uint256 amount) { + require(amount < value, "Invalid royalty amount"); + + recipients = new address payable[](1); + amounts = new uint256[](1); + recipients[0] = payable(recipient); + amounts[0] = amount; + } catch {} + } else { + (recipients, amounts) = IRoyaltyEngineV1(royaltyEngineAddress).getRoyalty(tokenAddress, tokenId, value); + } + } + + /** + * Set or override RoyaltyEngine address + * + * @param _royaltyEngineAddress - RoyaltyEngineV1 address + */ + function setRoyaltyEngine(address _royaltyEngineAddress) external { + if (!_canSetRoyaltyEngine()) { + revert("Not authorized"); + } + + require( + _royaltyEngineAddress != address(0) && + IERC165(_royaltyEngineAddress).supportsInterface(type(IRoyaltyEngineV1).interfaceId), + "Doesn't support IRoyaltyEngineV1 interface" + ); + + _setupRoyaltyEngine(_royaltyEngineAddress); + } + + /// @dev Returns original or overridden address for RoyaltyEngineV1 + function getRoyaltyEngineAddress() public view returns (address royaltyEngineAddress) { + RoyaltyPaymentsStorage.Data storage data = RoyaltyPaymentsStorage.royaltyPaymentsStorage(); + address royaltyEngineOverride = data.royaltyEngineAddressOverride; + royaltyEngineAddress = royaltyEngineOverride != address(0) ? royaltyEngineOverride : ROYALTY_ENGINE_ADDRESS; + } + + /// @dev Lets a contract admin update the royalty engine address + function _setupRoyaltyEngine(address _royaltyEngineAddress) internal { + RoyaltyPaymentsStorage.Data storage data = RoyaltyPaymentsStorage.royaltyPaymentsStorage(); + address currentAddress = data.royaltyEngineAddressOverride; + + data.royaltyEngineAddressOverride = _royaltyEngineAddress; + + emit RoyaltyEngineUpdated(currentAddress, _royaltyEngineAddress); + } + + /// @dev Returns whether royalty engine address can be set in the given execution context. + function _canSetRoyaltyEngine() internal view virtual returns (bool); +} diff --git a/contracts/extension/upgradeable/AccountPermissions.sol b/contracts/extension/upgradeable/AccountPermissions.sol new file mode 100644 index 000000000..63332489c --- /dev/null +++ b/contracts/extension/upgradeable/AccountPermissions.sol @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../interface/IAccountPermissions.sol"; +import "../../external-deps/openzeppelin/utils/cryptography/EIP712.sol"; +import "../../external-deps/openzeppelin/utils/structs/EnumerableSet.sol"; + +library AccountPermissionsStorage { + /// @custom:storage-location erc7201:account.permissions.storage + /// @dev keccak256(abi.encode(uint256(keccak256("account.permissions.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant ACCOUNT_PERMISSIONS_STORAGE_POSITION = + 0x3181e78fc1b109bc611fd2406150bf06e33faa75f71cba12c3e1fd670f2def00; + + struct Data { + /// @dev The set of all admins of the wallet. + EnumerableSet.AddressSet allAdmins; + /// @dev The set of all signers with permission to use the account. + EnumerableSet.AddressSet allSigners; + /// @dev Map from address => whether the address is an admin. + mapping(address => bool) isAdmin; + /// @dev Map from signer address => active restrictions for that signer. + mapping(address => IAccountPermissions.SignerPermissionsStatic) signerPermissions; + /// @dev Map from signer address => approved target the signer can call using the account contract. + mapping(address => EnumerableSet.AddressSet) approvedTargets; + /// @dev Mapping from a signed request UID => whether the request is processed. + mapping(bytes32 => bool) executed; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = ACCOUNT_PERMISSIONS_STORAGE_POSITION; + assembly { + data_.slot := position + } + } +} + +abstract contract AccountPermissions is IAccountPermissions, EIP712 { + using ECDSA for bytes32; + using EnumerableSet for EnumerableSet.AddressSet; + + bytes32 private constant TYPEHASH = + keccak256( + "SignerPermissionRequest(address signer,uint8 isAdmin,address[] approvedTargets,uint256 nativeTokenLimitPerTransaction,uint128 permissionStartTimestamp,uint128 permissionEndTimestamp,uint128 reqValidityStartTimestamp,uint128 reqValidityEndTimestamp,bytes32 uid)" + ); + + function _onlyAdmin() internal virtual { + require(isAdmin(msg.sender), "!admin"); + } + + /*/////////////////////////////////////////////////////////////// + External functions + //////////////////////////////////////////////////////////////*/ + + /// @notice Sets the permissions for a given signer. + function setPermissionsForSigner(SignerPermissionRequest calldata _req, bytes calldata _signature) external { + address targetSigner = _req.signer; + + require( + _req.reqValidityStartTimestamp <= block.timestamp && block.timestamp < _req.reqValidityEndTimestamp, + "!period" + ); + + (bool success, address signer) = verifySignerPermissionRequest(_req, _signature); + require(success, "!sig"); + + _accountPermissionsStorage().executed[_req.uid] = true; + + //isAdmin > 0, set admin or remove admin + if (_req.isAdmin > 0) { + //isAdmin = 1, set admin + //isAdmin > 1, remove admin + bool _isAdmin = _req.isAdmin == 1; + + _setAdmin(targetSigner, _isAdmin); + return; + } + + require(!isAdmin(targetSigner), "admin"); + + _accountPermissionsStorage().allSigners.add(targetSigner); + + _accountPermissionsStorage().signerPermissions[targetSigner] = SignerPermissionsStatic( + _req.nativeTokenLimitPerTransaction, + _req.permissionStartTimestamp, + _req.permissionEndTimestamp + ); + + address[] memory currentTargets = _accountPermissionsStorage().approvedTargets[targetSigner].values(); + uint256 len = currentTargets.length; + + for (uint256 i = 0; i < len; i += 1) { + _accountPermissionsStorage().approvedTargets[targetSigner].remove(currentTargets[i]); + } + + len = _req.approvedTargets.length; + for (uint256 i = 0; i < len; i += 1) { + _accountPermissionsStorage().approvedTargets[targetSigner].add(_req.approvedTargets[i]); + } + + _afterSignerPermissionsUpdate(_req); + + emit SignerPermissionsUpdated(signer, targetSigner, _req); + } + + /*/////////////////////////////////////////////////////////////// + View functions + //////////////////////////////////////////////////////////////*/ + + /// @notice Returns whether the given account is an admin. + function isAdmin(address _account) public view virtual returns (bool) { + return _accountPermissionsStorage().isAdmin[_account]; + } + + /// @notice Returns whether the given account is an active signer on the account. + function isActiveSigner(address signer) public view returns (bool) { + SignerPermissionsStatic memory permissions = _accountPermissionsStorage().signerPermissions[signer]; + + return + permissions.startTimestamp <= block.timestamp && + block.timestamp < permissions.endTimestamp && + _accountPermissionsStorage().approvedTargets[signer].length() > 0; + } + + /// @notice Returns the restrictions under which a signer can use the smart wallet. + function getPermissionsForSigner(address signer) external view returns (SignerPermissions memory) { + SignerPermissionsStatic memory permissions = _accountPermissionsStorage().signerPermissions[signer]; + + return + SignerPermissions( + signer, + _accountPermissionsStorage().approvedTargets[signer].values(), + permissions.nativeTokenLimitPerTransaction, + permissions.startTimestamp, + permissions.endTimestamp + ); + } + + /// @dev Verifies that a request is signed by an authorized account. + function verifySignerPermissionRequest( + SignerPermissionRequest calldata req, + bytes calldata signature + ) public view virtual returns (bool success, address signer) { + signer = _recoverAddress(_encodeRequest(req), signature); + success = !_accountPermissionsStorage().executed[req.uid] && isAdmin(signer); + } + + /// @notice Returns all active and inactive signers of the account. + function getAllSigners() external view returns (SignerPermissions[] memory signers) { + address[] memory allSigners = _accountPermissionsStorage().allSigners.values(); + + uint256 len = allSigners.length; + signers = new SignerPermissions[](len); + for (uint256 i = 0; i < len; i += 1) { + address signer = allSigners[i]; + SignerPermissionsStatic memory permissions = _accountPermissionsStorage().signerPermissions[signer]; + + signers[i] = SignerPermissions( + signer, + _accountPermissionsStorage().approvedTargets[signer].values(), + permissions.nativeTokenLimitPerTransaction, + permissions.startTimestamp, + permissions.endTimestamp + ); + } + } + + /// @notice Returns all signers with active permissions to use the account. + function getAllActiveSigners() external view returns (SignerPermissions[] memory signers) { + address[] memory allSigners = _accountPermissionsStorage().allSigners.values(); + + uint256 len = allSigners.length; + uint256 numOfActiveSigners = 0; + + for (uint256 i = 0; i < len; i += 1) { + if (isActiveSigner(allSigners[i])) { + numOfActiveSigners++; + } else { + allSigners[i] = address(0); + } + } + + signers = new SignerPermissions[](numOfActiveSigners); + uint256 index = 0; + for (uint256 i = 0; i < len; i += 1) { + if (allSigners[i] != address(0)) { + address signer = allSigners[i]; + SignerPermissionsStatic memory permissions = _accountPermissionsStorage().signerPermissions[signer]; + + signers[index++] = SignerPermissions( + signer, + _accountPermissionsStorage().approvedTargets[signer].values(), + permissions.nativeTokenLimitPerTransaction, + permissions.startTimestamp, + permissions.endTimestamp + ); + } + } + } + + /// @notice Returns all admins of the account. + function getAllAdmins() external view returns (address[] memory) { + return _accountPermissionsStorage().allAdmins.values(); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @notice Runs after every `changeRole` run. + function _afterSignerPermissionsUpdate(SignerPermissionRequest calldata _req) internal virtual; + + /// @notice Makes the given account an admin. + function _setAdmin(address _account, bool _isAdmin) internal virtual { + _accountPermissionsStorage().isAdmin[_account] = _isAdmin; + + if (_isAdmin) { + _accountPermissionsStorage().allAdmins.add(_account); + } else { + _accountPermissionsStorage().allAdmins.remove(_account); + } + + emit AdminUpdated(_account, _isAdmin); + } + + /// @dev Returns the address of the signer of the request. + function _recoverAddress(bytes memory _encoded, bytes calldata _signature) internal view virtual returns (address) { + return _hashTypedDataV4(keccak256(_encoded)).recover(_signature); + } + + /// @dev Encodes a request for recovery of the signer in `recoverAddress`. + function _encodeRequest(SignerPermissionRequest calldata _req) internal pure virtual returns (bytes memory) { + return + abi.encode( + TYPEHASH, + _req.signer, + _req.isAdmin, + keccak256(abi.encodePacked(_req.approvedTargets)), + _req.nativeTokenLimitPerTransaction, + _req.permissionStartTimestamp, + _req.permissionEndTimestamp, + _req.reqValidityStartTimestamp, + _req.reqValidityEndTimestamp, + _req.uid + ); + } + + /// @dev Returns the AccountPermissions storage. + function _accountPermissionsStorage() internal pure returns (AccountPermissionsStorage.Data storage data) { + data = AccountPermissionsStorage.data(); + } +} diff --git a/contracts/extension/upgradeable/BatchMintMetadata.sol b/contracts/extension/upgradeable/BatchMintMetadata.sol new file mode 100644 index 000000000..8d4546033 --- /dev/null +++ b/contracts/extension/upgradeable/BatchMintMetadata.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +library BatchMintMetadataStorage { + /// @custom:storage-location erc7201:batch.mint.metadata.storage + /// @dev keccak256(abi.encode(uint256(keccak256("batch.mint.metadata.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant BATCH_MINT_METADATA_STORAGE_POSITION = + 0xf5b99f0648d517803cfbd359284c3fd81ac54e1c89b4874d917ae042d05e8500; + + struct Data { + /// @dev Largest tokenId of each batch of tokens with the same baseURI. + uint256[] batchIds; + /// @dev Mapping from id of a batch of tokens => to base URI for the respective batch of tokens. + mapping(uint256 => string) baseURI; + /// @dev Mapping from id of a batch of tokens => to whether the base URI for the respective batch of tokens is frozen. + mapping(uint256 => bool) batchFrozen; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = BATCH_MINT_METADATA_STORAGE_POSITION; + assembly { + data_.slot := position + } + } +} + +/** + * @title Batch-mint Metadata + * @notice The `BatchMintMetadata` is a contract extension for any base NFT contract. It lets the smart contract + * using this extension set metadata for `n` number of NFTs all at once. This is enabled by storing a single + * base URI for a batch of `n` NFTs, where the metadata for each NFT in a relevant batch is `baseURI/tokenId`. + */ + +contract BatchMintMetadata { + /// @dev This event emits when the metadata of all tokens are frozen. + /// While not currently supported by marketplaces, this event allows + /// future indexing if desired. + event MetadataFrozen(); + + // @dev This event emits when the metadata of a range of tokens is updated. + /// So that the third-party platforms such as NFT market could + /// timely update the images and related attributes of the NFTs. + event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); + + /** + * @notice Returns the count of batches of NFTs. + * @dev Each batch of tokens has an in ID and an associated `baseURI`. + * See {batchIds}. + */ + function getBaseURICount() public view returns (uint256) { + return _batchMintMetadataStorage().batchIds.length; + } + + /** + * @notice Returns the ID for the batch of tokens the given tokenId belongs to. + * @dev See {getBaseURICount}. + * @param _index ID of a token. + */ + function getBatchIdAtIndex(uint256 _index) public view returns (uint256) { + if (_index >= getBaseURICount()) { + revert("Invalid index"); + } + return _batchMintMetadataStorage().batchIds[_index]; + } + + /// @dev Returns the id for the batch of tokens the given tokenId belongs to. + function _getBatchId(uint256 _tokenId) internal view returns (uint256 batchId, uint256 index) { + uint256 numOfTokenBatches = getBaseURICount(); + uint256[] memory indices = _batchMintMetadataStorage().batchIds; + + for (uint256 i = 0; i < numOfTokenBatches; i += 1) { + if (_tokenId < indices[i]) { + index = i; + batchId = indices[i]; + + return (batchId, index); + } + } + + revert("Invalid tokenId"); + } + + /// @dev Returns the baseURI for a token. The intended metadata URI for the token is baseURI + tokenId. + function _getBaseURI(uint256 _tokenId) internal view returns (string memory) { + uint256 numOfTokenBatches = getBaseURICount(); + uint256[] memory indices = _batchMintMetadataStorage().batchIds; + + for (uint256 i = 0; i < numOfTokenBatches; i += 1) { + if (_tokenId < indices[i]) { + return _batchMintMetadataStorage().baseURI[indices[i]]; + } + } + revert("Invalid tokenId"); + } + + /// @dev returns the starting tokenId of a given batchId. + function _getBatchStartId(uint256 _batchID) internal view returns (uint256) { + uint256 numOfTokenBatches = getBaseURICount(); + uint256[] memory indices = _batchMintMetadataStorage().batchIds; + + for (uint256 i = 0; i < numOfTokenBatches; i++) { + if (_batchID == indices[i]) { + if (i > 0) { + return indices[i - 1]; + } + return 0; + } + } + revert("Invalid batchId"); + } + + /// @dev Sets the base URI for the batch of tokens with the given batchId. + function _setBaseURI(uint256 _batchId, string memory _baseURI) internal { + require(!_batchMintMetadataStorage().batchFrozen[_batchId], "Batch frozen"); + _batchMintMetadataStorage().baseURI[_batchId] = _baseURI; + emit BatchMetadataUpdate(_getBatchStartId(_batchId), _batchId); + } + + /// @dev Freezes the base URI for the batch of tokens with the given batchId. + function _freezeBaseURI(uint256 _batchId) internal { + string memory baseURIForBatch = _batchMintMetadataStorage().baseURI[_batchId]; + require(bytes(baseURIForBatch).length > 0, "Invalid batch"); + _batchMintMetadataStorage().batchFrozen[_batchId] = true; + emit MetadataFrozen(); + } + + /// @dev Mints a batch of tokenIds and associates a common baseURI to all those Ids. + function _batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) internal returns (uint256 nextTokenIdToMint, uint256 batchId) { + batchId = _startId + _amountToMint; + nextTokenIdToMint = batchId; + + _batchMintMetadataStorage().batchIds.push(batchId); + _batchMintMetadataStorage().baseURI[batchId] = _baseURIForTokens; + } + + /// @dev Returns the BatchMintMetadata storage. + function _batchMintMetadataStorage() internal pure returns (BatchMintMetadataStorage.Data storage data) { + data = BatchMintMetadataStorage.data(); + } +} diff --git a/contracts/extension/upgradeable/BurnToClaim.sol b/contracts/extension/upgradeable/BurnToClaim.sol new file mode 100644 index 000000000..8d3736062 --- /dev/null +++ b/contracts/extension/upgradeable/BurnToClaim.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import { ERC1155Burnable } from "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Burnable.sol"; +import { ERC721Burnable } from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol"; + +import "../../eip/interface/IERC1155.sol"; +import "../../eip/interface/IERC721.sol"; + +import "../interface/IBurnToClaim.sol"; + +library BurnToClaimStorage { + /// @custom:storage-location erc7201:burn.to.claim.storage + /// @dev keccak256(abi.encode(uint256(keccak256("burn.to.claim.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant BURN_TO_CLAIM_STORAGE_POSITION = + 0x6f0d20bed2d5528732497d5a17ac45087a6175b2a140eebe2a39ab447d7ad400; + + struct Data { + IBurnToClaim.BurnToClaimInfo burnToClaimInfo; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = BURN_TO_CLAIM_STORAGE_POSITION; + assembly { + data_.slot := position + } + } +} + +abstract contract BurnToClaim is IBurnToClaim { + /// @notice Returns the confugration for burning tokens to claim new tokens. + function getBurnToClaimInfo() public view returns (BurnToClaimInfo memory) { + return _burnToClaimStorage().burnToClaimInfo; + } + + /// @notice Sets the configuration for burning tokens to claim new tokens. + function setBurnToClaimInfo(BurnToClaimInfo calldata _burnToClaimInfo) external virtual { + require(_canSetBurnToClaim(), "Not authorized."); + require(_burnToClaimInfo.originContractAddress != address(0), "Origin contract not set."); + require(_burnToClaimInfo.currency != address(0), "Currency not set."); + + _burnToClaimStorage().burnToClaimInfo = _burnToClaimInfo; + } + + /// @notice Verifies an attempt to burn tokens to claim new tokens. + function verifyBurnToClaim(address _tokenOwner, uint256 _tokenId, uint256 _quantity) public view virtual { + BurnToClaimInfo memory _burnToClaimInfo = getBurnToClaimInfo(); + + if (_burnToClaimInfo.tokenType == IBurnToClaim.TokenType.ERC721) { + require(_quantity == 1, "Invalid amount"); + require(IERC721(_burnToClaimInfo.originContractAddress).ownerOf(_tokenId) == _tokenOwner, "!Owner"); + } else if (_burnToClaimInfo.tokenType == IBurnToClaim.TokenType.ERC1155) { + uint256 _eligible1155TokenId = _burnToClaimInfo.tokenId; + + require(_tokenId == _eligible1155TokenId, "Invalid token Id"); + require( + IERC1155(_burnToClaimInfo.originContractAddress).balanceOf(_tokenOwner, _tokenId) >= _quantity, + "!Balance" + ); + } + } + + /// @dev Burns tokens to claim new tokens. + function _burnTokensOnOrigin(address _tokenOwner, uint256 _tokenId, uint256 _quantity) internal virtual { + BurnToClaimInfo memory _burnToClaimInfo = getBurnToClaimInfo(); + + if (_burnToClaimInfo.tokenType == IBurnToClaim.TokenType.ERC721) { + ERC721Burnable(_burnToClaimInfo.originContractAddress).burn(_tokenId); + } else if (_burnToClaimInfo.tokenType == IBurnToClaim.TokenType.ERC1155) { + ERC1155Burnable(_burnToClaimInfo.originContractAddress).burn(_tokenOwner, _tokenId, _quantity); + } + } + + /// @dev Returns the BurnToClaimStorage storage. + function _burnToClaimStorage() internal pure returns (BurnToClaimStorage.Data storage data) { + data = BurnToClaimStorage.data(); + } + + /// @dev Returns whether the caller can set the burn to claim configuration. + function _canSetBurnToClaim() internal view virtual returns (bool); +} diff --git a/contracts/extension/upgradeable/ContractMetadata.sol b/contracts/extension/upgradeable/ContractMetadata.sol new file mode 100644 index 000000000..09ef5cb08 --- /dev/null +++ b/contracts/extension/upgradeable/ContractMetadata.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../interface/IContractMetadata.sol"; + +/** + * @author thirdweb.com + * + * @title Contract Metadata + * @notice Thirdweb's `ContractMetadata` is a contract extension for any base contracts. It lets you set a metadata URI + * for you contract. + * Additionally, `ContractMetadata` is necessary for NFT contracts that want royalties to get distributed on OpenSea. + */ + +library ContractMetadataStorage { + /// @custom:storage-location erc7201:contract.metadata.storage + /// @dev keccak256(abi.encode(uint256(keccak256("contract.metadata.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant CONTRACT_METADATA_STORAGE_POSITION = + 0x4bc804ba64359c0e35e5ed5d90ee596ecaa49a3a930ddcb1470ea0dd625da900; + + struct Data { + /// @notice Returns the contract metadata URI. + string contractURI; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = CONTRACT_METADATA_STORAGE_POSITION; + assembly { + data_.slot := position + } + } +} + +abstract contract ContractMetadata is IContractMetadata { + /** + * @notice Lets a contract admin set the URI for contract-level metadata. + * @dev Caller should be authorized to setup contractURI, e.g. contract admin. + * See {_canSetContractURI}. + * Emits {ContractURIUpdated Event}. + * + * @param _uri keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + */ + function setContractURI(string memory _uri) external override { + if (!_canSetContractURI()) { + revert("Not authorized"); + } + + _setupContractURI(_uri); + } + + /// @dev Lets a contract admin set the URI for contract-level metadata. + function _setupContractURI(string memory _uri) internal { + string memory prevURI = _contractMetadataStorage().contractURI; + _contractMetadataStorage().contractURI = _uri; + + emit ContractURIUpdated(prevURI, _uri); + } + + /// @notice Returns the contract metadata URI. + function contractURI() public view virtual override returns (string memory) { + return _contractMetadataStorage().contractURI; + } + + /// @dev Returns the AccountPermissions storage. + function _contractMetadataStorage() internal pure returns (ContractMetadataStorage.Data storage data) { + data = ContractMetadataStorage.data(); + } + + /// @dev Returns whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view virtual returns (bool); +} diff --git a/contracts/extension/upgradeable/DelayedReveal.sol b/contracts/extension/upgradeable/DelayedReveal.sol new file mode 100644 index 000000000..d36a4922c --- /dev/null +++ b/contracts/extension/upgradeable/DelayedReveal.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../interface/IDelayedReveal.sol"; + +library DelayedRevealStorage { + /// @custom:storage-location erc7201:delayed.reveal.storage + ///@dev keccak256(abi.encode(uint256(keccak256("delayed.reveal.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant DELAYED_REVEAL_STORAGE_POSITION = + 0x29cbb6a3768b42f407b01945994a37861bf5a2179c5dea5be7e378415e755100; + + struct Data { + /// @dev Mapping from tokenId of a batch of tokens => to delayed reveal data. + mapping(uint256 => bytes) encryptedData; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = DELAYED_REVEAL_STORAGE_POSITION; + assembly { + data_.slot := position + } + } +} + +/** + * @title Delayed Reveal + * @notice Thirdweb's `DelayedReveal` is a contract extension for base NFT contracts. It lets you create batches of + * 'delayed-reveal' NFTs. You can learn more about the usage of delayed reveal NFTs here - https://blog.thirdweb.com/delayed-reveal-nfts + */ + +abstract contract DelayedReveal is IDelayedReveal { + /// @dev Mapping from tokenId of a batch of tokens => to delayed reveal data. + function encryptedData(uint256 _tokenId) public view returns (bytes memory) { + return _delayedRevealStorage().encryptedData[_tokenId]; + } + + /// @dev Sets the delayed reveal data for a batchId. + function _setEncryptedData(uint256 _batchId, bytes memory _encryptedData) internal { + _delayedRevealStorage().encryptedData[_batchId] = _encryptedData; + } + + /** + * @notice Returns revealed URI for a batch of NFTs. + * @dev Reveal encrypted base URI for `_batchId` with caller/admin's `_key` used for encryption. + * Reverts if there's no encrypted URI for `_batchId`. + * See {encryptDecrypt}. + * + * @param _batchId ID of the batch for which URI is being revealed. + * @param _key Secure key used by caller/admin for encryption of baseURI. + * + * @return revealedURI Decrypted base URI. + */ + function getRevealURI(uint256 _batchId, bytes calldata _key) public view returns (string memory revealedURI) { + bytes memory dataForBatch = _delayedRevealStorage().encryptedData[_batchId]; + if (dataForBatch.length == 0) { + revert("Nothing to reveal"); + } + + (bytes memory encryptedURI, bytes32 provenanceHash) = abi.decode(dataForBatch, (bytes, bytes32)); + + revealedURI = string(encryptDecrypt(encryptedURI, _key)); + + require(keccak256(abi.encodePacked(revealedURI, _key, block.chainid)) == provenanceHash, "Incorrect key"); + } + + /** + * @notice Encrypt/decrypt data on chain. + * @dev Encrypt/decrypt given `data` with `key`. Uses inline assembly. + * See: https://ethereum.stackexchange.com/questions/69825/decrypt-message-on-chain + * + * @param data Bytes of data to encrypt/decrypt. + * @param key Secure key used by caller for encryption/decryption. + * + * @return result Output after encryption/decryption of given data. + */ + function encryptDecrypt(bytes memory data, bytes calldata key) public pure override returns (bytes memory result) { + // Store data length on stack for later use + uint256 length = data.length; + + // solhint-disable-next-line no-inline-assembly + assembly { + // Set result to free memory pointer + result := mload(0x40) + // Increase free memory pointer by lenght + 32 + mstore(0x40, add(add(result, length), 32)) + // Set result length + mstore(result, length) + } + + // Iterate over the data stepping by 32 bytes + for (uint256 i = 0; i < length; i += 32) { + // Generate hash of the key and offset + bytes32 hash = keccak256(abi.encodePacked(key, i)); + + bytes32 chunk; + // solhint-disable-next-line no-inline-assembly + assembly { + // Read 32-bytes data chunk + chunk := mload(add(data, add(i, 32))) + } + // XOR the chunk with hash + chunk ^= hash; + // solhint-disable-next-line no-inline-assembly + assembly { + // Write 32-byte encrypted chunk + mstore(add(result, add(i, 32)), chunk) + } + } + } + + /** + * @notice Returns whether the relvant batch of NFTs is subject to a delayed reveal. + * @dev Returns `true` if `_batchId`'s base URI is encrypted. + * @param _batchId ID of a batch of NFTs. + */ + function isEncryptedBatch(uint256 _batchId) public view returns (bool) { + return _delayedRevealStorage().encryptedData[_batchId].length > 0; + } + + /// @dev Returns the DelayedReveal storage. + function _delayedRevealStorage() internal pure returns (DelayedRevealStorage.Data storage data) { + data = DelayedRevealStorage.data(); + } +} diff --git a/contracts/extension/upgradeable/Drop.sol b/contracts/extension/upgradeable/Drop.sol new file mode 100644 index 000000000..4d701a204 --- /dev/null +++ b/contracts/extension/upgradeable/Drop.sol @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../interface/IDrop.sol"; +import "../../lib/MerkleProof.sol"; + +library DropStorage { + /// @custom:storage-location erc7201:extension.manager.storage + bytes32 public constant DROP_STORAGE_POSITION = + keccak256(abi.encode(uint256(keccak256("drop.storage")) - 1)) & ~bytes32(uint256(0xff)); + + struct Data { + /// @dev The active conditions for claiming tokens. + IDrop.ClaimConditionList claimCondition; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = DROP_STORAGE_POSITION; + assembly { + data_.slot := position + } + } +} + +abstract contract Drop is IDrop { + function claimCondition() public view returns (uint256, uint256) { + return (_dropStorage().claimCondition.currentStartId, _dropStorage().claimCondition.count); + } + + /*/////////////////////////////////////////////////////////////// + Drop logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Lets an account claim tokens. + function claim( + address _receiver, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof, + bytes memory _data + ) public payable virtual override { + _beforeClaim(_receiver, _quantity, _currency, _pricePerToken, _allowlistProof, _data); + + uint256 activeConditionId = getActiveClaimConditionId(); + + verifyClaim(activeConditionId, _dropMsgSender(), _quantity, _currency, _pricePerToken, _allowlistProof); + + // Update contract state. + _dropStorage().claimCondition.conditions[activeConditionId].supplyClaimed += _quantity; + _dropStorage().claimCondition.supplyClaimedByWallet[activeConditionId][_dropMsgSender()] += _quantity; + + // If there's a price, collect price. + _collectPriceOnClaim(address(0), _quantity, _currency, _pricePerToken); + + // Mint the relevant tokens to claimer. + uint256 startTokenId = _transferTokensOnClaim(_receiver, _quantity); + + emit TokensClaimed(activeConditionId, _dropMsgSender(), _receiver, startTokenId, _quantity); + + _afterClaim(_receiver, _quantity, _currency, _pricePerToken, _allowlistProof, _data); + } + + /// @dev Lets a contract admin set claim conditions. + function setClaimConditions( + ClaimCondition[] calldata _conditions, + bool _resetClaimEligibility + ) external virtual override { + if (!_canSetClaimConditions()) { + revert("Not authorized"); + } + + uint256 existingStartIndex = _dropStorage().claimCondition.currentStartId; + uint256 existingPhaseCount = _dropStorage().claimCondition.count; + + /** + * The mapping `supplyClaimedByWallet` uses a claim condition's UID as a key. + * + * If `_resetClaimEligibility == true`, we assign completely new UIDs to the claim + * conditions in `_conditions`, effectively resetting the restrictions on claims expressed + * by `supplyClaimedByWallet`. + */ + uint256 newStartIndex = existingStartIndex; + if (_resetClaimEligibility) { + newStartIndex = existingStartIndex + existingPhaseCount; + } + + _dropStorage().claimCondition.count = _conditions.length; + _dropStorage().claimCondition.currentStartId = newStartIndex; + + uint256 lastConditionStartTimestamp; + for (uint256 i = 0; i < _conditions.length; i++) { + require(i == 0 || lastConditionStartTimestamp < _conditions[i].startTimestamp, "ST"); + + uint256 supplyClaimedAlready = _dropStorage().claimCondition.conditions[newStartIndex + i].supplyClaimed; + if (supplyClaimedAlready > _conditions[i].maxClaimableSupply) { + revert("max supply claimed"); + } + + _dropStorage().claimCondition.conditions[newStartIndex + i] = _conditions[i]; + _dropStorage().claimCondition.conditions[newStartIndex + i].supplyClaimed = supplyClaimedAlready; + + lastConditionStartTimestamp = _conditions[i].startTimestamp; + } + + /** + * Gas refunds (as much as possible) + * + * If `_resetClaimEligibility == true`, we assign completely new UIDs to the claim + * conditions in `_conditions`. So, we delete claim conditions with UID < `newStartIndex`. + * + * If `_resetClaimEligibility == false`, and there are more existing claim conditions + * than in `_conditions`, we delete the existing claim conditions that don't get replaced + * by the conditions in `_conditions`. + */ + if (_resetClaimEligibility) { + for (uint256 i = existingStartIndex; i < newStartIndex; i++) { + delete _dropStorage().claimCondition.conditions[i]; + } + } else { + if (existingPhaseCount > _conditions.length) { + for (uint256 i = _conditions.length; i < existingPhaseCount; i++) { + delete _dropStorage().claimCondition.conditions[newStartIndex + i]; + } + } + } + + emit ClaimConditionsUpdated(_conditions, _resetClaimEligibility); + } + + /// @dev Checks a request to claim NFTs against the active claim condition's criteria. + function verifyClaim( + uint256 _conditionId, + address _claimer, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof + ) public view virtual returns (bool isOverride) { + ClaimCondition memory currentClaimPhase = _dropStorage().claimCondition.conditions[_conditionId]; + uint256 claimLimit = currentClaimPhase.quantityLimitPerWallet; + uint256 claimPrice = currentClaimPhase.pricePerToken; + address claimCurrency = currentClaimPhase.currency; + + /* + * Here `isOverride` implies that if the merkle proof verification fails, + * the claimer would claim through open claim limit instead of allowlisted limit. + */ + if (currentClaimPhase.merkleRoot != bytes32(0)) { + (isOverride, ) = MerkleProof.verify( + _allowlistProof.proof, + currentClaimPhase.merkleRoot, + keccak256( + abi.encodePacked( + _claimer, + _allowlistProof.quantityLimitPerWallet, + _allowlistProof.pricePerToken, + _allowlistProof.currency + ) + ) + ); + } + + if (isOverride) { + claimLimit = _allowlistProof.quantityLimitPerWallet != 0 + ? _allowlistProof.quantityLimitPerWallet + : claimLimit; + claimPrice = _allowlistProof.pricePerToken != type(uint256).max + ? _allowlistProof.pricePerToken + : claimPrice; + claimCurrency = _allowlistProof.pricePerToken != type(uint256).max && _allowlistProof.currency != address(0) + ? _allowlistProof.currency + : claimCurrency; + } + + uint256 supplyClaimedByWallet = _dropStorage().claimCondition.supplyClaimedByWallet[_conditionId][_claimer]; + + if (_currency != claimCurrency || _pricePerToken != claimPrice) { + revert("!PriceOrCurrency"); + } + + if (_quantity == 0 || (_quantity + supplyClaimedByWallet > claimLimit)) { + revert("!Qty"); + } + if (currentClaimPhase.supplyClaimed + _quantity > currentClaimPhase.maxClaimableSupply) { + revert("!MaxSupply"); + } + + if (currentClaimPhase.startTimestamp > block.timestamp) { + revert("cant claim yet"); + } + } + + /// @dev At any given moment, returns the uid for the active claim condition. + function getActiveClaimConditionId() public view returns (uint256) { + for ( + uint256 i = _dropStorage().claimCondition.currentStartId + _dropStorage().claimCondition.count; + i > _dropStorage().claimCondition.currentStartId; + i-- + ) { + if (block.timestamp >= _dropStorage().claimCondition.conditions[i - 1].startTimestamp) { + return i - 1; + } + } + + revert("!CONDITION."); + } + + /// @dev Returns the claim condition at the given uid. + function getClaimConditionById(uint256 _conditionId) external view returns (ClaimCondition memory condition) { + condition = _dropStorage().claimCondition.conditions[_conditionId]; + } + + /// @dev Returns the supply claimed by claimer for a given conditionId. + function getSupplyClaimedByWallet( + uint256 _conditionId, + address _claimer + ) public view returns (uint256 supplyClaimedByWallet) { + supplyClaimedByWallet = _dropStorage().claimCondition.supplyClaimedByWallet[_conditionId][_claimer]; + } + + /*//////////////////////////////////////////////////////////////////// + Optional hooks that can be implemented in the derived contract + ///////////////////////////////////////////////////////////////////*/ + + /// @dev Exposes the ability to override the msg sender. + function _dropMsgSender() internal virtual returns (address) { + return msg.sender; + } + + /// @dev Runs before every `claim` function call. + function _beforeClaim( + address _receiver, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof, + bytes memory _data + ) internal virtual {} + + /// @dev Runs after every `claim` function call. + function _afterClaim( + address _receiver, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof, + bytes memory _data + ) internal virtual {} + + /// @dev Returns the DropStorage storage. + function _dropStorage() internal pure returns (DropStorage.Data storage data) { + data = DropStorage.data(); + } + + /*/////////////////////////////////////////////////////////////// + Virtual functions: to be implemented in derived contract + //////////////////////////////////////////////////////////////*/ + + /// @dev Collects and distributes the primary sale value of NFTs being claimed. + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal virtual; + + /// @dev Transfers the NFTs being claimed. + function _transferTokensOnClaim( + address _to, + uint256 _quantityBeingClaimed + ) internal virtual returns (uint256 startTokenId); + + /// @dev Determine what wallet can update claim conditions + function _canSetClaimConditions() internal view virtual returns (bool); +} diff --git a/contracts/extension/upgradeable/ERC2771Context.sol b/contracts/extension/upgradeable/ERC2771Context.sol new file mode 100644 index 000000000..1898a876a --- /dev/null +++ b/contracts/extension/upgradeable/ERC2771Context.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.0; + +import "../interface/IERC2771Context.sol"; + +/** + * @dev Context variant with ERC2771 support. + */ + +library ERC2771ContextStorage { + /// @custom:storage-location erc7201:erc2771.context.storage + /// @dev keccak256(abi.encode(uint256(keccak256("erc2771.context.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant ERC2771_CONTEXT_STORAGE_POSITION = + 0x82aadcdf5bea62fd30615b6c0754b644e71b6c1e8c55b71bb927ad005b504f00; + + struct Data { + mapping(address => bool) trustedForwarder; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = ERC2771_CONTEXT_STORAGE_POSITION; + assembly { + data_.slot := position + } + } +} + +contract ERC2771Context is IERC2771Context { + constructor(address[] memory trustedForwarder) { + for (uint256 i = 0; i < trustedForwarder.length; i++) { + _erc2771ContextStorage().trustedForwarder[trustedForwarder[i]] = true; + } + } + + function isTrustedForwarder(address forwarder) public view virtual returns (bool) { + return _erc2771ContextStorage().trustedForwarder[forwarder]; + } + + function _msgSender() internal view virtual returns (address sender) { + if (isTrustedForwarder(msg.sender)) { + // The assembly code is more direct than the Solidity version using `abi.decode`. + assembly { + sender := shr(96, calldataload(sub(calldatasize(), 20))) + } + } else { + return msg.sender; + } + } + + function _msgData() internal view virtual returns (bytes calldata) { + if (isTrustedForwarder(msg.sender)) { + return msg.data[:msg.data.length - 20]; + } else { + return msg.data; + } + } + + /// @dev Returns the ERC2771ContextStorage storage. + function _erc2771ContextStorage() internal pure returns (ERC2771ContextStorage.Data storage data) { + data = ERC2771ContextStorage.data(); + } + + uint256[49] private __gap; +} diff --git a/contracts/extension/upgradeable/ERC2771ContextConsumer.sol b/contracts/extension/upgradeable/ERC2771ContextConsumer.sol new file mode 100644 index 000000000..1dde5ad5e --- /dev/null +++ b/contracts/extension/upgradeable/ERC2771ContextConsumer.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +interface IERC2771Context { + function isTrustedForwarder(address forwarder) external view returns (bool); +} + +/** + * @dev Context variant with ERC2771 support. + */ +abstract contract ERC2771ContextConsumer { + function _msgSender() public view virtual returns (address sender) { + if (IERC2771Context(address(this)).isTrustedForwarder(msg.sender)) { + // The assembly code is more direct than the Solidity version using `abi.decode`. + assembly { + sender := shr(96, calldataload(sub(calldatasize(), 20))) + } + } else { + return msg.sender; + } + } + + function _msgData() public view virtual returns (bytes calldata) { + if (IERC2771Context(address(this)).isTrustedForwarder(msg.sender)) { + return msg.data[:msg.data.length - 20]; + } else { + return msg.data; + } + } +} diff --git a/contracts/extension/upgradeable/ERC2771ContextUpgradeable.sol b/contracts/extension/upgradeable/ERC2771ContextUpgradeable.sol new file mode 100644 index 000000000..7ca71a972 --- /dev/null +++ b/contracts/extension/upgradeable/ERC2771ContextUpgradeable.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.0; + +import "../interface/IERC2771Context.sol"; +import "./Initializable.sol"; + +/** + * @dev Context variant with ERC2771 support. + */ + +library ERC2771ContextStorage { + /// @custom:storage-location erc7201:erc2771.context.storage + /// @dev keccak256(abi.encode(uint256(keccak256("erc2771.context.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant ERC2771_CONTEXT_STORAGE_POSITION = + 0x82aadcdf5bea62fd30615b6c0754b644e71b6c1e8c55b71bb927ad005b504f00; + + struct Data { + mapping(address => bool) trustedForwarder; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = ERC2771_CONTEXT_STORAGE_POSITION; + assembly { + data_.slot := position + } + } +} + +/** + * @dev Context variant with ERC2771 support. + */ +abstract contract ERC2771ContextUpgradeable is Initializable { + function __ERC2771Context_init(address[] memory trustedForwarder) internal onlyInitializing { + __ERC2771Context_init_unchained(trustedForwarder); + } + + function __ERC2771Context_init_unchained(address[] memory trustedForwarder) internal onlyInitializing { + for (uint256 i = 0; i < trustedForwarder.length; i++) { + _erc2771ContextStorage().trustedForwarder[trustedForwarder[i]] = true; + } + } + + function isTrustedForwarder(address forwarder) public view virtual returns (bool) { + return _erc2771ContextStorage().trustedForwarder[forwarder]; + } + + function _msgSender() internal view virtual returns (address sender) { + if (isTrustedForwarder(msg.sender)) { + // The assembly code is more direct than the Solidity version using `abi.decode`. + assembly { + sender := shr(96, calldataload(sub(calldatasize(), 20))) + } + } else { + return msg.sender; + } + } + + function _msgData() internal view virtual returns (bytes calldata) { + if (isTrustedForwarder(msg.sender)) { + return msg.data[:msg.data.length - 20]; + } else { + return msg.data; + } + } + + /// @dev Returns the ERC2771ContextStorage storage. + function _erc2771ContextStorage() internal pure returns (ERC2771ContextStorage.Data storage data) { + data = ERC2771ContextStorage.data(); + } + + uint256[49] private __gap; +} diff --git a/contracts/extension/upgradeable/Initializable.sol b/contracts/extension/upgradeable/Initializable.sol new file mode 100644 index 000000000..dfa3f4bcd --- /dev/null +++ b/contracts/extension/upgradeable/Initializable.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.0; + +import "../../lib/Address.sol"; + +library InitStorage { + /// @custom:storage-location erc7201:init.storage + /// @dev keccak256(abi.encode(uint256(keccak256("init.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 constant INIT_STORAGE_POSITION = 0x322cf19c484104d3b1a9c2982ebae869ede3fa5f6c4703ca41b9a48c76ee0300; + + /// @dev Layout of the entrypoint contract's storage. + struct Data { + uint8 initialized; + bool initializing; + } + + /// @dev Returns the entrypoint contract's data at the relevant storage location. + function data() internal pure returns (Data storage data_) { + bytes32 position = INIT_STORAGE_POSITION; + assembly { + data_.slot := position + } + } +} + +abstract contract Initializable { + /** + * @dev Triggered when the contract has been initialized or reinitialized. + */ + event Initialized(uint8 version); + + /** + * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope, + * `onlyInitializing` functions can be used to initialize parent contracts. Equivalent to `reinitializer(1)`. + */ + modifier initializer() { + uint8 _initialized = _initStorage().initialized; + bool _initializing = _initStorage().initializing; + + bool isTopLevelCall = !_initializing; + require( + (isTopLevelCall && _initialized < 1) || (!Address.isContract(address(this)) && _initialized == 1), + "Initializable: contract is already initialized" + ); + _initStorage().initialized = 1; + if (isTopLevelCall) { + _initStorage().initializing = true; + } + _; + if (isTopLevelCall) { + _initStorage().initializing = false; + emit Initialized(1); + } + } + + /** + * @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the + * contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be + * used to initialize parent contracts. + * + * `initializer` is equivalent to `reinitializer(1)`, so a reinitializer may be used after the original + * initialization step. This is essential to configure modules that are added through upgrades and that require + * initialization. + * + * Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in + * a contract, executing them in the right order is up to the developer or operator. + */ + modifier reinitializer(uint8 version) { + uint8 _initialized = _initStorage().initialized; + bool _initializing = _initStorage().initializing; + + require(!_initializing && _initialized < version, "Initializable: contract is already initialized"); + _initStorage().initialized = version; + _initStorage().initializing = true; + _; + _initStorage().initializing = false; + emit Initialized(version); + } + + /** + * @dev Modifier to protect an initialization function so that it can only be invoked by functions with the + * {initializer} and {reinitializer} modifiers, directly or indirectly. + */ + modifier onlyInitializing() { + require(_initStorage().initializing, "Initializable: contract is not initializing"); + _; + } + + /** + * @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. + * Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized + * to any version. It is recommended to use this to lock implementation contracts that are designed to be called + * through proxies. + */ + function _disableInitializers() internal virtual { + uint8 _initialized = _initStorage().initialized; + bool _initializing = _initStorage().initializing; + + require(!_initializing, "Initializable: contract is initializing"); + if (_initialized < type(uint8).max) { + _initStorage().initialized = type(uint8).max; + emit Initialized(type(uint8).max); + } + } + + /// @dev Returns the InitStorage storage. + function _initStorage() internal pure returns (InitStorage.Data storage data) { + data = InitStorage.data(); + } +} diff --git a/contracts/extension/upgradeable/LazyMint.sol b/contracts/extension/upgradeable/LazyMint.sol new file mode 100644 index 000000000..e4213836f --- /dev/null +++ b/contracts/extension/upgradeable/LazyMint.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../interface/ILazyMint.sol"; +import "./BatchMintMetadata.sol"; + +library LazyMintStorage { + /// @custom:storage-location erc7201:lazy.mint.storage + /// @dev keccak256(abi.encode(uint256(keccak256("lazy.mint.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant LAZY_MINT_STORAGE_POSITION = + 0xb9d1563179e0b515350da446a9b78048cef890c6aaa6e34cdf88122d970b5c00; + + struct Data { + /// @notice The tokenId assigned to the next new NFT to be lazy minted. + uint256 nextTokenIdToLazyMint; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = LAZY_MINT_STORAGE_POSITION; + assembly { + data_.slot := position + } + } +} + +/** + * The `LazyMint` is a contract extension for any base NFT contract. It lets you 'lazy mint' any number of NFTs + * at once. Here, 'lazy mint' means defining the metadata for particular tokenIds of your NFT contract, without actually + * minting a non-zero balance of NFTs of those tokenIds. + */ + +abstract contract LazyMint is ILazyMint, BatchMintMetadata { + function nextTokenIdToLazyMint() internal view returns (uint256) { + return _lazyMintStorage().nextTokenIdToLazyMint; + } + + /** + * @notice Lets an authorized address lazy mint a given amount of NFTs. + * + * @param _amount The number of NFTs to lazy mint. + * @param _baseURIForTokens The base URI for the 'n' number of NFTs being lazy minted, where the metadata for each + * of those NFTs is `${baseURIForTokens}/${tokenId}`. + * @param _data Additional bytes data to be used at the discretion of the consumer of the contract. + * @return batchId A unique integer identifier for the batch of NFTs lazy minted together. + */ + function lazyMint( + uint256 _amount, + string calldata _baseURIForTokens, + bytes calldata _data + ) public virtual override returns (uint256 batchId) { + if (!_canLazyMint()) { + revert("Not authorized"); + } + + if (_amount == 0) { + revert("0 amt"); + } + + uint256 startId = _lazyMintStorage().nextTokenIdToLazyMint; + + (_lazyMintStorage().nextTokenIdToLazyMint, batchId) = _batchMintMetadata(startId, _amount, _baseURIForTokens); + + emit TokensLazyMinted(startId, startId + _amount - 1, _baseURIForTokens, _data); + + return batchId; + } + + /// @dev Returns the LazyMintStorage storage. + function _lazyMintStorage() internal pure returns (LazyMintStorage.Data storage data) { + data = LazyMintStorage.data(); + } + + /// @dev Returns whether lazy minting can be performed in the given execution context. + function _canLazyMint() internal view virtual returns (bool); +} diff --git a/contracts/extension/upgradeable/OperatorFilterToggle.sol b/contracts/extension/upgradeable/OperatorFilterToggle.sol new file mode 100644 index 000000000..deb1707c1 --- /dev/null +++ b/contracts/extension/upgradeable/OperatorFilterToggle.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../interface/IOperatorFilterToggle.sol"; + +library OperatorFilterToggleStorage { + /// @custom:storage-location erc7201:operator.filter.toggle.storage + /// @dev keccak256(abi.encode(uint256(keccak256("operator.filter.toggle.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant OPERATOR_FILTER_TOGGLE_STORAGE_POSITION = + 0xc9c6c05578224a00a593ea5c05021a182582a08fc1143a677c61a8fe56c23800; + + struct Data { + bool operatorRestriction; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = OPERATOR_FILTER_TOGGLE_STORAGE_POSITION; + assembly { + data_.slot := position + } + } +} + +abstract contract OperatorFilterToggle is IOperatorFilterToggle { + function operatorRestriction() external view override returns (bool) { + return _operatorFilterToggleStorage().operatorRestriction; + } + + function setOperatorRestriction(bool _restriction) external { + require(_canSetOperatorRestriction(), "Not authorized to set operator restriction."); + _setOperatorRestriction(_restriction); + } + + function _setOperatorRestriction(bool _restriction) internal { + _operatorFilterToggleStorage().operatorRestriction = _restriction; + emit OperatorRestriction(_restriction); + } + + /// @dev Returns the OperatorFilterToggle storage. + function _operatorFilterToggleStorage() internal pure returns (OperatorFilterToggleStorage.Data storage data) { + data = OperatorFilterToggleStorage.data(); + } + + function _canSetOperatorRestriction() internal virtual returns (bool); +} diff --git a/contracts/extension/upgradeable/OperatorFiltererUpgradeable.sol b/contracts/extension/upgradeable/OperatorFiltererUpgradeable.sol new file mode 100644 index 000000000..dd5a2c04c --- /dev/null +++ b/contracts/extension/upgradeable/OperatorFiltererUpgradeable.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../interface/IOperatorFilterRegistry.sol"; +import "./OperatorFilterToggle.sol"; + +abstract contract OperatorFiltererUpgradeable is OperatorFilterToggle { + IOperatorFilterRegistry constant OPERATOR_FILTER_REGISTRY = + IOperatorFilterRegistry(0x000000000000AAeB6D7670E522A718067333cd4E); + + function __OperatorFilterer_init(address subscriptionOrRegistrantToCopy, bool subscribe) internal { + // If an inheriting token contract is deployed to a network without the registry deployed, the modifier + // will not revert, but the contract will need to be registered with the registry once it is deployed in + // order for the modifier to filter addresses. + if (address(OPERATOR_FILTER_REGISTRY).code.length > 0) { + if (!OPERATOR_FILTER_REGISTRY.isRegistered(address(this))) { + if (subscribe) { + OPERATOR_FILTER_REGISTRY.registerAndSubscribe(address(this), subscriptionOrRegistrantToCopy); + } else { + if (subscriptionOrRegistrantToCopy != address(0)) { + OPERATOR_FILTER_REGISTRY.registerAndCopyEntries(address(this), subscriptionOrRegistrantToCopy); + } else { + OPERATOR_FILTER_REGISTRY.register(address(this)); + } + } + } + } + } + + modifier onlyAllowedOperator(address from) virtual { + // Check registry code length to facilitate testing in environments without a deployed registry. + if (_operatorFilterToggleStorage().operatorRestriction) { + if (address(OPERATOR_FILTER_REGISTRY).code.length > 0) { + // Allow spending tokens from addresses with balance + // Note that this still allows listings and marketplaces with escrow to transfer tokens if transferred + // from an EOA. + if (from == msg.sender) { + _; + return; + } + OPERATOR_FILTER_REGISTRY.isOperatorAllowed(address(this), msg.sender); + } + } + _; + } + + modifier onlyAllowedOperatorApproval(address operator) virtual { + // Check registry code length to facilitate testing in environments without a deployed registry. + if (_operatorFilterToggleStorage().operatorRestriction) { + if (address(OPERATOR_FILTER_REGISTRY).code.length > 0) { + OPERATOR_FILTER_REGISTRY.isOperatorAllowed(address(this), msg.sender); + } + } + _; + } +} diff --git a/contracts/extension/upgradeable/Ownable.sol b/contracts/extension/upgradeable/Ownable.sol new file mode 100644 index 000000000..db048dba1 --- /dev/null +++ b/contracts/extension/upgradeable/Ownable.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../interface/IOwnable.sol"; + +/** + * @title Ownable + * @notice Thirdweb's `Ownable` is a contract extension to be used with any base contract. It exposes functions for setting and reading + * who the 'owner' of the inheriting smart contract is, and lets the inheriting contract perform conditional logic that uses + * information about who the contract's owner is. + */ + +library OwnableStorage { + /// @custom:storage-location erc7201:extension.manager.storage + bytes32 public constant OWNABLE_STORAGE_POSITION = + keccak256(abi.encode(uint256(keccak256("ownable.storage")) - 1)) & ~bytes32(uint256(0xff)); + + struct Data { + /// @dev Owner of the contract (purpose: OpenSea compatibility) + address _owner; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = OWNABLE_STORAGE_POSITION; + assembly { + data_.slot := position + } + } +} + +abstract contract Ownable is IOwnable { + /// @dev Reverts if caller is not the owner. + modifier onlyOwner() { + if (msg.sender != _ownableStorage()._owner) { + revert("Not authorized"); + } + _; + } + + /** + * @notice Returns the owner of the contract. + */ + function owner() public view override returns (address) { + return _ownableStorage()._owner; + } + + /** + * @notice Lets an authorized wallet set a new owner for the contract. + * @param _newOwner The address to set as the new owner of the contract. + */ + function setOwner(address _newOwner) external override { + if (!_canSetOwner()) { + revert("Not authorized"); + } + _setupOwner(_newOwner); + } + + /// @dev Lets a contract admin set a new owner for the contract. The new owner must be a contract admin. + function _setupOwner(address _newOwner) internal { + address _prevOwner = _ownableStorage()._owner; + _ownableStorage()._owner = _newOwner; + + emit OwnerUpdated(_prevOwner, _newOwner); + } + + /// @dev Returns the Ownable storage. + function _ownableStorage() internal pure returns (OwnableStorage.Data storage data) { + data = OwnableStorage.data(); + } + + /// @dev Returns whether owner can be set in the given execution context. + function _canSetOwner() internal view virtual returns (bool); +} diff --git a/contracts/extension/upgradeable/Permissions.sol b/contracts/extension/upgradeable/Permissions.sol new file mode 100644 index 000000000..c1fa7925e --- /dev/null +++ b/contracts/extension/upgradeable/Permissions.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../interface/IPermissions.sol"; +import "../../lib/Strings.sol"; + +/** + * @title Permissions + * @dev This contracts provides extending-contracts with role-based access control mechanisms + */ + +library PermissionsStorage { + /// @custom:storage-location erc7201:permissions.storage + /// @dev keccak256(abi.encode(uint256(keccak256("permissions.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant PERMISSIONS_STORAGE_POSITION = + 0x0a7b0f5c59907924802379ebe98cdc23e2ee7820f63d30126e10b3752010e500; + + struct Data { + /// @dev Map from keccak256 hash of a role => a map from address => whether address has role. + mapping(bytes32 => mapping(address => bool)) _hasRole; + /// @dev Map from keccak256 hash of a role to role admin. See {getRoleAdmin}. + mapping(bytes32 => bytes32) _getRoleAdmin; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = PERMISSIONS_STORAGE_POSITION; + assembly { + data_.slot := position + } + } +} + +contract Permissions is IPermissions { + /// @dev Default admin role for all roles. Only accounts with this role can grant/revoke other roles. + bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; + + /// @dev Modifier that checks if an account has the specified role; reverts otherwise. + modifier onlyRole(bytes32 role) { + _checkRole(role, _msgSender()); + _; + } + + /** + * @notice Checks whether an account has a particular role. + * @dev Returns `true` if `account` has been granted `role`. + * + * @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + * @param account Address of the account for which the role is being checked. + */ + function hasRole(bytes32 role, address account) public view override returns (bool) { + return _permissionsStorage()._hasRole[role][account]; + } + + /** + * @notice Checks whether an account has a particular role; + * role restrictions can be swtiched on and off. + * + * @dev Returns `true` if `account` has been granted `role`. + * Role restrictions can be swtiched on and off: + * - If address(0) has ROLE, then the ROLE restrictions + * don't apply. + * - If address(0) does not have ROLE, then the ROLE + * restrictions will apply. + * + * @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + * @param account Address of the account for which the role is being checked. + */ + function hasRoleWithSwitch(bytes32 role, address account) public view returns (bool) { + if (!_permissionsStorage()._hasRole[role][address(0)]) { + return _permissionsStorage()._hasRole[role][account]; + } + + return true; + } + + /** + * @notice Returns the admin role that controls the specified role. + * @dev See {grantRole} and {revokeRole}. + * To change a role's admin, use {_setRoleAdmin}. + * + * @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + */ + function getRoleAdmin(bytes32 role) external view override returns (bytes32) { + return _permissionsStorage()._getRoleAdmin[role]; + } + + /** + * @notice Grants a role to an account, if not previously granted. + * @dev Caller must have admin role for the `role`. + * Emits {RoleGranted Event}. + * + * @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + * @param account Address of the account to which the role is being granted. + */ + function grantRole(bytes32 role, address account) public virtual override { + _checkRole(_permissionsStorage()._getRoleAdmin[role], _msgSender()); + if (_permissionsStorage()._hasRole[role][account]) { + revert("Can only grant to non holders"); + } + _setupRole(role, account); + } + + /** + * @notice Revokes role from an account. + * @dev Caller must have admin role for the `role`. + * Emits {RoleRevoked Event}. + * + * @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + * @param account Address of the account from which the role is being revoked. + */ + function revokeRole(bytes32 role, address account) public virtual override { + _checkRole(_permissionsStorage()._getRoleAdmin[role], _msgSender()); + _revokeRole(role, account); + } + + /** + * @notice Revokes role from the account. + * @dev Caller must have the `role`, with caller being the same as `account`. + * Emits {RoleRevoked Event}. + * + * @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + * @param account Address of the account from which the role is being revoked. + */ + function renounceRole(bytes32 role, address account) public virtual override { + if (_msgSender() != account) { + revert("Can only renounce for self"); + } + _revokeRole(role, account); + } + + /// @dev Sets `adminRole` as `role`'s admin role. + function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual { + bytes32 previousAdminRole = _permissionsStorage()._getRoleAdmin[role]; + _permissionsStorage()._getRoleAdmin[role] = adminRole; + emit RoleAdminChanged(role, previousAdminRole, adminRole); + } + + /// @dev Sets up `role` for `account` + function _setupRole(bytes32 role, address account) internal virtual { + _permissionsStorage()._hasRole[role][account] = true; + emit RoleGranted(role, account, _msgSender()); + } + + /// @dev Revokes `role` from `account` + function _revokeRole(bytes32 role, address account) internal virtual { + _checkRole(role, account); + delete _permissionsStorage()._hasRole[role][account]; + emit RoleRevoked(role, account, _msgSender()); + } + + /// @dev Checks `role` for `account`. Reverts with a message including the required role. + function _checkRole(bytes32 role, address account) internal view virtual { + if (!_permissionsStorage()._hasRole[role][account]) { + revert( + string( + abi.encodePacked( + "Permissions: account ", + Strings.toHexString(uint160(account), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ) + ); + } + } + + /// @dev Checks `role` for `account`. Reverts with a message including the required role. + function _checkRoleWithSwitch(bytes32 role, address account) internal view virtual { + if (!hasRoleWithSwitch(role, account)) { + revert( + string( + abi.encodePacked( + "Permissions: account ", + Strings.toHexString(uint160(account), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ) + ); + } + } + + function _msgSender() internal view virtual returns (address sender) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } + + /// @dev Returns the Permissions storage. + function _permissionsStorage() internal pure returns (PermissionsStorage.Data storage data) { + data = PermissionsStorage.data(); + } +} diff --git a/contracts/extension/upgradeable/PermissionsEnumerable.sol b/contracts/extension/upgradeable/PermissionsEnumerable.sol new file mode 100644 index 000000000..5307b7e54 --- /dev/null +++ b/contracts/extension/upgradeable/PermissionsEnumerable.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../interface/IPermissionsEnumerable.sol"; +import "./Permissions.sol"; + +/** + * @title PermissionsEnumerable + * @dev This contracts provides extending-contracts with role-based access control mechanisms. + * Also provides interfaces to view all members with a given role, and total count of members. + */ + +library PermissionsEnumerableStorage { + /// @custom:storage-location erc7201:extension.manager.storage + bytes32 public constant PERMISSIONS_ENUMERABLE_STORAGE_POSITION = + keccak256(abi.encode(uint256(keccak256("permissions.enumerable.storage")) - 1)) & ~bytes32(uint256(0xff)); + + /** + * @notice A data structure to store data of members for a given role. + * + * @param index Current index in the list of accounts that have a role. + * @param members map from index => address of account that has a role + * @param indexOf map from address => index which the account has. + */ + struct RoleMembers { + uint256 index; + mapping(uint256 => address) members; + mapping(address => uint256) indexOf; + } + + struct Data { + /// @dev map from keccak256 hash of a role to its members' data. See {RoleMembers}. + mapping(bytes32 => RoleMembers) roleMembers; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = PERMISSIONS_ENUMERABLE_STORAGE_POSITION; + assembly { + data_.slot := position + } + } +} + +contract PermissionsEnumerable is IPermissionsEnumerable, Permissions { + /** + * @notice Returns the role-member from a list of members for a role, + * at a given index. + * @dev Returns `member` who has `role`, at `index` of role-members list. + * See struct {RoleMembers}, and mapping {roleMembers} + * + * @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + * @param index Index in list of current members for the role. + * + * @return member Address of account that has `role` + */ + function getRoleMember(bytes32 role, uint256 index) external view override returns (address member) { + uint256 currentIndex = _permissionsEnumerableStorage().roleMembers[role].index; + uint256 check; + + for (uint256 i = 0; i < currentIndex; i += 1) { + if (_permissionsEnumerableStorage().roleMembers[role].members[i] != address(0)) { + if (check == index) { + member = _permissionsEnumerableStorage().roleMembers[role].members[i]; + return member; + } + check += 1; + } else if ( + hasRole(role, address(0)) && i == _permissionsEnumerableStorage().roleMembers[role].indexOf[address(0)] + ) { + check += 1; + } + } + } + + /** + * @notice Returns total number of accounts that have a role. + * @dev Returns `count` of accounts that have `role`. + * See struct {RoleMembers}, and mapping {roleMembers} + * + * @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") + * + * @return count Total number of accounts that have `role` + */ + function getRoleMemberCount(bytes32 role) external view override returns (uint256 count) { + uint256 currentIndex = _permissionsEnumerableStorage().roleMembers[role].index; + + for (uint256 i = 0; i < currentIndex; i += 1) { + if (_permissionsEnumerableStorage().roleMembers[role].members[i] != address(0)) { + count += 1; + } + } + if (hasRole(role, address(0))) { + count += 1; + } + } + + /// @dev Revokes `role` from `account`, and removes `account` from {roleMembers} + /// See {_removeMember} + function _revokeRole(bytes32 role, address account) internal virtual override { + super._revokeRole(role, account); + _removeMember(role, account); + } + + /// @dev Grants `role` to `account`, and adds `account` to {roleMembers} + /// See {_addMember} + function _setupRole(bytes32 role, address account) internal virtual override { + super._setupRole(role, account); + _addMember(role, account); + } + + /// @dev adds `account` to {roleMembers}, for `role` + function _addMember(bytes32 role, address account) internal { + uint256 idx = _permissionsEnumerableStorage().roleMembers[role].index; + _permissionsEnumerableStorage().roleMembers[role].index += 1; + + _permissionsEnumerableStorage().roleMembers[role].members[idx] = account; + _permissionsEnumerableStorage().roleMembers[role].indexOf[account] = idx; + } + + /// @dev removes `account` from {roleMembers}, for `role` + function _removeMember(bytes32 role, address account) internal { + uint256 idx = _permissionsEnumerableStorage().roleMembers[role].indexOf[account]; + + delete _permissionsEnumerableStorage().roleMembers[role].members[idx]; + delete _permissionsEnumerableStorage().roleMembers[role].indexOf[account]; + } + + /// @dev Returns the PermissionsEnumerable storage. + function _permissionsEnumerableStorage() internal pure returns (PermissionsEnumerableStorage.Data storage data) { + data = PermissionsEnumerableStorage.data(); + } +} diff --git a/contracts/extension/upgradeable/PlatformFee.sol b/contracts/extension/upgradeable/PlatformFee.sol new file mode 100644 index 000000000..6b9e9ef21 --- /dev/null +++ b/contracts/extension/upgradeable/PlatformFee.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../interface/IPlatformFee.sol"; + +/** + * @author thirdweb.com + */ +library PlatformFeeStorage { + /// @custom:storage-location erc7201:platform.fee.storage + /// @dev keccak256(abi.encode(uint256(keccak256("platform.fee.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant PLATFORM_FEE_STORAGE_POSITION = + 0xc0c34308b4a2f4c5ee9af8ba82541cfb3c33b076d1fd05c65f9ce7060c64c400; + + struct Data { + /// @dev The address that receives all platform fees from all sales. + address platformFeeRecipient; + /// @dev The % of primary sales collected as platform fees. + uint16 platformFeeBps; + /// @dev Fee type variants: percentage fee and flat fee + IPlatformFee.PlatformFeeType platformFeeType; + /// @dev The flat amount collected by the contract as fees on primary sales. + uint256 flatPlatformFee; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = PLATFORM_FEE_STORAGE_POSITION; + assembly { + data_.slot := position + } + } +} + +/** + * @author thirdweb.com + * + * @title Platform Fee + * @notice Thirdweb's `PlatformFee` is a contract extension to be used with any base contract. It exposes functions for setting and reading + * the recipient of platform fee and the platform fee basis points, and lets the inheriting contract perform conditional logic + * that uses information about platform fees, if desired. + */ + +abstract contract PlatformFee is IPlatformFee { + /// @dev Returns the platform fee recipient and bps. + function getPlatformFeeInfo() public view override returns (address, uint16) { + return (_platformFeeStorage().platformFeeRecipient, uint16(_platformFeeStorage().platformFeeBps)); + } + + /// @dev Returns the platform fee bps and recipient. + function getFlatPlatformFeeInfo() public view returns (address, uint256) { + return (_platformFeeStorage().platformFeeRecipient, _platformFeeStorage().flatPlatformFee); + } + + /// @dev Returns the platform fee type. + function getPlatformFeeType() public view returns (PlatformFeeType) { + return _platformFeeStorage().platformFeeType; + } + + /** + * @notice Updates the platform fee recipient and bps. + * @dev Caller should be authorized to set platform fee info. + * See {_canSetPlatformFeeInfo}. + * Emits {PlatformFeeInfoUpdated Event}; See {_setupPlatformFeeInfo}. + * + * @param _platformFeeRecipient Address to be set as new platformFeeRecipient. + * @param _platformFeeBps Updated platformFeeBps. + */ + function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) external override { + if (!_canSetPlatformFeeInfo()) { + revert("Not authorized"); + } + _setupPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } + + /// @dev Lets a contract admin update the platform fee recipient and bps + function _setupPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) internal { + if (_platformFeeBps > 10_000) { + revert("Exceeds max bps"); + } + if (_platformFeeRecipient == address(0)) { + revert("Invalid recipient"); + } + + _platformFeeStorage().platformFeeBps = uint16(_platformFeeBps); + _platformFeeStorage().platformFeeRecipient = _platformFeeRecipient; + + emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); + } + + /// @notice Lets a module admin set a flat fee on primary sales. + function setFlatPlatformFeeInfo(address _platformFeeRecipient, uint256 _flatFee) external { + if (!_canSetPlatformFeeInfo()) { + revert("Not authorized"); + } + + _setupFlatPlatformFeeInfo(_platformFeeRecipient, _flatFee); + } + + /// @dev Sets a flat fee on primary sales. + function _setupFlatPlatformFeeInfo(address _platformFeeRecipient, uint256 _flatFee) internal { + _platformFeeStorage().flatPlatformFee = _flatFee; + _platformFeeStorage().platformFeeRecipient = _platformFeeRecipient; + + emit FlatPlatformFeeUpdated(_platformFeeRecipient, _flatFee); + } + + /// @notice Lets a module admin set platform fee type. + function setPlatformFeeType(PlatformFeeType _feeType) external { + if (!_canSetPlatformFeeInfo()) { + revert("Not authorized"); + } + _setupPlatformFeeType(_feeType); + } + + /// @dev Sets platform fee type. + function _setupPlatformFeeType(PlatformFeeType _feeType) internal { + _platformFeeStorage().platformFeeType = _feeType; + + emit PlatformFeeTypeUpdated(_feeType); + } + + /// @dev Returns the PlatformFee storage. + function _platformFeeStorage() internal pure returns (PlatformFeeStorage.Data storage data) { + data = PlatformFeeStorage.data(); + } + + /// @dev Returns whether platform fee info can be set in the given execution context. + function _canSetPlatformFeeInfo() internal view virtual returns (bool); +} diff --git a/contracts/extension/upgradeable/PrimarySale.sol b/contracts/extension/upgradeable/PrimarySale.sol new file mode 100644 index 000000000..6280f6d15 --- /dev/null +++ b/contracts/extension/upgradeable/PrimarySale.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../interface/IPrimarySale.sol"; + +library PrimarySaleStorage { + /// @custom:storage-location erc7201:extension.manager.storage + bytes32 public constant PRIMARY_SALE_STORAGE_POSITION = + keccak256(abi.encode(uint256(keccak256("primary.sale.storage")) - 1)) & ~bytes32(uint256(0xff)); + + struct Data { + address recipient; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = PRIMARY_SALE_STORAGE_POSITION; + assembly { + data_.slot := position + } + } +} + +/** + * @title Primary Sale + * @notice Thirdweb's `PrimarySale` is a contract extension to be used with any base contract. It exposes functions for setting and reading + * the recipient of primary sales, and lets the inheriting contract perform conditional logic that uses information about + * primary sales, if desired. + */ + +abstract contract PrimarySale is IPrimarySale { + /// @dev Returns primary sale recipient address. + function primarySaleRecipient() public view override returns (address) { + return _primarySaleStorage().recipient; + } + + /** + * @notice Updates primary sale recipient. + * @dev Caller should be authorized to set primary sales info. + * See {_canSetPrimarySaleRecipient}. + * Emits {PrimarySaleRecipientUpdated Event}; See {_setupPrimarySaleRecipient}. + * + * @param _saleRecipient Address to be set as new recipient of primary sales. + */ + function setPrimarySaleRecipient(address _saleRecipient) external override { + if (!_canSetPrimarySaleRecipient()) { + revert("Not authorized"); + } + _setupPrimarySaleRecipient(_saleRecipient); + } + + /// @dev Lets a contract admin set the recipient for all primary sales. + function _setupPrimarySaleRecipient(address _saleRecipient) internal { + if (_saleRecipient == address(0)) { + revert("Invalid recipient"); + } + _primarySaleStorage().recipient = _saleRecipient; + emit PrimarySaleRecipientUpdated(_saleRecipient); + } + + /// @dev Returns the PrimarySale storage. + function _primarySaleStorage() internal pure returns (PrimarySaleStorage.Data storage data) { + data = PrimarySaleStorage.data(); + } + + /// @dev Returns whether primary sale recipient can be set in the given execution context. + function _canSetPrimarySaleRecipient() internal view virtual returns (bool); +} diff --git a/contracts/extension/upgradeable/ReentrancyGuard.sol b/contracts/extension/upgradeable/ReentrancyGuard.sol new file mode 100644 index 000000000..c815dac97 --- /dev/null +++ b/contracts/extension/upgradeable/ReentrancyGuard.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (security/ReentrancyGuard.sol) + +pragma solidity ^0.8.0; + +library ReentrancyGuardStorage { + /// @custom:storage-location erc7201:reentrancy.guard.storage + /// @dev keccak256(abi.encode(uint256(keccak256("reentrancy.guard.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant REENTRANCY_GUARD_STORAGE_POSITION = + 0x1d281c488dae143b6ea4122e80c65059929950b9c32f17fc57be22089d9c3b00; + + struct Data { + uint256 _status; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = REENTRANCY_GUARD_STORAGE_POSITION; + assembly { + data_.slot := position + } + } +} + +abstract contract ReentrancyGuard { + uint256 private constant _NOT_ENTERED = 1; + uint256 private constant _ENTERED = 2; + + constructor() { + _reentrancyGuardStorage()._status = _NOT_ENTERED; + } + + /** + * @dev Prevents a contract from calling itself, directly or indirectly. + */ + modifier nonReentrant() { + // On the first call to nonReentrant, _notEntered will be true + require(_reentrancyGuardStorage()._status != _ENTERED, "ReentrancyGuard: reentrant call"); + + // Any calls to nonReentrant after this point will fail + _reentrancyGuardStorage()._status = _ENTERED; + + _; + + // By storing the original value once again, a refund is triggered (see + // https://eips.ethereum.org/EIPS/eip-2200) + _reentrancyGuardStorage()._status = _NOT_ENTERED; + } + + /// @dev Returns the ReentrancyGuard storage. + function _reentrancyGuardStorage() internal pure returns (ReentrancyGuardStorage.Data storage data) { + data = ReentrancyGuardStorage.data(); + } +} diff --git a/contracts/extension/upgradeable/Royalty.sol b/contracts/extension/upgradeable/Royalty.sol new file mode 100644 index 000000000..b8cadcd02 --- /dev/null +++ b/contracts/extension/upgradeable/Royalty.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../interface/IRoyalty.sol"; + +library RoyaltyStorage { + /// @custom:storage-location erc7201:royalty.storage + /// @dev keccak256(abi.encode(uint256(keccak256("royalty.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant ROYALTY_STORAGE_POSITION = + 0x8116a128b135962baae86382f90f26a5e28c4bb803b8888f92fd98e3bbbc6d00; + + struct Data { + /// @dev The (default) address that receives all royalty value. + address royaltyRecipient; + /// @dev The (default) % of a sale to take as royalty (in basis points). + uint16 royaltyBps; + /// @dev Token ID => royalty recipient and bps for token + mapping(uint256 => IRoyalty.RoyaltyInfo) royaltyInfoForToken; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = ROYALTY_STORAGE_POSITION; + assembly { + data_.slot := position + } + } +} + +/** + * @title Royalty + * @notice Thirdweb's `Royalty` is a contract extension to be used with any base contract. It exposes functions for setting and reading + * the recipient of royalty fee and the royalty fee basis points, and lets the inheriting contract perform conditional logic + * that uses information about royalty fees, if desired. + * + * @dev The `Royalty` contract is ERC2981 compliant. + */ + +abstract contract Royalty is IRoyalty { + /** + * @notice View royalty info for a given token and sale price. + * @dev Returns royalty amount and recipient for `tokenId` and `salePrice`. + * @param tokenId The tokenID of the NFT for which to query royalty info. + * @param salePrice Sale price of the token. + * + * @return receiver Address of royalty recipient account. + * @return royaltyAmount Royalty amount calculated at current royaltyBps value. + */ + function royaltyInfo( + uint256 tokenId, + uint256 salePrice + ) external view virtual override returns (address receiver, uint256 royaltyAmount) { + (address recipient, uint256 bps) = getRoyaltyInfoForToken(tokenId); + receiver = recipient; + royaltyAmount = (salePrice * bps) / 10_000; + } + + /** + * @notice View royalty info for a given token. + * @dev Returns royalty recipient and bps for `_tokenId`. + * @param _tokenId The tokenID of the NFT for which to query royalty info. + */ + function getRoyaltyInfoForToken(uint256 _tokenId) public view override returns (address, uint16) { + RoyaltyInfo memory royaltyForToken = _royaltyStorage().royaltyInfoForToken[_tokenId]; + + return + royaltyForToken.recipient == address(0) + ? (_royaltyStorage().royaltyRecipient, uint16(_royaltyStorage().royaltyBps)) + : (royaltyForToken.recipient, uint16(royaltyForToken.bps)); + } + + /** + * @notice Returns the defualt royalty recipient and BPS for this contract's NFTs. + */ + function getDefaultRoyaltyInfo() external view override returns (address, uint16) { + return (_royaltyStorage().royaltyRecipient, uint16(_royaltyStorage().royaltyBps)); + } + + /** + * @notice Updates default royalty recipient and bps. + * @dev Caller should be authorized to set royalty info. + * See {_canSetRoyaltyInfo}. + * Emits {DefaultRoyalty Event}; See {_setupDefaultRoyaltyInfo}. + * + * @param _royaltyRecipient Address to be set as default royalty recipient. + * @param _royaltyBps Updated royalty bps. + */ + function setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) external override { + if (!_canSetRoyaltyInfo()) { + revert("Not authorized"); + } + + _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + } + + /// @dev Lets a contract admin update the default royalty recipient and bps. + function _setupDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) internal { + if (_royaltyBps > 10_000) { + revert("Exceeds max bps"); + } + + _royaltyStorage().royaltyRecipient = _royaltyRecipient; + _royaltyStorage().royaltyBps = uint16(_royaltyBps); + + emit DefaultRoyalty(_royaltyRecipient, _royaltyBps); + } + + /** + * @notice Updates default royalty recipient and bps for a particular token. + * @dev Sets royalty info for `_tokenId`. Caller should be authorized to set royalty info. + * See {_canSetRoyaltyInfo}. + * Emits {RoyaltyForToken Event}; See {_setupRoyaltyInfoForToken}. + * + * @param _recipient Address to be set as royalty recipient for given token Id. + * @param _bps Updated royalty bps for the token Id. + */ + function setRoyaltyInfoForToken(uint256 _tokenId, address _recipient, uint256 _bps) external override { + if (!_canSetRoyaltyInfo()) { + revert("Not authorized"); + } + + _setupRoyaltyInfoForToken(_tokenId, _recipient, _bps); + } + + /// @dev Lets a contract admin set the royalty recipient and bps for a particular token Id. + function _setupRoyaltyInfoForToken(uint256 _tokenId, address _recipient, uint256 _bps) internal { + if (_bps > 10_000) { + revert("Exceeds max bps"); + } + + _royaltyStorage().royaltyInfoForToken[_tokenId] = RoyaltyInfo({ recipient: _recipient, bps: _bps }); + + emit RoyaltyForToken(_tokenId, _recipient, _bps); + } + + /// @dev Returns the Royalty storage. + function _royaltyStorage() internal pure returns (RoyaltyStorage.Data storage data) { + data = RoyaltyStorage.data(); + } + + /// @dev Returns whether royalty info can be set in the given execution context. + function _canSetRoyaltyInfo() internal view virtual returns (bool); +} diff --git a/contracts/extension/upgradeable/RoyaltyPayments.sol b/contracts/extension/upgradeable/RoyaltyPayments.sol new file mode 100644 index 000000000..2b5f504be --- /dev/null +++ b/contracts/extension/upgradeable/RoyaltyPayments.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../interface/IRoyaltyPayments.sol"; +import "../interface/IRoyaltyEngineV1.sol"; +import { IERC2981 } from "../../eip/interface/IERC2981.sol"; + +library RoyaltyPaymentsStorage { + /// @custom:storage-location erc7201:royalty.payments.storage + /// @dev keccak256(abi.encode(uint256(keccak256("royalty.payments.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant ROYALTY_PAYMENTS_STORAGE_POSITION = + 0xc802b338f3fb784853cf3c808df5ff08335200e394ea2c687d12571a91045000; + + struct Data { + /// @dev The address of RoyaltyEngineV1, replacing the one set during construction. + address royaltyEngineAddressOverride; + } + + function royaltyPaymentsStorage() internal pure returns (Data storage royaltyPaymentsData) { + bytes32 position = ROYALTY_PAYMENTS_STORAGE_POSITION; + assembly { + royaltyPaymentsData.slot := position + } + } +} + +/** + * @author thirdweb.com + * + * @title Royalty Payments + * @notice Thirdweb's `RoyaltyPayments` is a contract extension to be used with a marketplace contract. + * It exposes functions for fetching royalty settings for a token. + * It Supports RoyaltyEngineV1 and RoyaltyRegistry by manifold.xyz. + */ + +abstract contract RoyaltyPaymentsLogic is IRoyaltyPayments { + // solhint-disable-next-line var-name-mixedcase + address immutable ROYALTY_ENGINE_ADDRESS; + + constructor(address _royaltyEngineAddress) { + // allow address(0) in case RoyaltyEngineV1 not present on a network + require( + _royaltyEngineAddress == address(0) || + IERC165(_royaltyEngineAddress).supportsInterface(type(IRoyaltyEngineV1).interfaceId), + "Doesn't support IRoyaltyEngineV1 interface" + ); + + ROYALTY_ENGINE_ADDRESS = _royaltyEngineAddress; + } + + /** + * Get the royalty for a given token (address, id) and value amount. Does not cache the bps/amounts. Caches the spec for a given token address + * + * @param tokenAddress - The address of the token + * @param tokenId - The id of the token + * @param value - The value you wish to get the royalty of + * + * returns Two arrays of equal length, royalty recipients and the corresponding amount each recipient should get + */ + function getRoyalty( + address tokenAddress, + uint256 tokenId, + uint256 value + ) external returns (address payable[] memory recipients, uint256[] memory amounts) { + address royaltyEngineAddress = getRoyaltyEngineAddress(); + + if (royaltyEngineAddress == address(0)) { + try IERC2981(tokenAddress).royaltyInfo(tokenId, value) returns (address recipient, uint256 amount) { + require(amount <= value, "Invalid royalty amount"); + + recipients = new address payable[](1); + amounts = new uint256[](1); + recipients[0] = payable(recipient); + amounts[0] = amount; + } catch {} + } else { + (recipients, amounts) = IRoyaltyEngineV1(royaltyEngineAddress).getRoyalty(tokenAddress, tokenId, value); + } + } + + /** + * Set or override RoyaltyEngine address + * + * @param _royaltyEngineAddress - RoyaltyEngineV1 address + */ + function setRoyaltyEngine(address _royaltyEngineAddress) external { + if (!_canSetRoyaltyEngine()) { + revert("Not authorized"); + } + + require( + _royaltyEngineAddress != address(0) && + IERC165(_royaltyEngineAddress).supportsInterface(type(IRoyaltyEngineV1).interfaceId), + "Doesn't support IRoyaltyEngineV1 interface" + ); + + _setupRoyaltyEngine(_royaltyEngineAddress); + } + + /// @dev Returns original or overridden address for RoyaltyEngineV1 + function getRoyaltyEngineAddress() public view returns (address royaltyEngineAddress) { + RoyaltyPaymentsStorage.Data storage data = RoyaltyPaymentsStorage.royaltyPaymentsStorage(); + address royaltyEngineOverride = data.royaltyEngineAddressOverride; + royaltyEngineAddress = royaltyEngineOverride != address(0) ? royaltyEngineOverride : ROYALTY_ENGINE_ADDRESS; + } + + /// @dev Lets a contract admin update the royalty engine address + function _setupRoyaltyEngine(address _royaltyEngineAddress) internal { + RoyaltyPaymentsStorage.Data storage data = RoyaltyPaymentsStorage.royaltyPaymentsStorage(); + address currentAddress = data.royaltyEngineAddressOverride; + + data.royaltyEngineAddressOverride = _royaltyEngineAddress; + + emit RoyaltyEngineUpdated(currentAddress, _royaltyEngineAddress); + } + + /// @dev Returns whether royalty engine address can be set in the given execution context. + function _canSetRoyaltyEngine() internal view virtual returns (bool); +} diff --git a/contracts/extension/upgradeable/RulesEngine.sol b/contracts/extension/upgradeable/RulesEngine.sol new file mode 100644 index 000000000..93fa215b9 --- /dev/null +++ b/contracts/extension/upgradeable/RulesEngine.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +import "../interface/IRulesEngine.sol"; + +import "../../eip/interface/IERC20.sol"; +import "../../eip/interface/IERC20Metadata.sol"; +import "../../eip/interface/IERC721.sol"; +import "../../eip/interface/IERC1155.sol"; + +import "../../external-deps/openzeppelin/utils/structs/EnumerableSet.sol"; + +library RulesEngineStorage { + /// @custom:storage-location erc7201:rules.engine.storage + /// @dev keccak256(abi.encode(uint256(keccak256("rules.engine.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant RULES_ENGINE_STORAGE_POSITION = + 0x41d4cb087b2c44a761b2288e4c8ac115e76a546efd837c9a2e9cec2661a49a00; + + struct Data { + address rulesEngineOverride; + EnumerableSet.Bytes32Set ids; + mapping(bytes32 => IRulesEngine.RuleWithId) rules; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = RULES_ENGINE_STORAGE_POSITION; + assembly { + data_.slot := position + } + } +} + +abstract contract RulesEngine is IRulesEngine { + using EnumerableSet for EnumerableSet.Bytes32Set; + + /*/////////////////////////////////////////////////////////////// + View functions + //////////////////////////////////////////////////////////////*/ + + function getScore(address _tokenOwner) public view returns (uint256 score) { + address engineOverride = getRulesEngineOverride(); + if (engineOverride != address(0)) { + return IRulesEngine(engineOverride).getScore(_tokenOwner); + } + + bytes32[] memory ids = _rulesEngineStorage().ids.values(); + uint256 len = ids.length; + + for (uint256 i = 0; i < len; i += 1) { + RuleWithId memory rule = _rulesEngineStorage().rules[ids[i]]; + score += _getScoreForRule(_tokenOwner, rule); + } + } + + function getAllRules() external view returns (RuleWithId[] memory rules) { + bytes32[] memory ids = _rulesEngineStorage().ids.values(); + uint256 len = ids.length; + + rules = new RuleWithId[](len); + + for (uint256 i = 0; i < len; i += 1) { + rules[i] = _rulesEngineStorage().rules[ids[i]]; + } + } + + function getRulesEngineOverride() public view returns (address rulesEngineAddress) { + rulesEngineAddress = _rulesEngineStorage().rulesEngineOverride; + } + + /*/////////////////////////////////////////////////////////////// + External functions + //////////////////////////////////////////////////////////////*/ + + function createRuleMultiplicative(RuleTypeMultiplicative memory rule) external returns (bytes32 ruleId) { + require(_canSetRules(), "RulesEngine: cannot set rules"); + + ruleId = keccak256( + abi.encodePacked(rule.token, rule.tokenType, rule.tokenId, rule.scorePerOwnedToken, RuleType.Multiplicative) + ); + _createRule( + RuleWithId( + ruleId, + rule.token, + rule.tokenType, + rule.tokenId, + 0, // balance + rule.scorePerOwnedToken, + RuleType.Multiplicative + ) + ); + } + + function createRuleThreshold(RuleTypeThreshold memory rule) external returns (bytes32 ruleId) { + require(_canSetRules(), "RulesEngine: cannot set rules"); + + ruleId = keccak256( + abi.encodePacked(rule.token, rule.tokenType, rule.tokenId, rule.balance, rule.score, RuleType.Threshold) + ); + _createRule( + RuleWithId(ruleId, rule.token, rule.tokenType, rule.tokenId, rule.balance, rule.score, RuleType.Threshold) + ); + } + + function deleteRule(bytes32 _ruleId) external { + require(_canSetRules(), "RulesEngine: cannot set rules"); + _deleteRule(_ruleId); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + function _getScoreForRule(address _tokenOwner, RuleWithId memory _rule) internal view returns (uint256 score) { + uint256 balance = 0; + + if (_rule.tokenType == TokenType.ERC20) { + // NOTE: We are rounding down the ERC20 balance to the nearest full unit. + uint256 unit = 10 ** IERC20Metadata(_rule.token).decimals(); + balance = IERC20(_rule.token).balanceOf(_tokenOwner) / unit; + } else if (_rule.tokenType == TokenType.ERC721) { + balance = IERC721(_rule.token).balanceOf(_tokenOwner); + } else if (_rule.tokenType == TokenType.ERC1155) { + balance = IERC1155(_rule.token).balanceOf(_tokenOwner, _rule.tokenId); + } + + if (_rule.ruleType == RuleType.Threshold) { + if (balance >= _rule.balance) { + score = _rule.score; + } + } else if (_rule.ruleType == RuleType.Multiplicative) { + score = balance * _rule.score; + } + } + + function _createRule(RuleWithId memory _rule) internal { + require(_rulesEngineStorage().ids.add(_rule.ruleId), "RulesEngine: rule already exists"); + _rulesEngineStorage().rules[_rule.ruleId] = _rule; + emit RuleCreated(_rule.ruleId, _rule); + } + + function _deleteRule(bytes32 _ruleId) internal { + require(_rulesEngineStorage().ids.remove(_ruleId), "RulesEngine: rule already exists"); + delete _rulesEngineStorage().rules[_ruleId]; + emit RuleDeleted(_ruleId); + } + + function setRulesEngineOverride(address _rulesEngineAddress) external { + require(_canOverrideRulesEngine(), "RulesEngine: cannot override rules engine"); + _rulesEngineStorage().rulesEngineOverride = _rulesEngineAddress; + + emit RulesEngineOverriden(_rulesEngineAddress); + } + + function _rulesEngineStorage() internal pure returns (RulesEngineStorage.Data storage data) { + data = RulesEngineStorage.data(); + } + + function _canSetRules() internal view virtual returns (bool); + + function _canOverrideRulesEngine() internal view virtual returns (bool); +} diff --git a/contracts/extension/upgradeable/SharedMetadataBatch.sol b/contracts/extension/upgradeable/SharedMetadataBatch.sol new file mode 100644 index 000000000..c4001864d --- /dev/null +++ b/contracts/extension/upgradeable/SharedMetadataBatch.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +import "../../lib/NFTMetadataRenderer.sol"; +import "../interface/ISharedMetadataBatch.sol"; +import "../../external-deps/openzeppelin/utils/EnumerableSet.sol"; + +/** + * @title Shared Metadata Batch + * @notice Store a batch of shared metadata for NFTs + */ +library SharedMetadataBatchStorage { + /// @custom:storage-location erc7201:shared.metadata.batch.storage + /// @dev keccak256(abi.encode(uint256(keccak256("shared.metadata.batch.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant SHARED_METADATA_BATCH_STORAGE_POSITION = + 0xf85ae2b98503142dac20c6561e88360cff7f1cb5634b6ad090b7f724e2f67a00; + + struct Data { + EnumerableSet.Bytes32Set ids; + mapping(bytes32 => ISharedMetadataBatch.SharedMetadataWithId) metadata; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = SHARED_METADATA_BATCH_STORAGE_POSITION; + assembly { + data_.slot := position + } + } +} + +abstract contract SharedMetadataBatch is ISharedMetadataBatch { + using EnumerableSet for EnumerableSet.Bytes32Set; + + /// @notice Set shared metadata for NFTs + function setSharedMetadata(SharedMetadataInfo calldata metadata, bytes32 _id) external { + require(_canSetSharedMetadata(), "SharedMetadataBatch: cannot set shared metadata"); + _createSharedMetadata(metadata, _id); + } + + /// @notice Delete shared metadata for NFTs + function deleteSharedMetadata(bytes32 _id) external { + require(_canSetSharedMetadata(), "SharedMetadataBatch: cannot set shared metadata"); + require(_sharedMetadataBatchStorage().ids.remove(_id), "SharedMetadataBatch: shared metadata does not exist"); + + delete _sharedMetadataBatchStorage().metadata[_id]; + + emit SharedMetadataDeleted(_id); + } + + /// @notice Get all shared metadata + function getAllSharedMetadata() external view returns (SharedMetadataWithId[] memory metadata) { + bytes32[] memory ids = _sharedMetadataBatchStorage().ids.values(); + metadata = new SharedMetadataWithId[](ids.length); + + for (uint256 i = 0; i < ids.length; i += 1) { + metadata[i] = _sharedMetadataBatchStorage().metadata[ids[i]]; + } + } + + /// @dev Store shared metadata + function _createSharedMetadata(SharedMetadataInfo calldata _metadata, bytes32 _id) internal { + require(_sharedMetadataBatchStorage().ids.add(_id), "SharedMetadataBatch: shared metadata already exists"); + + _sharedMetadataBatchStorage().metadata[_id] = SharedMetadataWithId(_id, _metadata); + + emit SharedMetadataUpdated( + _id, + _metadata.name, + _metadata.description, + _metadata.imageURI, + _metadata.animationURI + ); + } + + /// @dev Token URI information getter + function _getURIFromSharedMetadata(bytes32 id, uint256 tokenId) internal view returns (string memory) { + SharedMetadataInfo memory info = _sharedMetadataBatchStorage().metadata[id].metadata; + + return + NFTMetadataRenderer.createMetadataEdition({ + name: info.name, + description: info.description, + imageURI: info.imageURI, + animationURI: info.animationURI, + tokenOfEdition: tokenId + }); + } + + /// @dev Get contract storage + function _sharedMetadataBatchStorage() internal pure returns (SharedMetadataBatchStorage.Data storage data) { + data = SharedMetadataBatchStorage.data(); + } + + /// @dev Returns whether shared metadata can be set in the given execution context. + function _canSetSharedMetadata() internal view virtual returns (bool); +} diff --git a/contracts/extension/upgradeable/impl/ContractMetadataImpl.sol b/contracts/extension/upgradeable/impl/ContractMetadataImpl.sol new file mode 100644 index 000000000..777cd9202 --- /dev/null +++ b/contracts/extension/upgradeable/impl/ContractMetadataImpl.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../ContractMetadata.sol"; + +import "../../interface/IPermissions.sol"; +import "../../interface/IERC2771Context.sol"; + +contract ContractMetadataImpl is ContractMetadata { + bytes32 private constant DEFAULT_ADMIN_ROLE = 0x00; + + function _canSetContractURI() internal view override returns (bool) { + return IPermissions(address(this)).hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + function _msgSender() internal view returns (address sender) { + if (IERC2771Context(address(this)).isTrustedForwarder(msg.sender)) { + // The assembly code is more direct than the Solidity version using `abi.decode`. + assembly { + sender := shr(96, calldataload(sub(calldatasize(), 20))) + } + } else { + return msg.sender; + } + } + + function _msgData() internal view returns (bytes calldata) { + if (IERC2771Context(address(this)).isTrustedForwarder(msg.sender)) { + return msg.data[:msg.data.length - 20]; + } else { + return msg.data; + } + } +} diff --git a/contracts/extension/upgradeable/impl/MetaTx.sol b/contracts/extension/upgradeable/impl/MetaTx.sol new file mode 100644 index 000000000..0904eef78 --- /dev/null +++ b/contracts/extension/upgradeable/impl/MetaTx.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../ERC2771Context.sol"; + +contract MetaTx is ERC2771Context { + constructor(address[] memory trustedForwarder) ERC2771Context(trustedForwarder) {} +} diff --git a/contracts/extension/upgradeable/impl/PermissionsEnumerableImpl.sol b/contracts/extension/upgradeable/impl/PermissionsEnumerableImpl.sol new file mode 100644 index 000000000..4216d2f01 --- /dev/null +++ b/contracts/extension/upgradeable/impl/PermissionsEnumerableImpl.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../../interface/IERC2771Context.sol"; +import "../PermissionsEnumerable.sol"; + +contract PermissionsEnumerableImpl is PermissionsEnumerable { + function _msgSender() internal view override returns (address sender) { + if (IERC2771Context(address(this)).isTrustedForwarder(msg.sender)) { + // The assembly code is more direct than the Solidity version using `abi.decode`. + assembly { + sender := shr(96, calldataload(sub(calldatasize(), 20))) + } + } else { + return msg.sender; + } + } + + function _msgData() internal view override returns (bytes calldata) { + if (IERC2771Context(address(this)).isTrustedForwarder(msg.sender)) { + return msg.data[:msg.data.length - 20]; + } else { + return msg.data; + } + } +} diff --git a/contracts/extension/upgradeable/impl/PlatformFeeImpl.sol b/contracts/extension/upgradeable/impl/PlatformFeeImpl.sol new file mode 100644 index 000000000..8010d89ed --- /dev/null +++ b/contracts/extension/upgradeable/impl/PlatformFeeImpl.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../PlatformFee.sol"; + +import "../../interface/IPermissions.sol"; +import "../../interface/IERC2771Context.sol"; + +contract PlatformFeeImpl is PlatformFee { + bytes32 private constant DEFAULT_ADMIN_ROLE = 0x00; + + function _canSetPlatformFeeInfo() internal view override returns (bool) { + return IPermissions(address(this)).hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + function _msgSender() internal view returns (address sender) { + if (IERC2771Context(address(this)).isTrustedForwarder(msg.sender)) { + // The assembly code is more direct than the Solidity version using `abi.decode`. + assembly { + sender := shr(96, calldataload(sub(calldatasize(), 20))) + } + } else { + return msg.sender; + } + } + + function _msgData() internal view returns (bytes calldata) { + if (IERC2771Context(address(this)).isTrustedForwarder(msg.sender)) { + return msg.data[:msg.data.length - 20]; + } else { + return msg.data; + } + } +} diff --git a/contracts/extension/upgradeable/init/ContractMetadataInit.sol b/contracts/extension/upgradeable/init/ContractMetadataInit.sol new file mode 100644 index 000000000..c61ad085f --- /dev/null +++ b/contracts/extension/upgradeable/init/ContractMetadataInit.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { ContractMetadataStorage } from "../ContractMetadata.sol"; + +contract ContractMetadataInit { + event ContractURIUpdated(string prevURI, string newURI); + + /// @dev Lets a contract admin set the URI for contract-level metadata. + function _setupContractURI(string memory _uri) internal { + ContractMetadataStorage.Data storage data = ContractMetadataStorage.data(); + string memory prevURI = data.contractURI; + data.contractURI = _uri; + + emit ContractURIUpdated(prevURI, _uri); + } +} diff --git a/contracts/extension/upgradeable/init/ERC2771ContextInit.sol b/contracts/extension/upgradeable/init/ERC2771ContextInit.sol new file mode 100644 index 000000000..eef0d3e5a --- /dev/null +++ b/contracts/extension/upgradeable/init/ERC2771ContextInit.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { ERC2771ContextStorage } from "../ERC2771Context.sol"; +import "../Initializable.sol"; + +contract ERC2771ContextInit is Initializable { + function __ERC2771Context_init(address[] memory trustedForwarder) internal onlyInitializing { + __ERC2771Context_init_unchained(trustedForwarder); + } + + function __ERC2771Context_init_unchained(address[] memory trustedForwarder) internal onlyInitializing { + ERC2771ContextStorage.Data storage data = ERC2771ContextStorage.data(); + + for (uint256 i = 0; i < trustedForwarder.length; i++) { + data.trustedForwarder[trustedForwarder[i]] = true; + } + } +} diff --git a/contracts/extension/upgradeable/init/ERC721AInit.sol b/contracts/extension/upgradeable/init/ERC721AInit.sol new file mode 100644 index 000000000..17cc7e48f --- /dev/null +++ b/contracts/extension/upgradeable/init/ERC721AInit.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { ERC721AStorage } from "../../../eip/ERC721AUpgradeable.sol"; +import "../Initializable.sol"; + +contract ERC721AInit is Initializable { + function __ERC721A_init(string memory name_, string memory symbol_) internal onlyInitializing { + __ERC721A_init_unchained(name_, symbol_); + } + + function __ERC721A_init_unchained(string memory name_, string memory symbol_) internal onlyInitializing { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + + data._name = name_; + data._symbol = symbol_; + data._currentIndex = _startTokenId(); + } + + function _startTokenId() internal view virtual returns (uint256) { + return 0; + } +} diff --git a/contracts/extension/upgradeable/init/ERC721AQueryableInit.sol b/contracts/extension/upgradeable/init/ERC721AQueryableInit.sol new file mode 100644 index 000000000..eb5da0987 --- /dev/null +++ b/contracts/extension/upgradeable/init/ERC721AQueryableInit.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../../eip/queryable/ERC721AStorage.sol"; +import "../../../eip/queryable/ERC721A__Initializable.sol"; + +contract ERC721AQueryableInit is ERC721A__Initializable { + function __ERC721A_init(string memory name_, string memory symbol_) internal onlyInitializingERC721A { + __ERC721A_init_unchained(name_, symbol_); + } + + function __ERC721A_init_unchained(string memory name_, string memory symbol_) internal onlyInitializingERC721A { + ERC721AStorage.layout()._name = name_; + ERC721AStorage.layout()._symbol = symbol_; + ERC721AStorage.layout()._currentIndex = _startTokenId(); + } + + function _startTokenId() internal view virtual returns (uint256) { + return 0; + } +} diff --git a/contracts/extension/upgradeable/init/OwnableInit.sol b/contracts/extension/upgradeable/init/OwnableInit.sol new file mode 100644 index 000000000..5e6e8c2f1 --- /dev/null +++ b/contracts/extension/upgradeable/init/OwnableInit.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { OwnableStorage } from "../Ownable.sol"; + +contract OwnableInit { + event OwnerUpdated(address indexed prevOwner, address indexed newOwner); + + /// @dev Lets a contract admin set a new owner for the contract. The new owner must be a contract admin. + function _setupOwner(address _newOwner) internal { + OwnableStorage.Data storage data = OwnableStorage.data(); + + address _prevOwner = data._owner; + data._owner = _newOwner; + + emit OwnerUpdated(_prevOwner, _newOwner); + } +} diff --git a/contracts/extension/upgradeable/init/PermissionsEnumerableInit.sol b/contracts/extension/upgradeable/init/PermissionsEnumerableInit.sol new file mode 100644 index 000000000..4b00abd2b --- /dev/null +++ b/contracts/extension/upgradeable/init/PermissionsEnumerableInit.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { PermissionsEnumerableStorage } from "../PermissionsEnumerable.sol"; +import "./PermissionsInit.sol"; + +contract PermissionsEnumerableInit is PermissionsInit { + /// @dev Sets up `role` for `account` + function _setupRole(bytes32 role, address account) internal override { + super._setupRole(role, account); + _addMember(role, account); + } + + /// @dev adds `account` to {roleMembers}, for `role` + function _addMember(bytes32 role, address account) internal { + PermissionsEnumerableStorage.Data storage data = PermissionsEnumerableStorage.data(); + uint256 idx = data.roleMembers[role].index; + data.roleMembers[role].index += 1; + + data.roleMembers[role].members[idx] = account; + data.roleMembers[role].indexOf[account] = idx; + } +} diff --git a/contracts/extension/upgradeable/init/PermissionsInit.sol b/contracts/extension/upgradeable/init/PermissionsInit.sol new file mode 100644 index 000000000..549b6661e --- /dev/null +++ b/contracts/extension/upgradeable/init/PermissionsInit.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { PermissionsStorage } from "../Permissions.sol"; + +contract PermissionsInit { + event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole); + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + + /// @dev Default admin role for all roles. Only accounts with this role can grant/revoke other roles. + bytes32 internal constant DEFAULT_ADMIN_ROLE = 0x00; + + /// @dev Sets up `role` for `account` + function _setupRole(bytes32 role, address account) internal virtual { + PermissionsStorage.Data storage data = PermissionsStorage.data(); + data._hasRole[role][account] = true; + emit RoleGranted(role, account, msg.sender); + } + + /// @dev Sets `adminRole` as `role`'s admin role. + function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual { + PermissionsStorage.Data storage data = PermissionsStorage.data(); + bytes32 previousAdminRole = data._getRoleAdmin[role]; + data._getRoleAdmin[role] = adminRole; + emit RoleAdminChanged(role, previousAdminRole, adminRole); + } +} diff --git a/contracts/extension/upgradeable/init/PlatformFeeInit.sol b/contracts/extension/upgradeable/init/PlatformFeeInit.sol new file mode 100644 index 000000000..92f390b1c --- /dev/null +++ b/contracts/extension/upgradeable/init/PlatformFeeInit.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { PlatformFeeStorage } from "../PlatformFee.sol"; + +contract PlatformFeeInit { + event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps); + + /// @dev Lets a contract admin update the platform fee recipient and bps + function _setupPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) internal { + if (_platformFeeBps > 10_000) { + revert("Exceeds max bps"); + } + if (_platformFeeRecipient == address(0)) { + revert("Invalid recipient"); + } + + PlatformFeeStorage.Data storage data = PlatformFeeStorage.data(); + + data.platformFeeBps = uint16(_platformFeeBps); + data.platformFeeRecipient = _platformFeeRecipient; + + emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); + } +} diff --git a/contracts/extension/upgradeable/init/PrimarySaleInit.sol b/contracts/extension/upgradeable/init/PrimarySaleInit.sol new file mode 100644 index 000000000..edd7862c6 --- /dev/null +++ b/contracts/extension/upgradeable/init/PrimarySaleInit.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { PrimarySaleStorage } from "../PrimarySale.sol"; + +contract PrimarySaleInit { + /// @dev Emitted when a new sale recipient is set. + event PrimarySaleRecipientUpdated(address indexed recipient); + + /// @dev Lets a contract admin set the recipient for all primary sales. + function _setupPrimarySaleRecipient(address _saleRecipient) internal { + if (_saleRecipient == address(0)) { + revert("Invalid recipient"); + } + PrimarySaleStorage.Data storage data = PrimarySaleStorage.data(); + data.recipient = _saleRecipient; + emit PrimarySaleRecipientUpdated(_saleRecipient); + } +} diff --git a/contracts/extension/upgradeable/init/ReentrancyGuardInit.sol b/contracts/extension/upgradeable/init/ReentrancyGuardInit.sol new file mode 100644 index 000000000..b8bac13e1 --- /dev/null +++ b/contracts/extension/upgradeable/init/ReentrancyGuardInit.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { ReentrancyGuardStorage } from "../ReentrancyGuard.sol"; +import "../Initializable.sol"; + +contract ReentrancyGuardInit is Initializable { + uint256 private constant _NOT_ENTERED = 1; + + function __ReentrancyGuard_init() internal onlyInitializing { + __ReentrancyGuard_init_unchained(); + } + + function __ReentrancyGuard_init_unchained() internal onlyInitializing { + ReentrancyGuardStorage.Data storage data = ReentrancyGuardStorage.data(); + data._status = _NOT_ENTERED; + } +} diff --git a/contracts/extension/upgradeable/init/RoyaltyInit.sol b/contracts/extension/upgradeable/init/RoyaltyInit.sol new file mode 100644 index 000000000..70e638796 --- /dev/null +++ b/contracts/extension/upgradeable/init/RoyaltyInit.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { RoyaltyStorage, IRoyalty } from "../Royalty.sol"; + +contract RoyaltyInit { + event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps); + + /// @dev Lets a contract admin update the default royalty recipient and bps. + function _setupDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) internal { + if (_royaltyBps > 10_000) { + revert("Exceeds max bps"); + } + + RoyaltyStorage.Data storage data = RoyaltyStorage.data(); + + data.royaltyRecipient = _royaltyRecipient; + data.royaltyBps = uint16(_royaltyBps); + + emit DefaultRoyalty(_royaltyRecipient, _royaltyBps); + } +} diff --git a/contracts/external-deps/chainlink/LinkTokenInterface.sol b/contracts/external-deps/chainlink/LinkTokenInterface.sol new file mode 100644 index 000000000..203f8684c --- /dev/null +++ b/contracts/external-deps/chainlink/LinkTokenInterface.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface LinkTokenInterface { + function allowance(address owner, address spender) external view returns (uint256 remaining); + + function approve(address spender, uint256 value) external returns (bool success); + + function balanceOf(address owner) external view returns (uint256 balance); + + function decimals() external view returns (uint8 decimalPlaces); + + function decreaseApproval(address spender, uint256 addedValue) external returns (bool success); + + function increaseApproval(address spender, uint256 subtractedValue) external; + + function name() external view returns (string memory tokenName); + + function symbol() external view returns (string memory tokenSymbol); + + function totalSupply() external view returns (uint256 totalTokensIssued); + + function transfer(address to, uint256 value) external returns (bool success); + + function transferAndCall(address to, uint256 value, bytes calldata data) external returns (bool success); + + function transferFrom(address from, address to, uint256 value) external returns (bool success); +} diff --git a/contracts/external-deps/chainlink/VRFV2WrapperConsumerBase.sol b/contracts/external-deps/chainlink/VRFV2WrapperConsumerBase.sol new file mode 100644 index 000000000..48a62ee60 --- /dev/null +++ b/contracts/external-deps/chainlink/VRFV2WrapperConsumerBase.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./LinkTokenInterface.sol"; +import "./VRFV2WrapperInterface.sol"; + +/** ******************************************************************************* + * @notice Interface for contracts using VRF randomness through the VRF V2 wrapper + * ******************************************************************************** + * @dev PURPOSE + * + * @dev Create VRF V2 requests without the need for subscription management. Rather than creating + * @dev and funding a VRF V2 subscription, a user can use this wrapper to create one off requests, + * @dev paying up front rather than at fulfillment. + * + * @dev Since the price is determined using the gas price of the request transaction rather than + * @dev the fulfillment transaction, the wrapper charges an additional premium on callback gas + * @dev usage, in addition to some extra overhead costs associated with the VRFV2Wrapper contract. + * ***************************************************************************** + * @dev USAGE + * + * @dev Calling contracts must inherit from VRFV2WrapperConsumerBase. The consumer must be funded + * @dev with enough LINK to make the request, otherwise requests will revert. To request randomness, + * @dev call the 'requestRandomness' function with the desired VRF parameters. This function handles + * @dev paying for the request based on the current pricing. + * + * @dev Consumers must implement the fullfillRandomWords function, which will be called during + * @dev fulfillment with the randomness result. + */ +abstract contract VRFV2WrapperConsumerBase { + // solhint-disable-next-line var-name-mixedcase + LinkTokenInterface internal immutable LINK; + // solhint-disable-next-line var-name-mixedcase + VRFV2WrapperInterface internal immutable VRF_V2_WRAPPER; + + /** + * @param _link is the address of LinkToken + * @param _vrfV2Wrapper is the address of the VRFV2Wrapper contract + */ + constructor(address _link, address _vrfV2Wrapper) { + LINK = LinkTokenInterface(_link); + VRF_V2_WRAPPER = VRFV2WrapperInterface(_vrfV2Wrapper); + } + + /** + * @dev Requests randomness from the VRF V2 wrapper. + * + * @param _callbackGasLimit is the gas limit that should be used when calling the consumer's + * fulfillRandomWords function. + * @param _requestConfirmations is the number of confirmations to wait before fulfilling the + * request. A higher number of confirmations increases security by reducing the likelihood + * that a chain re-org changes a published randomness outcome. + * @param _numWords is the number of random words to request. + * + * @return requestId is the VRF V2 request ID of the newly created randomness request. + */ + function requestRandomness( + uint32 _callbackGasLimit, + uint16 _requestConfirmations, + uint32 _numWords + ) internal returns (uint256 requestId) { + LINK.transferAndCall( + address(VRF_V2_WRAPPER), + VRF_V2_WRAPPER.calculateRequestPrice(_callbackGasLimit), + abi.encode(_callbackGasLimit, _requestConfirmations, _numWords) + ); + return VRF_V2_WRAPPER.lastRequestId(); + } + + /** + * @notice fulfillRandomWords handles the VRF V2 wrapper response. The consuming contract must + * @notice implement it. + * + * @param _requestId is the VRF V2 request ID. + * @param _randomWords is the randomness result. + */ + function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) internal virtual; + + function rawFulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) external { + require(msg.sender == address(VRF_V2_WRAPPER), "only VRF V2 wrapper can fulfill"); + fulfillRandomWords(_requestId, _randomWords); + } +} diff --git a/contracts/external-deps/chainlink/VRFV2WrapperInterface.sol b/contracts/external-deps/chainlink/VRFV2WrapperInterface.sol new file mode 100644 index 000000000..b636940bb --- /dev/null +++ b/contracts/external-deps/chainlink/VRFV2WrapperInterface.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface VRFV2WrapperInterface { + /** + * @return the request ID of the most recent VRF V2 request made by this wrapper. This should only + * be relied option within the same transaction that the request was made. + */ + function lastRequestId() external view returns (uint256); + + /** + * @notice Calculates the price of a VRF request with the given callbackGasLimit at the current + * @notice block. + * + * @dev This function relies on the transaction gas price which is not automatically set during + * @dev simulation. To estimate the price at a specific gas price, use the estimatePrice function. + * + * @param _callbackGasLimit is the gas limit used to estimate the price. + */ + function calculateRequestPrice(uint32 _callbackGasLimit) external view returns (uint256); + + /** + * @notice Estimates the price of a VRF request with a specific gas limit and gas price. + * + * @dev This is a convenience function that can be called in simulation to better understand + * @dev pricing. + * + * @param _callbackGasLimit is the gas limit used to estimate the price. + * @param _requestGasPriceWei is the gas price in wei used for the estimation. + */ + function estimateRequestPrice( + uint32 _callbackGasLimit, + uint256 _requestGasPriceWei + ) external view returns (uint256); +} diff --git a/contracts/openzeppelin-presets/ERC1155PresetUpgradeable.sol b/contracts/external-deps/openzeppelin/ERC1155PresetUpgradeable.sol similarity index 92% rename from contracts/openzeppelin-presets/ERC1155PresetUpgradeable.sol rename to contracts/external-deps/openzeppelin/ERC1155PresetUpgradeable.sol index 736cd63db..e3163c3e2 100644 --- a/contracts/openzeppelin-presets/ERC1155PresetUpgradeable.sol +++ b/contracts/external-deps/openzeppelin/ERC1155PresetUpgradeable.sol @@ -45,7 +45,7 @@ contract ERC1155PresetUpgradeable is mapping(uint256 => uint256) private _totalSupply; - /// @dev Initiliazes the contract, like a constructor. + /// @dev Initializes the contract, like a constructor. function __ERC1155Preset_init(address _deployer, string memory uri) internal onlyInitializing { // Initialize inherited contracts, most base-like -> most derived. __ERC1155_init(uri); @@ -74,12 +74,7 @@ contract ERC1155PresetUpgradeable is * * - the caller must have the `MINTER_ROLE`. */ - function mint( - address to, - uint256 id, - uint256 amount, - bytes memory data - ) public virtual { + function mint(address to, uint256 id, uint256 amount, bytes memory data) public virtual { require(hasRole(MINTER_ROLE, _msgSender()), "must have minter role"); _mint(to, id, amount, data); @@ -88,12 +83,7 @@ contract ERC1155PresetUpgradeable is /** * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] variant of {mint}. */ - function mintBatch( - address to, - uint256[] memory ids, - uint256[] memory amounts, - bytes memory data - ) public virtual { + function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) public virtual { require(hasRole(MINTER_ROLE, _msgSender()), "must have minter role"); _mintBatch(to, ids, amounts, data); @@ -130,7 +120,9 @@ contract ERC1155PresetUpgradeable is /** * @dev See {IERC165-supportsInterface}. */ - function supportsInterface(bytes4 interfaceId) + function supportsInterface( + bytes4 interfaceId + ) public view virtual diff --git a/contracts/external-deps/openzeppelin/cryptography/EIP712ChainlessDomain.sol b/contracts/external-deps/openzeppelin/cryptography/EIP712ChainlessDomain.sol new file mode 100644 index 000000000..f5be7dd7c --- /dev/null +++ b/contracts/external-deps/openzeppelin/cryptography/EIP712ChainlessDomain.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (utils/cryptography/draft-EIP712.sol) + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +/** + * @dev https://eips.ethereum.org/EIPS/eip-712[EIP 712] is a standard for hashing and signing of typed structured data. + * + * The encoding specified in the EIP is very generic, and such a generic implementation in Solidity is not feasible, + * thus this contract does not implement the encoding itself. Protocols need to implement the type-specific encoding + * they need in their contracts using a combination of `abi.encode` and `keccak256`. + * + * This contract implements the EIP 712 domain separator ({_domainSeparatorV4}) that is used as part of the encoding + * scheme, and the final step of the encoding to obtain the message digest that is then signed via ECDSA + * ({_hashTypedDataV4}). + * + * The implementation of the domain separator was designed to be as efficient as possible while still properly updating + * the chain id to protect against replay attacks on an eventual fork of the chain. + * + * NOTE: This contract implements the version of the encoding known as "v4", as implemented by the JSON RPC method + * https://docs.metamask.io/guide/signing-data.html[`eth_signTypedDataV4` in MetaMask]. + * + * _Available since v3.4._ + */ +abstract contract EIP712ChainlessDomain { + /* solhint-disable var-name-mixedcase */ + // Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to + // invalidate the cached domain separator if the chain id changes. + bytes32 private immutable _CACHED_DOMAIN_SEPARATOR; + address private immutable _CACHED_THIS; + + bytes32 private immutable _HASHED_NAME; + bytes32 private immutable _HASHED_VERSION; + bytes32 private immutable _TYPE_HASH; + + /* solhint-enable var-name-mixedcase */ + + /** + * @dev Initializes the domain separator and parameter caches. + * + * The meaning of `name` and `version` is specified in + * https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator[EIP 712]: + * + * - `name`: the user readable name of the signing domain, i.e. the name of the DApp or the protocol. + * - `version`: the current major version of the signing domain. + * + * NOTE: These parameters cannot be changed except through a xref:learn::upgrading-smart-contracts.adoc[smart + * contract upgrade]. + */ + constructor(string memory name, string memory version) { + bytes32 hashedName = keccak256(bytes(name)); + bytes32 hashedVersion = keccak256(bytes(version)); + bytes32 typeHash = keccak256("EIP712Domain(string name,string version,address verifyingContract)"); + _HASHED_NAME = hashedName; + _HASHED_VERSION = hashedVersion; + _CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(typeHash, hashedName, hashedVersion); + _CACHED_THIS = address(this); + _TYPE_HASH = typeHash; + } + + /** + * @dev Returns the domain separator for the current chain. + */ + function _domainSeparatorV4() internal view returns (bytes32) { + if (address(this) == _CACHED_THIS) { + return _CACHED_DOMAIN_SEPARATOR; + } else { + return _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME, _HASHED_VERSION); + } + } + + function _buildDomainSeparator( + bytes32 typeHash, + bytes32 nameHash, + bytes32 versionHash + ) private view returns (bytes32) { + return keccak256(abi.encode(typeHash, nameHash, versionHash, address(this))); + } + + /** + * @dev Given an already https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct[hashed struct], this + * function returns the hash of the fully encoded EIP712 message for this domain. + * + * This hash can be used together with {ECDSA-recover} to obtain the signer of a message. For example: + * + * ```solidity + * bytes32 digest = _hashTypedDataV4(keccak256(abi.encode( + * keccak256("Mail(address to,string contents)"), + * mailTo, + * keccak256(bytes(mailContents)) + * ))); + * address signer = ECDSA.recover(digest, signature); + * ``` + */ + function _hashTypedDataV4(bytes32 structHash) internal view virtual returns (bytes32) { + return ECDSA.toTypedDataHash(_domainSeparatorV4(), structHash); + } +} diff --git a/contracts/openzeppelin-presets/finance/PaymentSplitterUpgradeable.sol b/contracts/external-deps/openzeppelin/finance/PaymentSplitterUpgradeable.sol similarity index 89% rename from contracts/openzeppelin-presets/finance/PaymentSplitterUpgradeable.sol rename to contracts/external-deps/openzeppelin/finance/PaymentSplitterUpgradeable.sol index 4608c4eb1..c60b0048d 100644 --- a/contracts/openzeppelin-presets/finance/PaymentSplitterUpgradeable.sol +++ b/contracts/external-deps/openzeppelin/finance/PaymentSplitterUpgradeable.sol @@ -14,6 +14,7 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; * - `_totalReleased`, `_released`, `_erc20TotalReleased`, `_erc20Released`, `_pendingPayment` * * - Add `payeeCount`: returns the length of `_payees` + * - Add `releasable` functions as per recent updates in OZ PaymentSplitterUpgradeable (v4.7.0) */ /** @@ -61,10 +62,10 @@ contract PaymentSplitterUpgradeable is Initializable, ContextUpgradeable { __PaymentSplitter_init_unchained(payees, shares_); } - function __PaymentSplitter_init_unchained(address[] memory payees, uint256[] memory shares_) - internal - onlyInitializing - { + function __PaymentSplitter_init_unchained( + address[] memory payees, + uint256[] memory shares_ + ) internal onlyInitializing { require(payees.length == shares_.length, "PaymentSplitter: payees and shares length mismatch"); require(payees.length > 0, "PaymentSplitter: no payees"); @@ -144,6 +145,23 @@ contract PaymentSplitterUpgradeable is Initializable, ContextUpgradeable { return _payees.length; } + /** + * @dev Getter for the amount of payee's releasable Ether. + */ + function releasable(address account) public view returns (uint256) { + uint256 totalReceived = address(this).balance + totalReleased(); + return _pendingPayment(account, totalReceived, released(account)); + } + + /** + * @dev Getter for the amount of payee's releasable `token` tokens. `token` should be the address of an + * IERC20 contract. + */ + function releasable(IERC20Upgradeable token, address account) public view returns (uint256) { + uint256 totalReceived = token.balanceOf(address(this)) + totalReleased(token); + return _pendingPayment(account, totalReceived, released(token, account)); + } + /** * @dev Triggers a transfer to `account` of the amount of Ether they are owed, according to their percentage of the * total shares and their previous withdrawals. @@ -151,8 +169,7 @@ contract PaymentSplitterUpgradeable is Initializable, ContextUpgradeable { function release(address payable account) public virtual { require(_shares[account] > 0, "PaymentSplitter: account has no shares"); - uint256 totalReceived = address(this).balance + totalReleased(); - uint256 payment = _pendingPayment(account, totalReceived, released(account)); + uint256 payment = releasable(account); require(payment != 0, "PaymentSplitter: account is not due payment"); @@ -171,8 +188,7 @@ contract PaymentSplitterUpgradeable is Initializable, ContextUpgradeable { function release(IERC20Upgradeable token, address account) public virtual { require(_shares[account] > 0, "PaymentSplitter: account has no shares"); - uint256 totalReceived = token.balanceOf(address(this)) + totalReleased(token); - uint256 payment = _pendingPayment(account, totalReceived, released(token, account)); + uint256 payment = releasable(token, account); require(payment != 0, "PaymentSplitter: account is not due payment"); diff --git a/contracts/external-deps/openzeppelin/governance/utils/IVotes.sol b/contracts/external-deps/openzeppelin/governance/utils/IVotes.sol new file mode 100644 index 000000000..0bef3f920 --- /dev/null +++ b/contracts/external-deps/openzeppelin/governance/utils/IVotes.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.5.0) (governance/utils/IVotes.sol) +pragma solidity ^0.8.0; + +/** + * @dev Common interface for {ERC20Votes}, {ERC721Votes}, and other {Votes}-enabled contracts. + * + * _Available since v4.5._ + */ +interface IVotes { + /** + * @dev Emitted when an account changes their delegate. + */ + event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); + + /** + * @dev Emitted when a token transfer or delegate change results in changes to a delegate's number of votes. + */ + event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance); + + /** + * @dev Returns the current amount of votes that `account` has. + */ + function getVotes(address account) external view returns (uint256); + + /** + * @dev Returns the amount of votes that `account` had at the end of a past block (`blockNumber`). + */ + function getPastVotes(address account, uint256 blockNumber) external view returns (uint256); + + /** + * @dev Returns the total supply of votes available at the end of a past block (`blockNumber`). + * + * NOTE: This value is the sum of all available votes, which is not necessarily the sum of all delegated votes. + * Votes that have not been delegated are still part of total supply, even though they would not participate in a + * vote. + */ + function getPastTotalSupply(uint256 blockNumber) external view returns (uint256); + + /** + * @dev Returns the delegate that `account` has chosen. + */ + function delegates(address account) external view returns (address); + + /** + * @dev Delegates votes from the sender to `delegatee`. + */ + function delegate(address delegatee) external; + + /** + * @dev Delegates votes from signer to `delegatee`. + */ + function delegateBySig(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) external; +} diff --git a/contracts/openzeppelin-presets/metatx/ERC2771Context.sol b/contracts/external-deps/openzeppelin/metatx/ERC2771Context.sol similarity index 100% rename from contracts/openzeppelin-presets/metatx/ERC2771Context.sol rename to contracts/external-deps/openzeppelin/metatx/ERC2771Context.sol diff --git a/contracts/openzeppelin-presets/metatx/ERC2771ContextUpgradeable.sol b/contracts/external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol similarity index 100% rename from contracts/openzeppelin-presets/metatx/ERC2771ContextUpgradeable.sol rename to contracts/external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol diff --git a/contracts/external-deps/openzeppelin/metatx/MinimalForwarderEOAOnly.sol b/contracts/external-deps/openzeppelin/metatx/MinimalForwarderEOAOnly.sol new file mode 100644 index 000000000..6a5c2ef61 --- /dev/null +++ b/contracts/external-deps/openzeppelin/metatx/MinimalForwarderEOAOnly.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.5.0) (metatx/MinimalForwarder.sol) + +pragma solidity ^0.8.0; + +import "../utils/cryptography/ECDSA.sol"; +import "../utils/cryptography/EIP712.sol"; + +/** + * @dev Simple minimal forwarder to be used together with an ERC2771 compatible contract. See {ERC2771Context}. + */ +contract MinimalForwarderEOAOnly is EIP712 { + using ECDSA for bytes32; + + struct ForwardRequest { + address from; + address to; + uint256 value; + uint256 gas; + uint256 nonce; + bytes data; + } + + bytes32 private constant _TYPEHASH = + keccak256("ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data)"); + + mapping(address => uint256) private _nonces; + + constructor() EIP712("GSNv2 Forwarder", "0.0.1") {} + + function getNonce(address from) public view returns (uint256) { + return _nonces[from]; + } + + function verify(ForwardRequest calldata req, bytes calldata signature) public view returns (bool) { + address signer = _hashTypedDataV4( + keccak256(abi.encode(_TYPEHASH, req.from, req.to, req.value, req.gas, req.nonce, keccak256(req.data))) + ).recover(signature); + return _nonces[req.from] == req.nonce && signer == req.from; + } + + function execute( + ForwardRequest calldata req, + bytes calldata signature + ) public payable returns (bool, bytes memory) { + require(msg.sender == tx.origin, "not EOA"); + require(verify(req, signature), "MinimalForwarder: signature does not match request"); + _nonces[req.from] = req.nonce + 1; + + (bool success, bytes memory returndata) = req.to.call{ gas: req.gas, value: req.value }( + abi.encodePacked(req.data, req.from) + ); + + // Validate that the relayer has sent enough gas for the call. + // See https://ronan.eth.link/blog/ethereum-gas-dangers/ + if (gasleft() <= req.gas / 63) { + // We explicitly trigger invalid opcode to consume all gas and bubble-up the effects, since + // neither revert or assert consume all gas since Solidity 0.8.0 + // https://docs.soliditylang.org/en/v0.8.0/control-structures.html#panic-via-assert-and-error-via-require + assembly { + invalid() + } + } + + return (success, returndata); + } +} diff --git a/contracts/external-deps/openzeppelin/proxy/Clones.sol b/contracts/external-deps/openzeppelin/proxy/Clones.sol new file mode 100644 index 000000000..712519892 --- /dev/null +++ b/contracts/external-deps/openzeppelin/proxy/Clones.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.8.0) (proxy/Clones.sol) + +pragma solidity ^0.8.0; + +/** + * @dev https://eips.ethereum.org/EIPS/eip-1167[EIP 1167] is a standard for + * deploying minimal proxy contracts, also known as "clones". + * + * > To simply and cheaply clone contract functionality in an immutable way, this standard specifies + * > a minimal bytecode implementation that delegates all calls to a known, fixed address. + * + * The library includes functions to deploy a proxy using either `create` (traditional deployment) or `create2` + * (salted deterministic deployment). It also includes functions to predict the addresses of clones deployed using the + * deterministic method. + * + * _Available since v3.4._ + */ +library Clones { + /** + * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`. + * + * This function uses the create opcode, which should never revert. + */ + function clone(address implementation) internal returns (address instance) { + /// @solidity memory-safe-assembly + assembly { + // Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes + // of the `implementation` address with the bytecode before the address. + mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000)) + // Packs the remaining 17 bytes of `implementation` with the bytecode after the address. + mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3)) + instance := create(0, 0x09, 0x37) + } + require(instance != address(0), "ERC1167: create failed"); + } + + /** + * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`. + * + * This function uses the create2 opcode and a `salt` to deterministically deploy + * the clone. Using the same `implementation` and `salt` multiple time will revert, since + * the clones cannot be deployed twice at the same address. + */ + function cloneDeterministic(address implementation, bytes32 salt) internal returns (address instance) { + /// @solidity memory-safe-assembly + assembly { + // Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes + // of the `implementation` address with the bytecode before the address. + mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000)) + // Packs the remaining 17 bytes of `implementation` with the bytecode after the address. + mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3)) + instance := create2(0, 0x09, 0x37, salt) + } + require(instance != address(0), "ERC1167: create2 failed"); + } + + /** + * @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}. + */ + function predictDeterministicAddress( + address implementation, + bytes32 salt, + address deployer + ) internal pure returns (address predicted) { + /// @solidity memory-safe-assembly + assembly { + let ptr := mload(0x40) + mstore(add(ptr, 0x38), deployer) + mstore(add(ptr, 0x24), 0x5af43d82803e903d91602b57fd5bf3ff) + mstore(add(ptr, 0x14), implementation) + mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73) + mstore(add(ptr, 0x58), salt) + mstore(add(ptr, 0x78), keccak256(add(ptr, 0x0c), 0x37)) + predicted := keccak256(add(ptr, 0x43), 0x55) + } + } + + /** + * @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}. + */ + function predictDeterministicAddress( + address implementation, + bytes32 salt + ) internal view returns (address predicted) { + return predictDeterministicAddress(implementation, salt, address(this)); + } +} diff --git a/contracts/external-deps/openzeppelin/proxy/ERC1967/ERC1967Proxy.sol b/contracts/external-deps/openzeppelin/proxy/ERC1967/ERC1967Proxy.sol new file mode 100644 index 000000000..a04d701ce --- /dev/null +++ b/contracts/external-deps/openzeppelin/proxy/ERC1967/ERC1967Proxy.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (proxy/ERC1967/ERC1967Proxy.sol) + +pragma solidity ^0.8.0; + +import "../Proxy.sol"; +import "./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 Initializes the upgradeable proxy with an initial implementation specified by `_logic`. + * + * If `_data` is nonempty, it's used as data in a delegate call to `_logic`. This will typically be an encoded + * function call, and allows initializing the storage of the proxy like a Solidity constructor. + */ + constructor(address _logic, bytes memory _data) payable { + _upgradeToAndCall(_logic, _data, false); + } + + /** + * @dev Returns the current implementation address. + */ + function _implementation() internal view virtual override returns (address impl) { + return ERC1967Upgrade._getImplementation(); + } +} diff --git a/contracts/external-deps/openzeppelin/proxy/ERC1967/ERC1967Upgrade.sol b/contracts/external-deps/openzeppelin/proxy/ERC1967/ERC1967Upgrade.sol new file mode 100644 index 000000000..d74e634a3 --- /dev/null +++ b/contracts/external-deps/openzeppelin/proxy/ERC1967/ERC1967Upgrade.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.5.0) (proxy/ERC1967/ERC1967Upgrade.sol) + +pragma solidity ^0.8.2; + +import "../beacon/IBeacon.sol"; +import "../IERC1822Proxiable.sol"; +import "../../../../lib/Address.sol"; +import "../../../../lib/StorageSlot.sol"; + +/** + * @dev This abstract contract provides getters and event emitting update functions for + * https://eips.ethereum.org/EIPS/eip-1967[EIP1967] slots. + * + * _Available since v4.1._ + * + * @custom:oz-upgrades-unsafe-allow delegatecall + */ +abstract contract ERC1967Upgrade { + // This is the keccak-256 hash of "eip1967.proxy.rollback" subtracted by 1 + bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143; + + /** + * @dev Storage slot with the address of the current implementation. + * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is + * validated in the constructor. + */ + bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + /** + * @dev Emitted when the implementation is upgraded. + */ + event Upgraded(address indexed implementation); + + /** + * @dev Returns the current implementation address. + */ + function _getImplementation() internal view returns (address) { + return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; + } + + /** + * @dev Stores a new address in the EIP1967 implementation slot. + */ + function _setImplementation(address newImplementation) private { + require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract"); + StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; + } + + /** + * @dev Perform implementation upgrade + * + * Emits an {Upgraded} event. + */ + function _upgradeTo(address newImplementation) internal { + _setImplementation(newImplementation); + emit Upgraded(newImplementation); + } + + /** + * @dev Perform implementation upgrade with additional setup call. + * + * Emits an {Upgraded} event. + */ + function _upgradeToAndCall(address newImplementation, bytes memory data, bool forceCall) internal { + _upgradeTo(newImplementation); + if (data.length > 0 || forceCall) { + Address.functionDelegateCall(newImplementation, data); + } + } + + /** + * @dev Perform implementation upgrade with security checks for UUPS proxies, and additional setup call. + * + * Emits an {Upgraded} event. + */ + function _upgradeToAndCallUUPS(address newImplementation, bytes memory data, bool forceCall) internal { + // Upgrades from old implementations will perform a rollback test. This test requires the new + // implementation to upgrade back to the old, non-ERC1822 compliant, implementation. Removing + // this special case will break upgrade paths from old UUPS implementation to new ones. + if (StorageSlot.getBooleanSlot(_ROLLBACK_SLOT).value) { + _setImplementation(newImplementation); + } else { + try IERC1822Proxiable(newImplementation).proxiableUUID() returns (bytes32 slot) { + require(slot == _IMPLEMENTATION_SLOT, "ERC1967Upgrade: unsupported proxiableUUID"); + } catch { + revert("ERC1967Upgrade: new implementation is not UUPS"); + } + _upgradeToAndCall(newImplementation, data, forceCall); + } + } + + /** + * @dev Storage slot with the admin of the contract. + * This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1, and is + * validated in the constructor. + */ + bytes32 internal constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + /** + * @dev Emitted when the admin account has changed. + */ + event AdminChanged(address previousAdmin, address newAdmin); + + /** + * @dev Returns the current admin. + */ + function _getAdmin() internal view returns (address) { + return StorageSlot.getAddressSlot(_ADMIN_SLOT).value; + } + + /** + * @dev Stores a new address in the EIP1967 admin slot. + */ + function _setAdmin(address newAdmin) private { + require(newAdmin != address(0), "ERC1967: new admin is the zero address"); + StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin; + } + + /** + * @dev Changes the admin of the proxy. + * + * Emits an {AdminChanged} event. + */ + function _changeAdmin(address newAdmin) internal { + emit AdminChanged(_getAdmin(), newAdmin); + _setAdmin(newAdmin); + } + + /** + * @dev The storage slot of the UpgradeableBeacon contract which defines the implementation for this proxy. + * This is bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)) and is validated in the constructor. + */ + bytes32 internal constant _BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; + + /** + * @dev Emitted when the beacon is upgraded. + */ + event BeaconUpgraded(address indexed beacon); + + /** + * @dev Returns the current beacon. + */ + function _getBeacon() internal view returns (address) { + return StorageSlot.getAddressSlot(_BEACON_SLOT).value; + } + + /** + * @dev Stores a new beacon in the EIP1967 beacon slot. + */ + function _setBeacon(address newBeacon) private { + require(Address.isContract(newBeacon), "ERC1967: new beacon is not a contract"); + require( + Address.isContract(IBeacon(newBeacon).implementation()), + "ERC1967: beacon implementation is not a contract" + ); + StorageSlot.getAddressSlot(_BEACON_SLOT).value = newBeacon; + } + + /** + * @dev Perform beacon upgrade with additional setup call. Note: This upgrades the address of the beacon, it does + * not upgrade the implementation contained in the beacon (see {UpgradeableBeacon-_setImplementation} for that). + * + * Emits a {BeaconUpgraded} event. + */ + function _upgradeBeaconToAndCall(address newBeacon, bytes memory data, bool forceCall) internal { + _setBeacon(newBeacon); + emit BeaconUpgraded(newBeacon); + if (data.length > 0 || forceCall) { + Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data); + } + } +} diff --git a/contracts/external-deps/openzeppelin/proxy/IERC1822Proxiable.sol b/contracts/external-deps/openzeppelin/proxy/IERC1822Proxiable.sol new file mode 100644 index 000000000..3b73d744c --- /dev/null +++ b/contracts/external-deps/openzeppelin/proxy/IERC1822Proxiable.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.5.0) (interfaces/draft-IERC1822.sol) + +pragma solidity ^0.8.0; + +/** + * @dev ERC1822: Universal Upgradeable Proxy Standard (UUPS) documents a method for upgradeability through a simplified + * proxy whose upgrades are fully controlled by the current implementation. + */ +interface IERC1822Proxiable { + /** + * @dev Returns the storage slot that the proxiable contract assumes is being used to store the implementation + * address. + * + * IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks + * bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this + * function revert if invoked through a proxy. + */ + function proxiableUUID() external view returns (bytes32); +} diff --git a/contracts/external-deps/openzeppelin/proxy/Proxy.sol b/contracts/external-deps/openzeppelin/proxy/Proxy.sol new file mode 100644 index 000000000..988cf72a0 --- /dev/null +++ b/contracts/external-deps/openzeppelin/proxy/Proxy.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.6.0) (proxy/Proxy.sol) + +pragma solidity ^0.8.0; + +/** + * @dev This abstract contract provides a fallback function that delegates all calls to another contract using the EVM + * instruction `delegatecall`. We refer to the second contract as the _implementation_ behind the proxy, and it has to + * be specified by overriding the virtual {_implementation} function. + * + * Additionally, delegation to the implementation can be triggered manually through the {_fallback} function, or to a + * different contract through the {_delegate} function. + * + * The success and return data of the delegated call will be returned back to the caller of the proxy. + */ +abstract contract Proxy { + /** + * @dev Delegates the current call to `implementation`. + * + * This function does not return to its internal call site, it will return directly to the external caller. + */ + function _delegate(address implementation) internal virtual { + assembly { + // Copy msg.data. We take full control of memory in this inline assembly + // block because it will not return to Solidity code. We overwrite the + // Solidity scratch pad at memory position 0. + calldatacopy(0, 0, calldatasize()) + + // Call the implementation. + // out and outsize are 0 because we don't know the size yet. + let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) + + // Copy the returned data. + returndatacopy(0, 0, returndatasize()) + + switch result + // delegatecall returns 0 on error. + case 0 { + revert(0, returndatasize()) + } + default { + return(0, returndatasize()) + } + } + } + + /** + * @dev This is a virtual function that should be overridden so it returns the address to which the fallback function + * and {_fallback} should delegate. + */ + function _implementation() internal view virtual returns (address); + + /** + * @dev Delegates the current call to the address returned by `_implementation()`. + * + * This function does not return to its internal call site, it will return directly to the external caller. + */ + function _fallback() internal virtual { + _beforeFallback(); + _delegate(_implementation()); + } + + /** + * @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if no other + * function in the contract matches the call data. + */ + fallback() external payable virtual { + _fallback(); + } + + /** + * @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if call data + * is empty. + */ + receive() external payable virtual { + _fallback(); + } + + /** + * @dev Hook that is called before falling back to the implementation. Can happen as part of a manual `_fallback` + * call, or as part of the Solidity `fallback` or `receive` functions. + * + * If overridden should call `super._beforeFallback()`. + */ + function _beforeFallback() internal virtual {} +} diff --git a/contracts/external-deps/openzeppelin/proxy/beacon/IBeacon.sol b/contracts/external-deps/openzeppelin/proxy/beacon/IBeacon.sol new file mode 100644 index 000000000..fba3ee2ab --- /dev/null +++ b/contracts/external-deps/openzeppelin/proxy/beacon/IBeacon.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (proxy/beacon/IBeacon.sol) + +pragma solidity ^0.8.0; + +/** + * @dev This is the interface that {BeaconProxy} expects of its beacon. + */ +interface IBeacon { + /** + * @dev Must return an address that can be used as a delegate call target. + * + * {BeaconProxy} will check that this address is a contract. + */ + function implementation() external view returns (address); +} diff --git a/contracts/external-deps/openzeppelin/proxy/utils/Initializable.sol b/contracts/external-deps/openzeppelin/proxy/utils/Initializable.sol new file mode 100644 index 000000000..a2ade335e --- /dev/null +++ b/contracts/external-deps/openzeppelin/proxy/utils/Initializable.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (proxy/utils/Initializable.sol) + +pragma solidity ^0.8.2; + +import "../../../../lib/Address.sol"; + +/** + * @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed + * behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an + * external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer + * function so it can only be called once. The {initializer} modifier provided by this contract will have this effect. + * + * The initialization functions use a version number. Once a version number is used, it is consumed and cannot be + * reused. This mechanism prevents re-execution of each "step" but allows the creation of new initialization steps in + * case an upgrade adds a module that needs to be initialized. + * + * For example: + * + * [.hljs-theme-light.nopadding] + * ``` + * contract MyToken is ERC20Upgradeable { + * function initialize() initializer public { + * __ERC20_init("MyToken", "MTK"); + * } + * } + * contract MyTokenV2 is MyToken, ERC20PermitUpgradeable { + * function initializeV2() reinitializer(2) public { + * __ERC20Permit_init("MyToken"); + * } + * } + * ``` + * + * TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as + * possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}. + * + * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure + * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity. + * + * [CAUTION] + * ==== + * Avoid leaving a contract uninitialized. + * + * An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation + * contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke + * the {_disableInitializers} function in the constructor to automatically lock it when it is deployed: + * + * [.hljs-theme-light.nopadding] + * ``` + * /// @custom:oz-upgrades-unsafe-allow constructor + * constructor() { + * _disableInitializers(); + * } + * ``` + * ==== + */ +abstract contract Initializable { + /** + * @dev Indicates that the contract has been initialized. + * @custom:oz-retyped-from bool + */ + uint8 private _initialized; + + /** + * @dev Indicates that the contract is in the process of being initialized. + */ + bool private _initializing; + + /** + * @dev Triggered when the contract has been initialized or reinitialized. + */ + event Initialized(uint8 version); + + /** + * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope, + * `onlyInitializing` functions can be used to initialize parent contracts. Equivalent to `reinitializer(1)`. + */ + modifier initializer() { + bool isTopLevelCall = !_initializing; + require( + (isTopLevelCall && _initialized < 1) || (!Address.isContract(address(this)) && _initialized == 1), + "Initializable: contract is already initialized" + ); + _initialized = 1; + if (isTopLevelCall) { + _initializing = true; + } + _; + if (isTopLevelCall) { + _initializing = false; + emit Initialized(1); + } + } + + /** + * @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the + * contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be + * used to initialize parent contracts. + * + * `initializer` is equivalent to `reinitializer(1)`, so a reinitializer may be used after the original + * initialization step. This is essential to configure modules that are added through upgrades and that require + * initialization. + * + * Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in + * a contract, executing them in the right order is up to the developer or operator. + */ + modifier reinitializer(uint8 version) { + require(!_initializing && _initialized < version, "Initializable: contract is already initialized"); + _initialized = version; + _initializing = true; + _; + _initializing = false; + emit Initialized(version); + } + + /** + * @dev Modifier to protect an initialization function so that it can only be invoked by functions with the + * {initializer} and {reinitializer} modifiers, directly or indirectly. + */ + modifier onlyInitializing() { + require(_initializing, "Initializable: contract is not initializing"); + _; + } + + /** + * @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. + * Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized + * to any version. It is recommended to use this to lock implementation contracts that are designed to be called + * through proxies. + */ + function _disableInitializers() internal virtual { + require(!_initializing, "Initializable: contract is initializing"); + if (_initialized < type(uint8).max) { + _initialized = type(uint8).max; + emit Initialized(type(uint8).max); + } + } +} diff --git a/contracts/external-deps/openzeppelin/security/ReentrancyGuard.sol b/contracts/external-deps/openzeppelin/security/ReentrancyGuard.sol new file mode 100644 index 000000000..70ae78e77 --- /dev/null +++ b/contracts/external-deps/openzeppelin/security/ReentrancyGuard.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (security/ReentrancyGuard.sol) + +pragma solidity ^0.8.0; + +abstract contract ReentrancyGuard { + uint256 private constant _NOT_ENTERED = 1; + uint256 private constant _ENTERED = 2; + + uint256 private _status; + + constructor() { + _status = _NOT_ENTERED; + } + + /** + * @dev Prevents a contract from calling itself, directly or indirectly. + */ + modifier nonReentrant() { + // On the first call to nonReentrant, _notEntered will be true + require(_status != _ENTERED, "ReentrancyGuard: reentrant call"); + + // Any calls to nonReentrant after this point will fail + _status = _ENTERED; + + _; + + // By storing the original value once again, a refund is triggered (see + // https://eips.ethereum.org/EIPS/eip-2200) + _status = _NOT_ENTERED; + } +} diff --git a/contracts/external-deps/openzeppelin/security/ReentrancyGuardUpgradeable.sol b/contracts/external-deps/openzeppelin/security/ReentrancyGuardUpgradeable.sol new file mode 100644 index 000000000..67041dbc7 --- /dev/null +++ b/contracts/external-deps/openzeppelin/security/ReentrancyGuardUpgradeable.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (security/ReentrancyGuard.sol) + +pragma solidity ^0.8.0; +import "../proxy/utils/Initializable.sol"; + +abstract contract ReentrancyGuardUpgradeable is Initializable { + uint256 private constant _NOT_ENTERED = 1; + uint256 private constant _ENTERED = 2; + + uint256 private _status; + + function __ReentrancyGuard_init() internal onlyInitializing { + __ReentrancyGuard_init_unchained(); + } + + function __ReentrancyGuard_init_unchained() internal onlyInitializing { + _status = _NOT_ENTERED; + } + + /** + * @dev Prevents a contract from calling itself, directly or indirectly. + */ + modifier nonReentrant() { + // On the first call to nonReentrant, _notEntered will be true + require(_status != _ENTERED, "ReentrancyGuard: reentrant call"); + + // Any calls to nonReentrant after this point will fail + _status = _ENTERED; + + _; + + // By storing the original value once again, a refund is triggered (see + // https://eips.ethereum.org/EIPS/eip-2200) + _status = _NOT_ENTERED; + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[49] private __gap; +} diff --git a/contracts/external-deps/openzeppelin/token/ERC1155/IERC1155Receiver.sol b/contracts/external-deps/openzeppelin/token/ERC1155/IERC1155Receiver.sol new file mode 100644 index 000000000..1abd0daf9 --- /dev/null +++ b/contracts/external-deps/openzeppelin/token/ERC1155/IERC1155Receiver.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC1155/IERC1155Receiver.sol) + +pragma solidity ^0.8.0; + +import "../../../../eip/interface/IERC165.sol"; + +/** + * @dev _Available since v3.1._ + */ +interface IERC1155Receiver is IERC165 { + /** + * @dev Handles the receipt of a single ERC1155 token type. This function is + * called at the end of a `safeTransferFrom` after the balance has been updated. + * + * NOTE: To accept the transfer, this must return + * `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + * (i.e. 0xf23a6e61, or its own function selector). + * + * @param operator The address which initiated the transfer (i.e. msg.sender) + * @param from The address which previously owned the token + * @param id The ID of the token being transferred + * @param value The amount of tokens being transferred + * @param data Additional data with no specified format + * @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed + */ + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes calldata data + ) external returns (bytes4); + + /** + * @dev Handles the receipt of a multiple ERC1155 token types. This function + * is called at the end of a `safeBatchTransferFrom` after the balances have + * been updated. + * + * NOTE: To accept the transfer(s), this must return + * `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + * (i.e. 0xbc197c81, or its own function selector). + * + * @param operator The address which initiated the batch transfer (i.e. msg.sender) + * @param from The address which previously owned the token + * @param ids An array containing ids of each token being transferred (order and length must match values array) + * @param values An array containing amounts of each token being transferred (order and length must match ids array) + * @param data Additional data with no specified format + * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed + */ + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external returns (bytes4); +} diff --git a/contracts/external-deps/openzeppelin/token/ERC1155/utils/ERC1155Holder.sol b/contracts/external-deps/openzeppelin/token/ERC1155/utils/ERC1155Holder.sol new file mode 100644 index 000000000..7249de841 --- /dev/null +++ b/contracts/external-deps/openzeppelin/token/ERC1155/utils/ERC1155Holder.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC1155/utils/ERC1155Holder.sol) + +pragma solidity ^0.8.0; + +import "./ERC1155Receiver.sol"; + +/** + * Simple implementation of `ERC1155Receiver` that will allow a contract to hold ERC1155 tokens. + * + * IMPORTANT: When inheriting this contract, you must include a way to use the received tokens, otherwise they will be + * stuck. + * + * @dev _Available since v3.1._ + */ +contract ERC1155Holder is ERC1155Receiver { + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes memory + ) public virtual override returns (bytes4) { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address, + address, + uint256[] memory, + uint256[] memory, + bytes memory + ) public virtual override returns (bytes4) { + return this.onERC1155BatchReceived.selector; + } +} diff --git a/contracts/external-deps/openzeppelin/token/ERC1155/utils/ERC1155Receiver.sol b/contracts/external-deps/openzeppelin/token/ERC1155/utils/ERC1155Receiver.sol new file mode 100644 index 000000000..8a315b71c --- /dev/null +++ b/contracts/external-deps/openzeppelin/token/ERC1155/utils/ERC1155Receiver.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (token/ERC1155/utils/ERC1155Receiver.sol) + +pragma solidity ^0.8.0; + +import "../IERC1155Receiver.sol"; +import "../../../../../eip/ERC165.sol"; + +/** + * @dev _Available since v3.1._ + */ +abstract contract ERC1155Receiver is ERC165, IERC1155Receiver { + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { + return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); + } +} diff --git a/contracts/external-deps/openzeppelin/token/ERC20/ERC20.sol b/contracts/external-deps/openzeppelin/token/ERC20/ERC20.sol new file mode 100644 index 000000000..66ce9ec07 --- /dev/null +++ b/contracts/external-deps/openzeppelin/token/ERC20/ERC20.sol @@ -0,0 +1,359 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC20/ERC20.sol) + +pragma solidity ^0.8.0; + +import "../../../../eip/interface/IERC20.sol"; +import "../../../../eip/interface/IERC20Metadata.sol"; +import "../../utils/Context.sol"; + +/** + * @dev Implementation of the {IERC20} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * For a generic mechanism see {ERC20PresetMinterPauser}. + * + * TIP: For a detailed writeup see our guide + * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * We have followed general OpenZeppelin Contracts guidelines: functions revert + * instead returning `false` on failure. This behavior is nonetheless + * conventional and does not conflict with the expectations of ERC20 + * applications. + * + * Additionally, an {Approval} event is emitted on calls to {transferFrom}. + * This allows applications to reconstruct the allowance for all accounts just + * by listening to said events. Other implementations of the EIP may not emit + * these events, as it isn't required by the specification. + * + * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} + * functions have been added to mitigate the well-known issues around setting + * allowances. See {IERC20-approve}. + */ +contract ERC20 is Context, IERC20, IERC20Metadata { + mapping(address => uint256) private _balances; + + mapping(address => mapping(address => uint256)) private _allowances; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + + /** + * @dev Sets the values for {name} and {symbol}. + * + * The default value of {decimals} is 18. To select a different value for + * {decimals} you should overload it. + * + * All two of these values are immutable: they can only be set once during + * construction. + */ + constructor(string memory name_, string memory symbol_) { + _name = name_; + _symbol = symbol_; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view virtual override returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5.05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the value {ERC20} uses, unless this function is + * overridden; + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view virtual override returns (uint8) { + return 18; + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view virtual override returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view virtual override returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address to, uint256 amount) public virtual override returns (bool) { + address owner = _msgSender(); + _transfer(owner, to, amount); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) public view virtual override returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on + * `transferFrom`. This is semantically equivalent to an infinite approval. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) public virtual override returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}. + * + * NOTE: Does not update the allowance if the current allowance + * is the maximum `uint256`. + * + * Requirements: + * + * - `from` and `to` cannot be the zero address. + * - `from` must have a balance of at least `amount`. + * - the caller must have allowance for ``from``'s tokens of at least + * `amount`. + */ + function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) { + address spender = _msgSender(); + _spendAllowance(from, spender, amount); + _transfer(from, to, amount); + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, _allowances[owner][spender] + addedValue); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { + address owner = _msgSender(); + uint256 currentAllowance = _allowances[owner][spender]; + require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero"); + unchecked { + _approve(owner, spender, currentAllowance - subtractedValue); + } + + return true; + } + + /** + * @dev Moves `amount` of tokens from `sender` to `recipient`. + * + * This internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `from` must have a balance of at least `amount`. + */ + function _transfer(address from, address to, uint256 amount) internal virtual { + require(from != address(0), "ERC20: transfer from the zero address"); + require(to != address(0), "ERC20: transfer to the zero address"); + + _beforeTokenTransfer(from, to, amount); + + uint256 fromBalance = _balances[from]; + require(fromBalance >= amount, "ERC20: transfer amount exceeds balance"); + unchecked { + _balances[from] = fromBalance - amount; + } + _balances[to] += amount; + + emit Transfer(from, to, amount); + + _afterTokenTransfer(from, to, amount); + } + + /** @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _beforeTokenTransfer(address(0), account, amount); + + _totalSupply += amount; + _balances[account] += amount; + emit Transfer(address(0), account, amount); + + _afterTokenTransfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + + _beforeTokenTransfer(account, address(0), amount); + + uint256 accountBalance = _balances[account]; + require(accountBalance >= amount, "ERC20: burn amount exceeds balance"); + unchecked { + _balances[account] = accountBalance - amount; + } + _totalSupply -= amount; + + emit Transfer(account, address(0), amount); + + _afterTokenTransfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve(address owner, address spender, uint256 amount) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Spend `amount` form the allowance of `owner` toward `spender`. + * + * Does not update the allowance amount in case of infinite allowance. + * Revert if not enough allowance is available. + * + * Might emit an {Approval} event. + */ + function _spendAllowance(address owner, address spender, uint256 amount) internal virtual { + uint256 currentAllowance = allowance(owner, spender); + if (currentAllowance != type(uint256).max) { + require(currentAllowance >= amount, "ERC20: insufficient allowance"); + unchecked { + _approve(owner, spender, currentAllowance - amount); + } + } + } + + /** + * @dev Hook that is called before any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * will be transferred to `to`. + * - when `from` is zero, `amount` tokens will be minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens will be burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {} + + /** + * @dev Hook that is called after any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * has been transferred to `to`. + * - when `from` is zero, `amount` tokens have been minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens have been burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {} +} diff --git a/contracts/external-deps/openzeppelin/token/ERC20/extensions/ERC20Permit.sol b/contracts/external-deps/openzeppelin/token/ERC20/extensions/ERC20Permit.sol new file mode 100644 index 000000000..e9433a92e --- /dev/null +++ b/contracts/external-deps/openzeppelin/token/ERC20/extensions/ERC20Permit.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/draft-ERC20Permit.sol) + +pragma solidity ^0.8.0; + +import "../../../../../eip/interface/IERC20Permit.sol"; +import "../ERC20.sol"; +import "../../../utils/cryptography/EIP712.sol"; +import "../../../utils/cryptography/ECDSA.sol"; +import "../../../utils/Counters.sol"; + +/** + * @dev Implementation of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by + * presenting a message signed by the account. By not relying on `{IERC20-approve}`, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + * + * _Available since v3.4._ + */ +abstract contract ERC20Permit is ERC20, IERC20Permit { + using Counters for Counters.Counter; + + mapping(address => Counters.Counter) private _nonces; + + // solhint-disable-next-line var-name-mixedcase + bytes32 private immutable _CACHED_DOMAIN_SEPARATOR; + + // solhint-disable-next-line var-name-mixedcase + uint256 private immutable _CACHED_CHAIN_ID; + + // solhint-disable-next-line var-name-mixedcase + address private immutable _CACHED_THIS; + + // solhint-disable-next-line var-name-mixedcase + bytes32 private immutable _PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + /** + * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. + * + * It's a good idea to use the same `name` that is defined as the ERC20 token name. + */ + constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) { + _CACHED_CHAIN_ID = block.chainid; + _CACHED_THIS = address(this); + _CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(); + } + + /** + * @dev See {IERC20Permit-permit}. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override { + require(block.timestamp <= deadline, "ERC20Permit: expired deadline"); + + bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); + + bytes32 hash = ECDSA.toTypedDataHash(DOMAIN_SEPARATOR(), structHash); + + address signer = ECDSA.recover(hash, v, r, s); + require(signer == owner, "ERC20Permit: invalid signature"); + + _approve(owner, spender, value); + } + + /** + * @dev See {IERC20Permit-nonces}. + */ + function nonces(address owner) public view virtual override returns (uint256) { + return _nonces[owner].current(); + } + + /** + * @dev See {IERC20Permit-DOMAIN_SEPARATOR}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() public view override returns (bytes32) { + if (address(this) == _CACHED_THIS && block.chainid == _CACHED_CHAIN_ID) { + return _CACHED_DOMAIN_SEPARATOR; + } else { + return _buildDomainSeparator(); + } + } + + function _buildDomainSeparator() private view returns (bytes32) { + return + keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(name())), + keccak256("1"), + block.chainid, + address(this) + ) + ); + } + + /** + * @dev "Consume a nonce": return the current value and increment. + * + * _Available since v4.1._ + */ + function _useNonce(address owner) internal virtual returns (uint256 current) { + Counters.Counter storage nonce = _nonces[owner]; + current = nonce.current(); + nonce.increment(); + } +} diff --git a/contracts/external-deps/openzeppelin/token/ERC20/extensions/ERC20Votes.sol b/contracts/external-deps/openzeppelin/token/ERC20/extensions/ERC20Votes.sol new file mode 100644 index 000000000..e80527066 --- /dev/null +++ b/contracts/external-deps/openzeppelin/token/ERC20/extensions/ERC20Votes.sol @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./ERC20Permit.sol"; + +import "../../../utils/math/Math.sol"; +import "../../../governance/utils/IVotes.sol"; +import "../../../utils/math/SafeCast.sol"; + +/** + * @dev Extension of ERC20 to support Compound-like voting and delegation. This version is more generic than Compound's, + * and supports token supply up to 2^224^ - 1, while COMP is limited to 2^96^ - 1. + * + * NOTE: If exact COMP compatibility is required, use the {ERC20VotesComp} variant of this module. + * + * This extension keeps a history (checkpoints) of each account's vote power. Vote power can be delegated either + * by calling the {delegate} function directly, or by providing a signature to be used with {delegateBySig}. Voting + * power can be queried through the public accessors {getVotes} and {getPastVotes}. + * + * By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it + * requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. + * + * _Available since v4.2._ + */ +abstract contract ERC20Votes is IVotes, ERC20Permit { + struct Checkpoint { + uint32 fromBlock; + uint224 votes; + } + + bytes32 private constant _DELEGATION_TYPEHASH = + keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); + + mapping(address => address) private _delegates; + mapping(address => Checkpoint[]) private _checkpoints; + Checkpoint[] private _totalSupplyCheckpoints; + + /** + * @dev Get the `pos`-th checkpoint for `account`. + */ + function checkpoints(address account, uint32 pos) public view virtual returns (Checkpoint memory) { + return _checkpoints[account][pos]; + } + + /** + * @dev Get number of checkpoints for `account`. + */ + function numCheckpoints(address account) public view virtual returns (uint32) { + return SafeCast.toUint32(_checkpoints[account].length); + } + + /** + * @dev Get the address `account` is currently delegating to. + */ + function delegates(address account) public view virtual override returns (address) { + return _delegates[account]; + } + + /** + * @dev Gets the current votes balance for `account` + */ + function getVotes(address account) public view virtual override returns (uint256) { + uint256 pos = _checkpoints[account].length; + return pos == 0 ? 0 : _checkpoints[account][pos - 1].votes; + } + + /** + * @dev Retrieve the number of votes for `account` at the end of `blockNumber`. + * + * Requirements: + * + * - `blockNumber` must have been already mined + */ + function getPastVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { + require(blockNumber < block.number, "ERC20Votes: block not yet mined"); + return _checkpointsLookup(_checkpoints[account], blockNumber); + } + + /** + * @dev Retrieve the `totalSupply` at the end of `blockNumber`. Note, this value is the sum of all balances. + * It is but NOT the sum of all the delegated votes! + * + * Requirements: + * + * - `blockNumber` must have been already mined + */ + function getPastTotalSupply(uint256 blockNumber) public view virtual override returns (uint256) { + require(blockNumber < block.number, "ERC20Votes: block not yet mined"); + return _checkpointsLookup(_totalSupplyCheckpoints, blockNumber); + } + + /** + * @dev Lookup a value in a list of (sorted) checkpoints. + */ + function _checkpointsLookup(Checkpoint[] storage ckpts, uint256 blockNumber) private view returns (uint256) { + // We run a binary search to look for the earliest checkpoint taken after `blockNumber`. + // + // During the loop, the index of the wanted checkpoint remains in the range [low-1, high). + // With each iteration, either `low` or `high` is moved towards the middle of the range to maintain the invariant. + // - If the middle checkpoint is after `blockNumber`, we look in [low, mid) + // - If the middle checkpoint is before or equal to `blockNumber`, we look in [mid+1, high) + // Once we reach a single value (when low == high), we've found the right checkpoint at the index high-1, if not + // out of bounds (in which case we're looking too far in the past and the result is 0). + // Note that if the latest checkpoint available is exactly for `blockNumber`, we end up with an index that is + // past the end of the array, so we technically don't find a checkpoint after `blockNumber`, but it works out + // the same. + uint256 high = ckpts.length; + uint256 low = 0; + while (low < high) { + uint256 mid = Math.average(low, high); + if (ckpts[mid].fromBlock > blockNumber) { + high = mid; + } else { + low = mid + 1; + } + } + + return high == 0 ? 0 : ckpts[high - 1].votes; + } + + /** + * @dev Delegate votes from the sender to `delegatee`. + */ + function delegate(address delegatee) public virtual override { + // _delegate(_msgSender(), delegatee); //check + _delegate(_msgSender(), delegatee); + } + + /*////////////////////////////////////////////////////////////// + Voting - delegation by signature + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Delegates votes from signer to `delegatee` + */ + function delegateBySig( + address delegatee, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override { + require(block.timestamp <= expiry, "ERC20Votes: signature expired"); + + bytes32 structHash = keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry)); + bytes32 hash = ECDSA.toTypedDataHash(DOMAIN_SEPARATOR(), structHash); + address signer = ECDSA.recover(hash, v, r, s); + + require(nonce == _useNonce(signer), "ERC20Votes: invalid nonce"); + _delegate(signer, delegatee); + } + + /** + * @dev Maximum token supply. Defaults to `type(uint224).max` (2^224^ - 1). + */ + function _maxSupply() internal view virtual returns (uint224) { + return type(uint224).max; + } + + /** + * @dev Snapshots the totalSupply after it has been increased. + */ + function _mint(address account, uint256 amount) internal virtual override { + super._mint(account, amount); + require(totalSupply() <= _maxSupply(), "ERC20Votes: total supply risks overflowing votes"); + + _writeCheckpoint(_totalSupplyCheckpoints, _add, amount); + } + + /** + * @dev Snapshots the totalSupply after it has been decreased. + */ + function _burn(address account, uint256 amount) internal virtual override { + super._burn(account, amount); + + _writeCheckpoint(_totalSupplyCheckpoints, _subtract, amount); + } + + /** + * @dev Move voting power when tokens are transferred. + * + * Emits a {DelegateVotesChanged} event. + */ + function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual override { + super._afterTokenTransfer(from, to, amount); + + _moveVotingPower(delegates(from), delegates(to), amount); + } + + /** + * @dev Change delegation for `delegator` to `delegatee`. + * + * Emits events {DelegateChanged} and {DelegateVotesChanged}. + */ + function _delegate(address delegator, address delegatee) internal virtual { + address currentDelegate = delegates(delegator); + uint256 delegatorBalance = balanceOf(delegator); + _delegates[delegator] = delegatee; + + emit DelegateChanged(delegator, currentDelegate, delegatee); + + _moveVotingPower(currentDelegate, delegatee, delegatorBalance); + } + + function _moveVotingPower(address src, address dst, uint256 amount) private { + if (src != dst && amount > 0) { + if (src != address(0)) { + (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[src], _subtract, amount); + emit DelegateVotesChanged(src, oldWeight, newWeight); + } + + if (dst != address(0)) { + (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[dst], _add, amount); + emit DelegateVotesChanged(dst, oldWeight, newWeight); + } + } + } + + function _writeCheckpoint( + Checkpoint[] storage ckpts, + function(uint256, uint256) view returns (uint256) op, + uint256 delta + ) private returns (uint256 oldWeight, uint256 newWeight) { + uint256 pos = ckpts.length; + oldWeight = pos == 0 ? 0 : ckpts[pos - 1].votes; + newWeight = op(oldWeight, delta); + + if (pos > 0 && ckpts[pos - 1].fromBlock == block.number) { + ckpts[pos - 1].votes = SafeCast.toUint224(newWeight); + } else { + ckpts.push( + Checkpoint({ fromBlock: SafeCast.toUint32(block.number), votes: SafeCast.toUint224(newWeight) }) + ); + } + } + + function _add(uint256 a, uint256 b) private pure returns (uint256) { + return a + b; + } + + function _subtract(uint256 a, uint256 b) private pure returns (uint256) { + return a - b; + } +} diff --git a/contracts/openzeppelin-presets/token/ERC20/utils/SafeERC20.sol b/contracts/external-deps/openzeppelin/token/ERC20/utils/SafeERC20.sol similarity index 81% rename from contracts/openzeppelin-presets/token/ERC20/utils/SafeERC20.sol rename to contracts/external-deps/openzeppelin/token/ERC20/utils/SafeERC20.sol index 7ae063a6f..00bdee343 100644 --- a/contracts/openzeppelin-presets/token/ERC20/utils/SafeERC20.sol +++ b/contracts/external-deps/openzeppelin/token/ERC20/utils/SafeERC20.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.0; -import "../../../../eip/interface/IERC20.sol"; -import "../../../../lib/TWAddress.sol"; +import "../../../../../eip/interface/IERC20.sol"; +import { Address } from "../../../../../lib/Address.sol"; /** * @title SafeERC20 @@ -16,22 +16,13 @@ import "../../../../lib/TWAddress.sol"; * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. */ library SafeERC20 { - using TWAddress for address; + using Address for address; - function safeTransfer( - IERC20 token, - address to, - uint256 value - ) internal { + function safeTransfer(IERC20 token, address to, uint256 value) internal { _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); } - function safeTransferFrom( - IERC20 token, - address from, - address to, - uint256 value - ) internal { + function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value)); } @@ -42,11 +33,7 @@ library SafeERC20 { * Whenever possible, use {safeIncreaseAllowance} and * {safeDecreaseAllowance} instead. */ - function safeApprove( - IERC20 token, - address spender, - uint256 value - ) internal { + function safeApprove(IERC20 token, address spender, uint256 value) internal { // safeApprove should only be called when setting an initial allowance, // or when resetting it to zero. To increase and decrease it, use // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' @@ -57,20 +44,12 @@ library SafeERC20 { _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); } - function safeIncreaseAllowance( - IERC20 token, - address spender, - uint256 value - ) internal { + function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal { uint256 newAllowance = token.allowance(address(this), spender) + value; _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance)); } - function safeDecreaseAllowance( - IERC20 token, - address spender, - uint256 value - ) internal { + function safeDecreaseAllowance(IERC20 token, address spender, uint256 value) internal { unchecked { uint256 oldAllowance = token.allowance(address(this), spender); require(oldAllowance >= value, "SafeERC20: decreased allowance below zero"); diff --git a/contracts/external-deps/openzeppelin/token/ERC721/IERC721Receiver.sol b/contracts/external-deps/openzeppelin/token/ERC721/IERC721Receiver.sol new file mode 100644 index 000000000..a42cb52ff --- /dev/null +++ b/contracts/external-deps/openzeppelin/token/ERC721/IERC721Receiver.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (token/ERC721/IERC721Receiver.sol) + +pragma solidity ^0.8.0; + +/** + * @title ERC721 token receiver interface + * @dev Interface for any contract that wants to support safeTransfers + * from ERC721 asset contracts. + */ +interface IERC721Receiver { + /** + * @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom} + * by `operator` from `from`, this function is called. + * + * It must return its Solidity selector to confirm the token transfer. + * If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted. + * + * The selector can be obtained in Solidity with `IERC721.onERC721Received.selector`. + */ + function onERC721Received( + address operator, + address from, + uint256 tokenId, + bytes calldata data + ) external returns (bytes4); +} diff --git a/contracts/external-deps/openzeppelin/token/ERC721/utils/ERC721Holder.sol b/contracts/external-deps/openzeppelin/token/ERC721/utils/ERC721Holder.sol new file mode 100644 index 000000000..cfa533a47 --- /dev/null +++ b/contracts/external-deps/openzeppelin/token/ERC721/utils/ERC721Holder.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (token/ERC721/utils/ERC721Holder.sol) + +pragma solidity ^0.8.0; + +import "../IERC721Receiver.sol"; + +/** + * @dev Implementation of the {IERC721Receiver} interface. + * + * Accepts all token transfers. + * Make sure the contract is able to use its token with {IERC721-safeTransferFrom}, {IERC721-approve} or {IERC721-setApprovalForAll}. + */ +contract ERC721Holder is IERC721Receiver { + /** + * @dev See {IERC721Receiver-onERC721Received}. + * + * Always returns `IERC721Receiver.onERC721Received.selector`. + */ + function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4) { + return this.onERC721Received.selector; + } +} diff --git a/contracts/external-deps/openzeppelin/utils/Base64.sol b/contracts/external-deps/openzeppelin/utils/Base64.sol new file mode 100644 index 000000000..4e08cd563 --- /dev/null +++ b/contracts/external-deps/openzeppelin/utils/Base64.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (utils/Base64.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Provides a set of functions to operate with Base64 strings. + * + * _Available since v4.5._ + */ +library Base64 { + /** + * @dev Base64 Encoding/Decoding Table + */ + string internal constant _TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + /** + * @dev Converts a `bytes` to its Bytes64 `string` representation. + */ + function encode(bytes memory data) internal pure returns (string memory) { + /** + * Inspired by Brecht Devos (Brechtpd) implementation - MIT licence + * https://github.com/Brechtpd/base64/blob/e78d9fd951e7b0977ddca77d92dc85183770daf4/base64.sol + */ + if (data.length == 0) return ""; + + // Loads the table into memory + string memory table = _TABLE; + + // Encoding takes 3 bytes chunks of binary data from `bytes` data parameter + // and split into 4 numbers of 6 bits. + // The final Base64 length should be `bytes` data length multiplied by 4/3 rounded up + // - `data.length + 2` -> Round up + // - `/ 3` -> Number of 3-bytes chunks + // - `4 *` -> 4 characters for each chunk + string memory result = new string(4 * ((data.length + 2) / 3)); + + /// @solidity memory-safe-assembly + assembly { + // Prepare the lookup table (skip the first "length" byte) + let tablePtr := add(table, 1) + + // Prepare result pointer, jump over length + let resultPtr := add(result, 32) + + // Run over the input, 3 bytes at a time + for { + let dataPtr := data + let endPtr := add(data, mload(data)) + } lt(dataPtr, endPtr) { + + } { + // Advance 3 bytes + dataPtr := add(dataPtr, 3) + let input := mload(dataPtr) + + // To write each character, shift the 3 bytes (18 bits) chunk + // 4 times in blocks of 6 bits for each character (18, 12, 6, 0) + // and apply logical AND with 0x3F which is the number of + // the previous character in the ASCII table prior to the Base64 Table + // The result is then added to the table to get the character to write, + // and finally write it in the result pointer but with a left shift + // of 256 (1 byte) - 8 (1 ASCII char) = 248 bits + + mstore8(resultPtr, mload(add(tablePtr, and(shr(18, input), 0x3F)))) + resultPtr := add(resultPtr, 1) // Advance + + mstore8(resultPtr, mload(add(tablePtr, and(shr(12, input), 0x3F)))) + resultPtr := add(resultPtr, 1) // Advance + + mstore8(resultPtr, mload(add(tablePtr, and(shr(6, input), 0x3F)))) + resultPtr := add(resultPtr, 1) // Advance + + mstore8(resultPtr, mload(add(tablePtr, and(input, 0x3F)))) + resultPtr := add(resultPtr, 1) // Advance + } + + // When data `bytes` is not exactly 3 bytes long + // it is padded with `=` characters at the end + switch mod(mload(data), 3) + case 1 { + mstore8(sub(resultPtr, 1), 0x3d) + mstore8(sub(resultPtr, 2), 0x3d) + } + case 2 { + mstore8(sub(resultPtr, 1), 0x3d) + } + } + + return result; + } +} diff --git a/contracts/openzeppelin-presets/utils/Context.sol b/contracts/external-deps/openzeppelin/utils/Context.sol similarity index 100% rename from contracts/openzeppelin-presets/utils/Context.sol rename to contracts/external-deps/openzeppelin/utils/Context.sol diff --git a/contracts/external-deps/openzeppelin/utils/Counters.sol b/contracts/external-deps/openzeppelin/utils/Counters.sol new file mode 100644 index 000000000..8a4f2a2e7 --- /dev/null +++ b/contracts/external-deps/openzeppelin/utils/Counters.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (utils/Counters.sol) + +pragma solidity ^0.8.0; + +/** + * @title Counters + * @author Matt Condon (@shrugs) + * @dev Provides counters that can only be incremented, decremented or reset. This can be used e.g. to track the number + * of elements in a mapping, issuing ERC721 ids, or counting request ids. + * + * Include with `using Counters for Counters.Counter;` + */ +library Counters { + struct Counter { + // This variable should never be directly accessed by users of the library: interactions must be restricted to + // the library's function. As of Solidity v0.5.2, this cannot be enforced, though there is a proposal to add + // this feature: see https://github.com/ethereum/solidity/issues/4637 + uint256 _value; // default: 0 + } + + function current(Counter storage counter) internal view returns (uint256) { + return counter._value; + } + + function increment(Counter storage counter) internal { + unchecked { + counter._value += 1; + } + } + + function decrement(Counter storage counter) internal { + uint256 value = counter._value; + require(value > 0, "Counter: decrement overflow"); + unchecked { + counter._value = value - 1; + } + } + + function reset(Counter storage counter) internal { + counter._value = 0; + } +} diff --git a/contracts/external-deps/openzeppelin/utils/Create2.sol b/contracts/external-deps/openzeppelin/utils/Create2.sol new file mode 100644 index 000000000..d810d8045 --- /dev/null +++ b/contracts/external-deps/openzeppelin/utils/Create2.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (utils/Create2.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Helper to make usage of the `CREATE2` EVM opcode easier and safer. + * `CREATE2` can be used to compute in advance the address where a smart + * contract will be deployed, which allows for interesting new mechanisms known + * as 'counterfactual interactions'. + * + * See the https://eips.ethereum.org/EIPS/eip-1014#motivation[EIP] for more + * information. + */ +library Create2 { + /** + * @dev Deploys a contract using `CREATE2`. The address where the contract + * will be deployed can be known in advance via {computeAddress}. + * + * The bytecode for a contract can be obtained from Solidity with + * `type(contractName).creationCode`. + * + * Requirements: + * + * - `bytecode` must not be empty. + * - `salt` must have not been used for `bytecode` already. + * - the factory must have a balance of at least `amount`. + * - if `amount` is non-zero, `bytecode` must have a `payable` constructor. + */ + function deploy(uint256 amount, bytes32 salt, bytes memory bytecode) internal returns (address) { + address addr; + require(address(this).balance >= amount, "Create2: insufficient balance"); + require(bytecode.length != 0, "Create2: bytecode length is zero"); + /// @solidity memory-safe-assembly + assembly { + addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt) + } + require(addr != address(0), "Create2: Failed on deploy"); + return addr; + } + + /** + * @dev Returns the address where a contract will be stored if deployed via {deploy}. Any change in the + * `bytecodeHash` or `salt` will result in a new destination address. + */ + function computeAddress(bytes32 salt, bytes32 bytecodeHash) internal view returns (address) { + return computeAddress(salt, bytecodeHash, address(this)); + } + + /** + * @dev Returns the address where a contract will be stored if deployed via {deploy} from a contract located at + * `deployer`. If `deployer` is this contract's address, returns the same value as {computeAddress}. + */ + function computeAddress(bytes32 salt, bytes32 bytecodeHash, address deployer) internal pure returns (address) { + bytes32 _data = keccak256(abi.encodePacked(bytes1(0xff), deployer, salt, bytecodeHash)); + return address(uint160(uint256(_data))); + } +} diff --git a/contracts/external-deps/openzeppelin/utils/ERC1155/ERC1155Holder.sol b/contracts/external-deps/openzeppelin/utils/ERC1155/ERC1155Holder.sol new file mode 100644 index 000000000..7249de841 --- /dev/null +++ b/contracts/external-deps/openzeppelin/utils/ERC1155/ERC1155Holder.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC1155/utils/ERC1155Holder.sol) + +pragma solidity ^0.8.0; + +import "./ERC1155Receiver.sol"; + +/** + * Simple implementation of `ERC1155Receiver` that will allow a contract to hold ERC1155 tokens. + * + * IMPORTANT: When inheriting this contract, you must include a way to use the received tokens, otherwise they will be + * stuck. + * + * @dev _Available since v3.1._ + */ +contract ERC1155Holder is ERC1155Receiver { + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes memory + ) public virtual override returns (bytes4) { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address, + address, + uint256[] memory, + uint256[] memory, + bytes memory + ) public virtual override returns (bytes4) { + return this.onERC1155BatchReceived.selector; + } +} diff --git a/contracts/external-deps/openzeppelin/utils/ERC1155/ERC1155Receiver.sol b/contracts/external-deps/openzeppelin/utils/ERC1155/ERC1155Receiver.sol new file mode 100644 index 000000000..1f6ade8c5 --- /dev/null +++ b/contracts/external-deps/openzeppelin/utils/ERC1155/ERC1155Receiver.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache 2.0 +// OpenZeppelin Contracts v4.4.1 (token/ERC1155/utils/ERC1155Receiver.sol) + +pragma solidity ^0.8.0; + +import "../../../../eip/interface/IERC1155Receiver.sol"; +import "../../../../eip/ERC165.sol"; + +/** + * @dev _Available since v3.1._ + */ +abstract contract ERC1155Receiver is ERC165, IERC1155Receiver { + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { + return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); + } +} diff --git a/contracts/external-deps/openzeppelin/utils/ERC721/ERC721Holder.sol b/contracts/external-deps/openzeppelin/utils/ERC721/ERC721Holder.sol new file mode 100644 index 000000000..5d364e52e --- /dev/null +++ b/contracts/external-deps/openzeppelin/utils/ERC721/ERC721Holder.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (token/ERC721/utils/ERC721Holder.sol) + +pragma solidity ^0.8.0; + +import "../../../../eip/interface/IERC721Receiver.sol"; + +/** + * @dev Implementation of the {IERC721Receiver} interface. + * + * Accepts all token transfers. + * Make sure the contract is able to use its token with {IERC721-safeTransferFrom}, {IERC721-approve} or {IERC721-setApprovalForAll}. + */ +contract ERC721Holder is IERC721Receiver { + /** + * @dev See {IERC721Receiver-onERC721Received}. + * + * Always returns `IERC721Receiver.onERC721Received.selector`. + */ + function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4) { + return this.onERC721Received.selector; + } +} diff --git a/contracts/external-deps/openzeppelin/utils/EnumerableSet.sol b/contracts/external-deps/openzeppelin/utils/EnumerableSet.sol new file mode 100644 index 000000000..b6c647f07 --- /dev/null +++ b/contracts/external-deps/openzeppelin/utils/EnumerableSet.sol @@ -0,0 +1,367 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (utils/structs/EnumerableSet.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Library for managing + * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive + * types. + * + * Sets have the following properties: + * + * - Elements are added, removed, and checked for existence in constant time + * (O(1)). + * - Elements are enumerated in O(n). No guarantees are made on the ordering. + * + * ``` + * contract Example { + * // Add the library methods + * using EnumerableSet for EnumerableSet.AddressSet; + * + * // Declare a set state variable + * EnumerableSet.AddressSet private mySet; + * } + * ``` + * + * As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`) + * and `uint256` (`UintSet`) are supported. + * + * [WARNING] + * ==== + * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure unusable. + * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info. + * + * In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an array of EnumerableSet. + * ==== + */ +library EnumerableSet { + // To implement this library for multiple types with as little code + // repetition as possible, we write it in terms of a generic Set type with + // bytes32 values. + // The Set implementation uses private functions, and user-facing + // implementations (such as AddressSet) are just wrappers around the + // underlying Set. + // This means that we can only create new EnumerableSets for types that fit + // in bytes32. + + struct Set { + // Storage of set values + bytes32[] _values; + // Position of the value in the `values` array, plus 1 because index 0 + // means a value is not in the set. + mapping(bytes32 => uint256) _indexes; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function _add(Set storage set, bytes32 value) private returns (bool) { + if (!_contains(set, value)) { + set._values.push(value); + // The value is stored at length-1, but we add 1 to all indexes + // and use 0 as a sentinel value + set._indexes[value] = set._values.length; + return true; + } else { + return false; + } + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function _remove(Set storage set, bytes32 value) private returns (bool) { + // We read and store the value's index to prevent multiple reads from the same storage slot + uint256 valueIndex = set._indexes[value]; + + if (valueIndex != 0) { + // Equivalent to contains(set, value) + // To delete an element from the _values array in O(1), we swap the element to delete with the last one in + // the array, and then remove the last element (sometimes called as 'swap and pop'). + // This modifies the order of the array, as noted in {at}. + + uint256 toDeleteIndex = valueIndex - 1; + uint256 lastIndex = set._values.length - 1; + + if (lastIndex != toDeleteIndex) { + bytes32 lastValue = set._values[lastIndex]; + + // Move the last value to the index where the value to delete is + set._values[toDeleteIndex] = lastValue; + // Update the index for the moved value + set._indexes[lastValue] = valueIndex; // Replace lastValue's index to valueIndex + } + + // Delete the slot where the moved value was stored + set._values.pop(); + + // Delete the index for the deleted slot + delete set._indexes[value]; + + return true; + } else { + return false; + } + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function _contains(Set storage set, bytes32 value) private view returns (bool) { + return set._indexes[value] != 0; + } + + /** + * @dev Returns the number of values on the set. O(1). + */ + function _length(Set storage set) private view returns (uint256) { + return set._values.length; + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function _at(Set storage set, uint256 index) private view returns (bytes32) { + return set._values[index]; + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function _values(Set storage set) private view returns (bytes32[] memory) { + return set._values; + } + + // Bytes32Set + + struct Bytes32Set { + Set _inner; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(Bytes32Set storage set, bytes32 value) internal returns (bool) { + return _add(set._inner, value); + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) { + return _remove(set._inner, value); + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) { + return _contains(set._inner, value); + } + + /** + * @dev Returns the number of values in the set. O(1). + */ + function length(Bytes32Set storage set) internal view returns (uint256) { + return _length(set._inner); + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) { + return _at(set._inner, index); + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function values(Bytes32Set storage set) internal view returns (bytes32[] memory) { + return _values(set._inner); + } + + // AddressSet + + struct AddressSet { + Set _inner; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(AddressSet storage set, address value) internal returns (bool) { + return _add(set._inner, bytes32(uint256(uint160(value)))); + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(AddressSet storage set, address value) internal returns (bool) { + return _remove(set._inner, bytes32(uint256(uint160(value)))); + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(AddressSet storage set, address value) internal view returns (bool) { + return _contains(set._inner, bytes32(uint256(uint160(value)))); + } + + /** + * @dev Returns the number of values in the set. O(1). + */ + function length(AddressSet storage set) internal view returns (uint256) { + return _length(set._inner); + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(AddressSet storage set, uint256 index) internal view returns (address) { + return address(uint160(uint256(_at(set._inner, index)))); + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function values(AddressSet storage set) internal view returns (address[] memory) { + bytes32[] memory store = _values(set._inner); + address[] memory result; + + /// @solidity memory-safe-assembly + assembly { + result := store + } + + return result; + } + + // UintSet + + struct UintSet { + Set _inner; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(UintSet storage set, uint256 value) internal returns (bool) { + return _add(set._inner, bytes32(value)); + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(UintSet storage set, uint256 value) internal returns (bool) { + return _remove(set._inner, bytes32(value)); + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(UintSet storage set, uint256 value) internal view returns (bool) { + return _contains(set._inner, bytes32(value)); + } + + /** + * @dev Returns the number of values on the set. O(1). + */ + function length(UintSet storage set) internal view returns (uint256) { + return _length(set._inner); + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(UintSet storage set, uint256 index) internal view returns (uint256) { + return uint256(_at(set._inner, index)); + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function values(UintSet storage set) internal view returns (uint256[] memory) { + bytes32[] memory store = _values(set._inner); + uint256[] memory result; + + /// @solidity memory-safe-assembly + assembly { + result := store + } + + return result; + } +} diff --git a/contracts/openzeppelin-presets/utils/cryptography/ECDSA.sol b/contracts/external-deps/openzeppelin/utils/cryptography/ECDSA.sol similarity index 80% rename from contracts/openzeppelin-presets/utils/cryptography/ECDSA.sol rename to contracts/external-deps/openzeppelin/utils/cryptography/ECDSA.sol index 88d652976..0c9f09bdb 100644 --- a/contracts/openzeppelin-presets/utils/cryptography/ECDSA.sol +++ b/contracts/external-deps/openzeppelin/utils/cryptography/ECDSA.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.5.0) (utils/cryptography/ECDSA.sol) +// OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/ECDSA.sol) pragma solidity ^0.8.0; -import "../../../lib/TWStrings.sol"; +import "../../../../lib/Strings.sol"; /** * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. @@ -17,7 +17,7 @@ library ECDSA { InvalidSignature, InvalidSignatureLength, InvalidSignatureS, - InvalidSignatureV + InvalidSignatureV // Deprecated in v4.8 } function _throwError(RecoverError error) private pure { @@ -29,8 +29,6 @@ library ECDSA { revert("ECDSA: invalid signature length"); } else if (error == RecoverError.InvalidSignatureS) { revert("ECDSA: invalid signature 's' value"); - } else if (error == RecoverError.InvalidSignatureV) { - revert("ECDSA: invalid signature 'v' value"); } } @@ -55,31 +53,19 @@ library ECDSA { * _Available since v4.3._ */ function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError) { - // Check the signature length - // - case 65: r,s,v signature (standard) - // - case 64: r,vs signature (cf https://eips.ethereum.org/EIPS/eip-2098) _Available since v4.1._ if (signature.length == 65) { bytes32 r; bytes32 s; uint8 v; // ecrecover takes the signature parameters, and the only way to get them // currently is to use assembly. + /// @solidity memory-safe-assembly assembly { r := mload(add(signature, 0x20)) s := mload(add(signature, 0x40)) v := byte(0, mload(add(signature, 0x60))) } return tryRecover(hash, v, r, s); - } else if (signature.length == 64) { - bytes32 r; - bytes32 vs; - // ecrecover takes the signature parameters, and the only way to get them - // currently is to use assembly. - assembly { - r := mload(add(signature, 0x20)) - vs := mload(add(signature, 0x40)) - } - return tryRecover(hash, r, vs); } else { return (address(0), RecoverError.InvalidSignatureLength); } @@ -112,11 +98,7 @@ library ECDSA { * * _Available since v4.3._ */ - function tryRecover( - bytes32 hash, - bytes32 r, - bytes32 vs - ) internal pure returns (address, RecoverError) { + function tryRecover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address, RecoverError) { bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); uint8 v = uint8((uint256(vs) >> 255) + 27); return tryRecover(hash, v, r, s); @@ -127,11 +109,7 @@ library ECDSA { * * _Available since v4.2._ */ - function recover( - bytes32 hash, - bytes32 r, - bytes32 vs - ) internal pure returns (address) { + function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) { (address recovered, RecoverError error) = tryRecover(hash, r, vs); _throwError(error); return recovered; @@ -143,12 +121,7 @@ library ECDSA { * * _Available since v4.3._ */ - function tryRecover( - bytes32 hash, - uint8 v, - bytes32 r, - bytes32 s - ) internal pure returns (address, RecoverError) { + function tryRecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address, RecoverError) { // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most @@ -161,9 +134,6 @@ library ECDSA { if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { return (address(0), RecoverError.InvalidSignatureS); } - if (v != 27 && v != 28) { - return (address(0), RecoverError.InvalidSignatureV); - } // If the signature is valid (and not malleable), return the signer address address signer = ecrecover(hash, v, r, s); @@ -178,12 +148,7 @@ library ECDSA { * @dev Overload of {ECDSA-recover} that receives the `v`, * `r` and `s` signature fields separately. */ - function recover( - bytes32 hash, - uint8 v, - bytes32 r, - bytes32 s - ) internal pure returns (address) { + function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) { (address recovered, RecoverError error) = tryRecover(hash, v, r, s); _throwError(error); return recovered; @@ -197,10 +162,15 @@ library ECDSA { * * See {recover}. */ - function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) { + function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32 message) { // 32 is the length in bytes of hash, // enforced by the type signature above - return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, "\x19Ethereum Signed Message:\n32") + mstore(0x1c, hash) + message := keccak256(0x00, 0x3c) + } } /** @@ -212,7 +182,7 @@ library ECDSA { * See {recover}. */ function toEthSignedMessageHash(bytes memory s) internal pure returns (bytes32) { - return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n", TWStrings.toString(s.length), s)); + return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n", Strings.toString(s.length), s)); } /** @@ -224,7 +194,24 @@ library ECDSA { * * See {recover}. */ - function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32) { - return keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32 data) { + /// @solidity memory-safe-assembly + assembly { + let ptr := mload(0x40) + mstore(ptr, "\x19\x01") + mstore(add(ptr, 0x02), domainSeparator) + mstore(add(ptr, 0x22), structHash) + data := keccak256(ptr, 0x42) + } + } + + /** + * @dev Returns an Ethereum Signed Data with intended validator, created from a + * `validator` and `data` according to the version 0 of EIP-191. + * + * See {recover}. + */ + function toDataWithIntendedValidatorHash(address validator, bytes memory data) internal pure returns (bytes32) { + return keccak256(abi.encodePacked("\x19\x00", validator, data)); } } diff --git a/contracts/openzeppelin-presets/utils/cryptography/EIP712.sol b/contracts/external-deps/openzeppelin/utils/cryptography/EIP712.sol similarity index 100% rename from contracts/openzeppelin-presets/utils/cryptography/EIP712.sol rename to contracts/external-deps/openzeppelin/utils/cryptography/EIP712.sol diff --git a/contracts/external-deps/openzeppelin/utils/math/Math.sol b/contracts/external-deps/openzeppelin/utils/math/Math.sol new file mode 100644 index 000000000..291d257b0 --- /dev/null +++ b/contracts/external-deps/openzeppelin/utils/math/Math.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.5.0) (utils/math/Math.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Standard math utilities missing in the Solidity language. + */ +library Math { + /** + * @dev Returns the largest of two numbers. + */ + function max(uint256 a, uint256 b) internal pure returns (uint256) { + return a >= b ? a : b; + } + + /** + * @dev Returns the smallest of two numbers. + */ + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } + + /** + * @dev Returns the average of two numbers. The result is rounded towards + * zero. + */ + function average(uint256 a, uint256 b) internal pure returns (uint256) { + // (a + b) / 2 can overflow. + return (a & b) + (a ^ b) / 2; + } + + /** + * @dev Returns the ceiling of the division of two numbers. + * + * This differs from standard division with `/` in that it rounds up instead + * of rounding down. + */ + function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) { + // (a + b - 1) / b can overflow on addition, so we distribute. + return a / b + (a % b == 0 ? 0 : 1); + } +} diff --git a/contracts/external-deps/openzeppelin/utils/math/SafeCast.sol b/contracts/external-deps/openzeppelin/utils/math/SafeCast.sol new file mode 100644 index 000000000..3cd647357 --- /dev/null +++ b/contracts/external-deps/openzeppelin/utils/math/SafeCast.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (utils/math/SafeCast.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Wrappers over Solidity's uintXX/intXX casting operators with added overflow + * checks. + * + * Downcasting from uint256/int256 in Solidity does not revert on overflow. This can + * easily result in undesired exploitation or bugs, since developers usually + * assume that overflows raise errors. `SafeCast` restores this intuition by + * reverting the transaction when such an operation overflows. + * + * Using this library instead of the unchecked operations eliminates an entire + * class of bugs, so it's recommended to use it always. + * + * Can be combined with {SafeMath} and {SignedSafeMath} to extend it to smaller types, by performing + * all math on `uint256` and `int256` and then downcasting. + */ +library SafeCast { + /** + * @dev Returns the downcasted uint224 from uint256, reverting on + * overflow (when the input is greater than largest uint224). + * + * Counterpart to Solidity's `uint224` operator. + * + * Requirements: + * + * - input must fit into 224 bits + */ + function toUint224(uint256 value) internal pure returns (uint224) { + require(value <= type(uint224).max, "SafeCast: value doesn't fit in 224 bits"); + return uint224(value); + } + + /** + * @dev Returns the downcasted uint128 from uint256, reverting on + * overflow (when the input is greater than largest uint128). + * + * Counterpart to Solidity's `uint128` operator. + * + * Requirements: + * + * - input must fit into 128 bits + */ + function toUint128(uint256 value) internal pure returns (uint128) { + require(value <= type(uint128).max, "SafeCast: value doesn't fit in 128 bits"); + return uint128(value); + } + + /** + * @dev Returns the downcasted uint96 from uint256, reverting on + * overflow (when the input is greater than largest uint96). + * + * Counterpart to Solidity's `uint96` operator. + * + * Requirements: + * + * - input must fit into 96 bits + */ + function toUint96(uint256 value) internal pure returns (uint96) { + require(value <= type(uint96).max, "SafeCast: value doesn't fit in 96 bits"); + return uint96(value); + } + + /** + * @dev Returns the downcasted uint64 from uint256, reverting on + * overflow (when the input is greater than largest uint64). + * + * Counterpart to Solidity's `uint64` operator. + * + * Requirements: + * + * - input must fit into 64 bits + */ + function toUint64(uint256 value) internal pure returns (uint64) { + require(value <= type(uint64).max, "SafeCast: value doesn't fit in 64 bits"); + return uint64(value); + } + + /** + * @dev Returns the downcasted uint32 from uint256, reverting on + * overflow (when the input is greater than largest uint32). + * + * Counterpart to Solidity's `uint32` operator. + * + * Requirements: + * + * - input must fit into 32 bits + */ + function toUint32(uint256 value) internal pure returns (uint32) { + require(value <= type(uint32).max, "SafeCast: value doesn't fit in 32 bits"); + return uint32(value); + } + + /** + * @dev Returns the downcasted uint16 from uint256, reverting on + * overflow (when the input is greater than largest uint16). + * + * Counterpart to Solidity's `uint16` operator. + * + * Requirements: + * + * - input must fit into 16 bits + */ + function toUint16(uint256 value) internal pure returns (uint16) { + require(value <= type(uint16).max, "SafeCast: value doesn't fit in 16 bits"); + return uint16(value); + } + + /** + * @dev Returns the downcasted uint8 from uint256, reverting on + * overflow (when the input is greater than largest uint8). + * + * Counterpart to Solidity's `uint8` operator. + * + * Requirements: + * + * - input must fit into 8 bits. + */ + function toUint8(uint256 value) internal pure returns (uint8) { + require(value <= type(uint8).max, "SafeCast: value doesn't fit in 8 bits"); + return uint8(value); + } + + /** + * @dev Converts a signed int256 into an unsigned uint256. + * + * Requirements: + * + * - input must be greater than or equal to 0. + */ + function toUint256(int256 value) internal pure returns (uint256) { + require(value >= 0, "SafeCast: value must be positive"); + return uint256(value); + } + + /** + * @dev Returns the downcasted int128 from int256, reverting on + * overflow (when the input is less than smallest int128 or + * greater than largest int128). + * + * Counterpart to Solidity's `int128` operator. + * + * Requirements: + * + * - input must fit into 128 bits + * + * _Available since v3.1._ + */ + function toInt128(int256 value) internal pure returns (int128) { + require(value >= type(int128).min && value <= type(int128).max, "SafeCast: value doesn't fit in 128 bits"); + return int128(value); + } + + /** + * @dev Returns the downcasted int64 from int256, reverting on + * overflow (when the input is less than smallest int64 or + * greater than largest int64). + * + * Counterpart to Solidity's `int64` operator. + * + * Requirements: + * + * - input must fit into 64 bits + * + * _Available since v3.1._ + */ + function toInt64(int256 value) internal pure returns (int64) { + require(value >= type(int64).min && value <= type(int64).max, "SafeCast: value doesn't fit in 64 bits"); + return int64(value); + } + + /** + * @dev Returns the downcasted int32 from int256, reverting on + * overflow (when the input is less than smallest int32 or + * greater than largest int32). + * + * Counterpart to Solidity's `int32` operator. + * + * Requirements: + * + * - input must fit into 32 bits + * + * _Available since v3.1._ + */ + function toInt32(int256 value) internal pure returns (int32) { + require(value >= type(int32).min && value <= type(int32).max, "SafeCast: value doesn't fit in 32 bits"); + return int32(value); + } + + /** + * @dev Returns the downcasted int16 from int256, reverting on + * overflow (when the input is less than smallest int16 or + * greater than largest int16). + * + * Counterpart to Solidity's `int16` operator. + * + * Requirements: + * + * - input must fit into 16 bits + * + * _Available since v3.1._ + */ + function toInt16(int256 value) internal pure returns (int16) { + require(value >= type(int16).min && value <= type(int16).max, "SafeCast: value doesn't fit in 16 bits"); + return int16(value); + } + + /** + * @dev Returns the downcasted int8 from int256, reverting on + * overflow (when the input is less than smallest int8 or + * greater than largest int8). + * + * Counterpart to Solidity's `int8` operator. + * + * Requirements: + * + * - input must fit into 8 bits. + * + * _Available since v3.1._ + */ + function toInt8(int256 value) internal pure returns (int8) { + require(value >= type(int8).min && value <= type(int8).max, "SafeCast: value doesn't fit in 8 bits"); + return int8(value); + } + + /** + * @dev Converts an unsigned uint256 into a signed int256. + * + * Requirements: + * + * - input must be less than or equal to maxInt256. + */ + function toInt256(uint256 value) internal pure returns (int256) { + // Note: Unsafe cast below is okay because `type(int256).max` is guaranteed to be positive + require(value <= uint256(type(int256).max), "SafeCast: value doesn't fit in an int256"); + return int256(value); + } +} diff --git a/contracts/external-deps/openzeppelin/utils/math/SafeMath.sol b/contracts/external-deps/openzeppelin/utils/math/SafeMath.sol new file mode 100644 index 000000000..2f48fb736 --- /dev/null +++ b/contracts/external-deps/openzeppelin/utils/math/SafeMath.sol @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.6.0) (utils/math/SafeMath.sol) + +pragma solidity ^0.8.0; + +// CAUTION +// This version of SafeMath should only be used with Solidity 0.8 or later, +// because it relies on the compiler's built in overflow checks. + +/** + * @dev Wrappers over Solidity's arithmetic operations. + * + * NOTE: `SafeMath` is generally not needed starting with Solidity 0.8, since the compiler + * now has built in overflow checking. + */ +library SafeMath { + /** + * @dev Returns the addition of two unsigned integers, with an overflow flag. + * + * _Available since v3.4._ + */ + function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + uint256 c = a + b; + if (c < a) return (false, 0); + return (true, c); + } + } + + /** + * @dev Returns the subtraction of two unsigned integers, with an overflow flag. + * + * _Available since v3.4._ + */ + function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b > a) return (false, 0); + return (true, a - b); + } + } + + /** + * @dev Returns the multiplication of two unsigned integers, with an overflow flag. + * + * _Available since v3.4._ + */ + function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) return (true, 0); + uint256 c = a * b; + if (c / a != b) return (false, 0); + return (true, c); + } + } + + /** + * @dev Returns the division of two unsigned integers, with a division by zero flag. + * + * _Available since v3.4._ + */ + function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b == 0) return (false, 0); + return (true, a / b); + } + } + + /** + * @dev Returns the remainder of dividing two unsigned integers, with a division by zero flag. + * + * _Available since v3.4._ + */ + function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b == 0) return (false, 0); + return (true, a % b); + } + } + + /** + * @dev Returns the addition of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `+` operator. + * + * Requirements: + * + * - Addition cannot overflow. + */ + function add(uint256 a, uint256 b) internal pure returns (uint256) { + return a + b; + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting on + * overflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * + * - Subtraction cannot overflow. + */ + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + return a - b; + } + + /** + * @dev Returns the multiplication of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `*` operator. + * + * Requirements: + * + * - Multiplication cannot overflow. + */ + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + return a * b; + } + + /** + * @dev Returns the integer division of two unsigned integers, reverting on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function div(uint256 a, uint256 b) internal pure returns (uint256) { + return a / b; + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * reverting when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function mod(uint256 a, uint256 b) internal pure returns (uint256) { + return a % b; + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting with custom message on + * overflow (when the result is negative). + * + * CAUTION: This function is deprecated because it requires allocating memory for the error + * message unnecessarily. For custom revert reasons use {trySub}. + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * + * - Subtraction cannot overflow. + */ + function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { + unchecked { + require(b <= a, errorMessage); + return a - b; + } + } + + /** + * @dev Returns the integer division of two unsigned integers, reverting with custom message on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { + unchecked { + require(b > 0, errorMessage); + return a / b; + } + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * reverting with custom message when dividing by zero. + * + * CAUTION: This function is deprecated because it requires allocating memory for the error + * message unnecessarily. For custom revert reasons use {tryMod}. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { + unchecked { + require(b > 0, errorMessage); + return a % b; + } + } +} diff --git a/contracts/external-deps/openzeppelin/utils/structs/EnumerableSet.sol b/contracts/external-deps/openzeppelin/utils/structs/EnumerableSet.sol new file mode 100644 index 000000000..b6c647f07 --- /dev/null +++ b/contracts/external-deps/openzeppelin/utils/structs/EnumerableSet.sol @@ -0,0 +1,367 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (utils/structs/EnumerableSet.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Library for managing + * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive + * types. + * + * Sets have the following properties: + * + * - Elements are added, removed, and checked for existence in constant time + * (O(1)). + * - Elements are enumerated in O(n). No guarantees are made on the ordering. + * + * ``` + * contract Example { + * // Add the library methods + * using EnumerableSet for EnumerableSet.AddressSet; + * + * // Declare a set state variable + * EnumerableSet.AddressSet private mySet; + * } + * ``` + * + * As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`) + * and `uint256` (`UintSet`) are supported. + * + * [WARNING] + * ==== + * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure unusable. + * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info. + * + * In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an array of EnumerableSet. + * ==== + */ +library EnumerableSet { + // To implement this library for multiple types with as little code + // repetition as possible, we write it in terms of a generic Set type with + // bytes32 values. + // The Set implementation uses private functions, and user-facing + // implementations (such as AddressSet) are just wrappers around the + // underlying Set. + // This means that we can only create new EnumerableSets for types that fit + // in bytes32. + + struct Set { + // Storage of set values + bytes32[] _values; + // Position of the value in the `values` array, plus 1 because index 0 + // means a value is not in the set. + mapping(bytes32 => uint256) _indexes; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function _add(Set storage set, bytes32 value) private returns (bool) { + if (!_contains(set, value)) { + set._values.push(value); + // The value is stored at length-1, but we add 1 to all indexes + // and use 0 as a sentinel value + set._indexes[value] = set._values.length; + return true; + } else { + return false; + } + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function _remove(Set storage set, bytes32 value) private returns (bool) { + // We read and store the value's index to prevent multiple reads from the same storage slot + uint256 valueIndex = set._indexes[value]; + + if (valueIndex != 0) { + // Equivalent to contains(set, value) + // To delete an element from the _values array in O(1), we swap the element to delete with the last one in + // the array, and then remove the last element (sometimes called as 'swap and pop'). + // This modifies the order of the array, as noted in {at}. + + uint256 toDeleteIndex = valueIndex - 1; + uint256 lastIndex = set._values.length - 1; + + if (lastIndex != toDeleteIndex) { + bytes32 lastValue = set._values[lastIndex]; + + // Move the last value to the index where the value to delete is + set._values[toDeleteIndex] = lastValue; + // Update the index for the moved value + set._indexes[lastValue] = valueIndex; // Replace lastValue's index to valueIndex + } + + // Delete the slot where the moved value was stored + set._values.pop(); + + // Delete the index for the deleted slot + delete set._indexes[value]; + + return true; + } else { + return false; + } + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function _contains(Set storage set, bytes32 value) private view returns (bool) { + return set._indexes[value] != 0; + } + + /** + * @dev Returns the number of values on the set. O(1). + */ + function _length(Set storage set) private view returns (uint256) { + return set._values.length; + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function _at(Set storage set, uint256 index) private view returns (bytes32) { + return set._values[index]; + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function _values(Set storage set) private view returns (bytes32[] memory) { + return set._values; + } + + // Bytes32Set + + struct Bytes32Set { + Set _inner; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(Bytes32Set storage set, bytes32 value) internal returns (bool) { + return _add(set._inner, value); + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) { + return _remove(set._inner, value); + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) { + return _contains(set._inner, value); + } + + /** + * @dev Returns the number of values in the set. O(1). + */ + function length(Bytes32Set storage set) internal view returns (uint256) { + return _length(set._inner); + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) { + return _at(set._inner, index); + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function values(Bytes32Set storage set) internal view returns (bytes32[] memory) { + return _values(set._inner); + } + + // AddressSet + + struct AddressSet { + Set _inner; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(AddressSet storage set, address value) internal returns (bool) { + return _add(set._inner, bytes32(uint256(uint160(value)))); + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(AddressSet storage set, address value) internal returns (bool) { + return _remove(set._inner, bytes32(uint256(uint160(value)))); + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(AddressSet storage set, address value) internal view returns (bool) { + return _contains(set._inner, bytes32(uint256(uint160(value)))); + } + + /** + * @dev Returns the number of values in the set. O(1). + */ + function length(AddressSet storage set) internal view returns (uint256) { + return _length(set._inner); + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(AddressSet storage set, uint256 index) internal view returns (address) { + return address(uint160(uint256(_at(set._inner, index)))); + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function values(AddressSet storage set) internal view returns (address[] memory) { + bytes32[] memory store = _values(set._inner); + address[] memory result; + + /// @solidity memory-safe-assembly + assembly { + result := store + } + + return result; + } + + // UintSet + + struct UintSet { + Set _inner; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(UintSet storage set, uint256 value) internal returns (bool) { + return _add(set._inner, bytes32(value)); + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(UintSet storage set, uint256 value) internal returns (bool) { + return _remove(set._inner, bytes32(value)); + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(UintSet storage set, uint256 value) internal view returns (bool) { + return _contains(set._inner, bytes32(value)); + } + + /** + * @dev Returns the number of values on the set. O(1). + */ + function length(UintSet storage set) internal view returns (uint256) { + return _length(set._inner); + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(UintSet storage set, uint256 index) internal view returns (uint256) { + return uint256(_at(set._inner, index)); + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function values(UintSet storage set) internal view returns (uint256[] memory) { + bytes32[] memory store = _values(set._inner); + uint256[] memory result; + + /// @solidity memory-safe-assembly + assembly { + result := store + } + + return result; + } +} diff --git a/contracts/infra/ContractPublisher.sol b/contracts/infra/ContractPublisher.sol new file mode 100644 index 000000000..979a6aab6 --- /dev/null +++ b/contracts/infra/ContractPublisher.sol @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// ========== External imports ========== +import "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import "../external-deps/openzeppelin/metatx/ERC2771Context.sol"; +import "../extension/Multicall.sol"; + +// ========== Internal imports ========== +import { IContractPublisher } from "./interface/IContractPublisher.sol"; + +contract ContractPublisher is IContractPublisher, ERC2771Context, AccessControlEnumerable, Multicall { + using EnumerableSet for EnumerableSet.Bytes32Set; + + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + /// @notice Whether the contract publisher is paused. + bool public isPaused; + IContractPublisher public prevPublisher; + + /// @dev Only MIGRATION holders can override previous publisher or migrate data + bytes32 private constant MIGRATION_ROLE = keccak256("MIGRATION_ROLE"); + + /*/////////////////////////////////////////////////////////////// + Mappings + //////////////////////////////////////////////////////////////*/ + + /// @dev Mapping from publisher address => set of published contracts. + mapping(address => CustomContractSet) private contractsOfPublisher; + /// @dev Mapping publisher address => profile uri + mapping(address => string) private profileUriOfPublisher; + /// @dev Mapping compilerMetadataUri => publishedMetadataUri + mapping(string => PublishedMetadataSet) private compilerMetadataUriToPublishedMetadataUris; + + /*/////////////////////////////////////////////////////////////// + Constructor + modifiers + //////////////////////////////////////////////////////////////*/ + + /// @dev Checks whether caller is publisher TODO enable external approvals + modifier onlyPublisher(address _publisher) { + require(_msgSender() == _publisher, "unapproved caller"); + + _; + } + + /// @dev Checks whether contract is unpaused or the caller is a contract admin. + modifier onlyUnpausedOrAdmin() { + require(!isPaused || hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "registry paused"); + + _; + } + + constructor( + address _defaultAdmin, + address[] memory _trustedForwarders, + IContractPublisher _prevPublisher + ) ERC2771Context(_trustedForwarders) { + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + _setupRole(MIGRATION_ROLE, _defaultAdmin); + _setRoleAdmin(MIGRATION_ROLE, MIGRATION_ROLE); + + prevPublisher = _prevPublisher; + } + + /*/////////////////////////////////////////////////////////////// + Getter logic + //////////////////////////////////////////////////////////////*/ + + /// @notice Returns the latest version of all contracts published by a publisher. + function getAllPublishedContracts( + address _publisher + ) external view returns (CustomContractInstance[] memory published) { + CustomContractInstance[] memory linkedData; + if (address(prevPublisher) != address(0)) { + linkedData = prevPublisher.getAllPublishedContracts(_publisher); + } + uint256 currentTotal = EnumerableSet.length(contractsOfPublisher[_publisher].contractIds); + uint256 prevTotal = linkedData.length; + uint256 total = prevTotal + currentTotal; + published = new CustomContractInstance[](total); + // fill in previously published contracts + for (uint256 i = 0; i < prevTotal; i += 1) { + published[i] = linkedData[i]; + } + // fill in current published contracts + for (uint256 i = 0; i < currentTotal; i += 1) { + bytes32 contractId = EnumerableSet.at(contractsOfPublisher[_publisher].contractIds, i); + published[i + prevTotal] = contractsOfPublisher[_publisher].contracts[contractId].latest; + } + } + + /// @notice Returns all versions of a published contract. + function getPublishedContractVersions( + address _publisher, + string memory _contractId + ) external view returns (CustomContractInstance[] memory published) { + CustomContractInstance[] memory linkedVersions; + + if (address(prevPublisher) != address(0)) { + linkedVersions = prevPublisher.getPublishedContractVersions(_publisher, _contractId); + } + uint256 prevTotal = linkedVersions.length; + + bytes32 id = keccak256(bytes(_contractId)); + uint256 currentTotal = contractsOfPublisher[_publisher].contracts[id].total; + uint256 total = prevTotal + currentTotal; + + published = new CustomContractInstance[](total); + + // fill in previously published contracts + for (uint256 i = 0; i < prevTotal; i += 1) { + published[i] = linkedVersions[i]; + } + // fill in current published contracts + for (uint256 i = 0; i < currentTotal; i += 1) { + published[i + prevTotal] = contractsOfPublisher[_publisher].contracts[id].instances[i]; + } + } + + /// @notice Returns the latest version of a contract published by a publisher. + function getPublishedContract( + address _publisher, + string memory _contractId + ) external view returns (CustomContractInstance memory published) { + published = contractsOfPublisher[_publisher].contracts[keccak256(bytes(_contractId))].latest; + // if not found, check the previous publisher + if (address(prevPublisher) != address(0) && published.publishTimestamp == 0) { + published = prevPublisher.getPublishedContract(_publisher, _contractId); + } + } + + /*/////////////////////////////////////////////////////////////// + Publish logic + //////////////////////////////////////////////////////////////*/ + + /// @notice Let's an account publish a contract. + function publishContract( + address _publisher, + string memory _contractId, + string memory _publishMetadataUri, + string memory _compilerMetadataUri, + bytes32 _bytecodeHash, + address _implementation + ) external onlyPublisher(_publisher) onlyUnpausedOrAdmin { + CustomContractInstance memory publishedContract = CustomContractInstance({ + contractId: _contractId, + publishTimestamp: block.timestamp, + publishMetadataUri: _publishMetadataUri, + bytecodeHash: _bytecodeHash, + implementation: _implementation + }); + + bytes32 contractIdInBytes = keccak256(bytes(_contractId)); + EnumerableSet.add(contractsOfPublisher[_publisher].contractIds, contractIdInBytes); + + contractsOfPublisher[_publisher].contracts[contractIdInBytes].latest = publishedContract; + + uint256 index = contractsOfPublisher[_publisher].contracts[contractIdInBytes].total; + contractsOfPublisher[_publisher].contracts[contractIdInBytes].total += 1; + contractsOfPublisher[_publisher].contracts[contractIdInBytes].instances[index] = publishedContract; + + uint256 metadataIndex = compilerMetadataUriToPublishedMetadataUris[_compilerMetadataUri].index; + compilerMetadataUriToPublishedMetadataUris[_compilerMetadataUri].uris[metadataIndex] = _publishMetadataUri; + compilerMetadataUriToPublishedMetadataUris[_compilerMetadataUri].index = metadataIndex + 1; + + emit ContractPublished(_msgSender(), _publisher, publishedContract); + } + + /// @notice Lets a publisher unpublish a contract and all its versions. + function unpublishContract( + address _publisher, + string memory _contractId + ) external onlyPublisher(_publisher) onlyUnpausedOrAdmin { + bytes32 contractIdInBytes = keccak256(bytes(_contractId)); + + bool removed = EnumerableSet.remove(contractsOfPublisher[_publisher].contractIds, contractIdInBytes); + require(removed, "given contractId DNE"); + + delete contractsOfPublisher[_publisher].contracts[contractIdInBytes]; + + emit ContractUnpublished(_msgSender(), _publisher, _contractId); + } + + function setPrevPublisher(IContractPublisher _prevPublisher) external { + require(hasRole(MIGRATION_ROLE, _msgSender()), "Not authorized"); + prevPublisher = _prevPublisher; + } + + /// @notice Lets an account set its own publisher profile uri + function setPublisherProfileUri(address publisher, string memory uri) public { + require( + (!isPaused && _msgSender() == publisher) || hasRole(MIGRATION_ROLE, _msgSender()), + "Registry paused or caller not authorized" + ); + string memory currentURI = profileUriOfPublisher[publisher]; + profileUriOfPublisher[publisher] = uri; + + emit PublisherProfileUpdated(publisher, currentURI, uri); + } + + // @notice Get a publisher profile uri + function getPublisherProfileUri(address publisher) public view returns (string memory uri) { + uri = profileUriOfPublisher[publisher]; + // if not found, check the previous publisher + if (address(prevPublisher) != address(0) && bytes(uri).length == 0) { + uri = prevPublisher.getPublisherProfileUri(publisher); + } + } + + /// @notice Retrieve the published metadata URI from a compiler metadata URI + function getPublishedUriFromCompilerUri( + string memory compilerMetadataUri + ) public view returns (string[] memory publishedMetadataUris) { + string[] memory linkedUris; + if (address(prevPublisher) != address(0)) { + linkedUris = prevPublisher.getPublishedUriFromCompilerUri(compilerMetadataUri); + } + uint256 prevTotal = linkedUris.length; + uint256 currentTotal = compilerMetadataUriToPublishedMetadataUris[compilerMetadataUri].index; + uint256 total = prevTotal + currentTotal; + publishedMetadataUris = new string[](total); + // fill in previously published uris + for (uint256 i = 0; i < prevTotal; i += 1) { + publishedMetadataUris[i] = linkedUris[i]; + } + // fill in current published uris + for (uint256 i = 0; i < currentTotal; i += 1) { + publishedMetadataUris[i + prevTotal] = compilerMetadataUriToPublishedMetadataUris[compilerMetadataUri].uris[ + i + ]; + } + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + /// @dev Lets a contract admin pause the registry. + function setPause(bool _pause) external { + require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "unapproved caller"); + isPaused = _pause; + emit Paused(_pause); + } + + /// @dev ERC2771Context overrides + function _msgSender() internal view virtual override(Context, ERC2771Context, Multicall) returns (address sender) { + return ERC2771Context._msgSender(); + } + + /// @dev ERC2771Context overrides + function _msgData() internal view virtual override(Context, ERC2771Context) returns (bytes calldata) { + return ERC2771Context._msgData(); + } +} diff --git a/contracts/infra/TWFactory.sol b/contracts/infra/TWFactory.sol new file mode 100644 index 000000000..ea98b73f1 --- /dev/null +++ b/contracts/infra/TWFactory.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +import { TWRegistry } from "./TWRegistry.sol"; +import "./interface/IThirdwebContract.sol"; +import "../extension/interface/IContractFactory.sol"; + +import { AccessControlEnumerable, Context } from "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; +import { ERC2771Context } from "../external-deps/openzeppelin/metatx/ERC2771Context.sol"; +import { Create2 } from "@openzeppelin/contracts/utils/Create2.sol"; +import { Multicall } from "../extension/Multicall.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; +import { Clones } from "@openzeppelin/contracts/proxy/Clones.sol"; + +contract TWFactory is Multicall, ERC2771Context, AccessControlEnumerable, IContractFactory { + /// @dev Only FACTORY_ROLE holders can approve/unapprove implementations for proxies to point to. + bytes32 public constant FACTORY_ROLE = keccak256("FACTORY_ROLE"); + + TWRegistry public immutable registry; + + /// @dev Emitted when a proxy is deployed. + event ProxyDeployed(address indexed implementation, address proxy, address indexed deployer); + event ImplementationAdded(address implementation, bytes32 indexed contractType, uint256 version); + event ImplementationApproved(address implementation, bool isApproved); + + /// @dev mapping of implementation address to deployment approval + mapping(address => bool) public approval; + + /// @dev mapping of implementation address to implementation added version + mapping(bytes32 => uint256) public currentVersion; + + /// @dev mapping of contract type to module version to implementation address + mapping(bytes32 => mapping(uint256 => address)) public implementation; + + /// @dev mapping of proxy address to deployer address + mapping(address => address) public deployer; + + constructor(address[] memory _trustedForwarders, address _registry) ERC2771Context(_trustedForwarders) { + _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); + _setupRole(FACTORY_ROLE, _msgSender()); + + registry = TWRegistry(_registry); + } + + /// @dev Deploys a proxy that points to the latest version of the given contract type. + function deployProxy(bytes32 _type, bytes memory _data) external returns (address) { + bytes32 salt = bytes32(registry.count(_msgSender())); + return deployProxyDeterministic(_type, _data, salt); + } + + /** + * @dev Deploys a proxy at a deterministic address by taking in `salt` as a parameter. + * Proxy points to the latest version of the given contract type. + */ + function deployProxyDeterministic(bytes32 _type, bytes memory _data, bytes32 _salt) public returns (address) { + address _implementation = implementation[_type][currentVersion[_type]]; + return deployProxyByImplementation(_implementation, _data, _salt); + } + + /// @dev Deploys a proxy that points to the given implementation. + function deployProxyByImplementation( + address _implementation, + bytes memory _data, + bytes32 _salt + ) public override returns (address deployedProxy) { + require(approval[_implementation], "implementation not approved"); + + bytes32 salthash = keccak256(abi.encodePacked(_msgSender(), _salt)); + deployedProxy = Clones.cloneDeterministic(_implementation, salthash); + + deployer[deployedProxy] = _msgSender(); + + emit ProxyDeployed(_implementation, deployedProxy, _msgSender()); + + registry.add(_msgSender(), deployedProxy); + + if (_data.length > 0) { + // slither-disable-next-line unused-return + Address.functionCall(deployedProxy, _data); + } + } + + /// @dev Lets a contract admin set the address of a contract type x version. + function addImplementation(address _implementation) external { + require(hasRole(FACTORY_ROLE, _msgSender()), "not admin."); + + IThirdwebContract module = IThirdwebContract(_implementation); + + bytes32 ctype = module.contractType(); + require(ctype.length > 0, "invalid module"); + + uint8 version = module.contractVersion(); + uint8 currentVersionOfType = uint8(currentVersion[ctype]); + require(version >= currentVersionOfType, "wrong module version"); + + currentVersion[ctype] = version; + implementation[ctype][version] = _implementation; + approval[_implementation] = true; + + emit ImplementationAdded(_implementation, ctype, version); + } + + /// @dev Lets a contract admin approve a specific contract for deployment. + function approveImplementation(address _implementation, bool _toApprove) external { + require(hasRole(FACTORY_ROLE, _msgSender()), "not admin."); + + approval[_implementation] = _toApprove; + + emit ImplementationApproved(_implementation, _toApprove); + } + + /// @dev Returns the implementation given a contract type and version. + function getImplementation(bytes32 _type, uint256 _version) external view returns (address) { + return implementation[_type][_version]; + } + + /// @dev Returns the latest implementation given a contract type. + function getLatestImplementation(bytes32 _type) external view returns (address) { + return implementation[_type][currentVersion[_type]]; + } + + function _msgSender() internal view virtual override(Context, ERC2771Context, Multicall) returns (address sender) { + return ERC2771Context._msgSender(); + } + + function _msgData() internal view virtual override(Context, ERC2771Context) returns (bytes calldata) { + return ERC2771Context._msgData(); + } +} diff --git a/contracts/TWFee.sol b/contracts/infra/TWFee.sol similarity index 89% rename from contracts/TWFee.sol rename to contracts/infra/TWFee.sol index cd158c16a..2b008a334 100644 --- a/contracts/TWFee.sol +++ b/contracts/infra/TWFee.sol @@ -1,19 +1,21 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.11; +/// @author thirdweb + import "./TWFactory.sol"; -import "./interfaces/ITWFee.sol"; +import "./interface/ITWFee.sol"; import "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; -import "@openzeppelin/contracts/utils/Multicall.sol"; -import "@openzeppelin/contracts/metatx/ERC2771Context.sol"; +import { Multicall } from "../extension/Multicall.sol"; +import "../external-deps/openzeppelin/metatx/ERC2771Context.sol"; interface IFeeTierPlacementExtension { /// @dev Returns the fee tier for a given proxy contract address and proxy deployer address. - function getFeeTier(address deployer, address proxy) - external - view - returns (uint128 tierId, uint128 validUntilTimestamp); + function getFeeTier( + address deployer, + address proxy + ) external view returns (uint128 tierId, uint128 validUntilTimestamp); } contract TWFee is ITWFee, Multicall, ERC2771Context, AccessControlEnumerable, IFeeTierPlacementExtension { @@ -53,7 +55,7 @@ contract TWFee is ITWFee, Multicall, ERC2771Context, AccessControlEnumerable, IF event TierUpdated(address indexed proxyOrDeployer, uint256 tierId, uint256 validUntilTimestamp); event FeeTierUpdated(uint256 indexed tierId, uint256 indexed feeType, address recipient, uint256 bps); - constructor(address _trustedForwarder, address _factory) ERC2771Context(_trustedForwarder) { + constructor(address[] memory _trustedForwarders, address _factory) ERC2771Context(_trustedForwarders) { factory = TWFactory(_factory); _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); @@ -66,12 +68,10 @@ contract TWFee is ITWFee, Multicall, ERC2771Context, AccessControlEnumerable, IF } /// @dev Returns the fee tier for a proxy deployer wallet or contract address. - function getFeeTier(address _deployer, address _proxy) - public - view - override - returns (uint128 tierId, uint128 validUntilTimestamp) - { + function getFeeTier( + address _deployer, + address _proxy + ) public view override returns (uint128 tierId, uint128 validUntilTimestamp) { Tier memory targetTier = tier[_proxy]; if (block.timestamp <= targetTier.validUntilTimestamp) { tierId = targetTier.id; @@ -152,7 +152,7 @@ contract TWFee is ITWFee, Multicall, ERC2771Context, AccessControlEnumerable, IF // ===== Getters ===== - function _msgSender() internal view virtual override(Context, ERC2771Context) returns (address sender) { + function _msgSender() internal view virtual override(Context, ERC2771Context, Multicall) returns (address sender) { return ERC2771Context._msgSender(); } diff --git a/contracts/infra/TWMinimalFactory.sol b/contracts/infra/TWMinimalFactory.sol new file mode 100644 index 000000000..63b81b9ed --- /dev/null +++ b/contracts/infra/TWMinimalFactory.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/proxy/Clones.sol"; + +contract TWMinimalFactory { + /// @dev Deploys a proxy that points to the given implementation. + constructor(address _implementation, bytes memory _data, bytes32 _salt) payable { + address instance; + bytes32 salthash = keccak256(abi.encodePacked(msg.sender, _salt)); + assembly { + let ptr := mload(0x40) + mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000) + mstore(add(ptr, 0x14), shl(0x60, _implementation)) + mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000) + instance := create2(0, ptr, 0x37, salthash) + } + + if (_data.length > 0) { + // instance.call{ value: msg.value }(_data); + + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory result) = instance.call{ value: msg.value }(_data); + + if (!success) { + // Next 5 lines from https://ethereum.stackexchange.com/a/83577 + if (result.length < 68) revert("Transaction reverted silently"); + assembly { + result := add(result, 0x04) + } + revert(abi.decode(result, (string))); + } + } + } +} diff --git a/contracts/infra/TWMultichainRegistry.sol b/contracts/infra/TWMultichainRegistry.sol new file mode 100644 index 000000000..0aadf61dc --- /dev/null +++ b/contracts/infra/TWMultichainRegistry.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; +import "../extension/Multicall.sol"; +import "../external-deps/openzeppelin/metatx/ERC2771Context.sol"; + +import "./interface/ITWMultichainRegistry.sol"; + +contract TWMultichainRegistry is ITWMultichainRegistry, Multicall, ERC2771Context, AccessControlEnumerable { + bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); + + using EnumerableSet for EnumerableSet.AddressSet; + using EnumerableSet for EnumerableSet.UintSet; + + /// @dev wallet address => [contract addresses] + mapping(address => mapping(uint256 => EnumerableSet.AddressSet)) private deployments; + /// @dev contract address deployed => imported metadata uri + mapping(uint256 => mapping(address => string)) private addressToMetadataUri; + + EnumerableSet.UintSet private chainIds; + + constructor(address[] memory _trustedForwarders) ERC2771Context(_trustedForwarders) { + _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + // slither-disable-next-line similar-names + function add(address _deployer, address _deployment, uint256 _chainId, string memory metadataUri) external { + require(hasRole(OPERATOR_ROLE, _msgSender()) || _deployer == _msgSender(), "not operator or deployer."); + + bool added = deployments[_deployer][_chainId].add(_deployment); + require(added, "failed to add"); + + chainIds.add(_chainId); + + if (bytes(metadataUri).length > 0) { + addressToMetadataUri[_chainId][_deployment] = metadataUri; + } + + emit Added(_deployer, _deployment, _chainId, metadataUri); + } + + // slither-disable-next-line similar-names + function remove(address _deployer, address _deployment, uint256 _chainId) external { + require(hasRole(OPERATOR_ROLE, _msgSender()) || _deployer == _msgSender(), "not operator or deployer."); + + bool removed = deployments[_deployer][_chainId].remove(_deployment); + require(removed, "failed to remove"); + + emit Deleted(_deployer, _deployment, _chainId); + } + + function getAll(address _deployer) external view returns (Deployment[] memory allDeployments) { + uint256 totalDeployments; + uint256 chainIdsLen = chainIds.length(); + + for (uint256 i = 0; i < chainIdsLen; i += 1) { + uint256 chainId = chainIds.at(i); + + totalDeployments += deployments[_deployer][chainId].length(); + } + + allDeployments = new Deployment[](totalDeployments); + uint256 idx; + + for (uint256 j = 0; j < chainIdsLen; j += 1) { + uint256 chainId = chainIds.at(j); + + uint256 len = deployments[_deployer][chainId].length(); + address[] memory deploymentAddrs = deployments[_deployer][chainId].values(); + + for (uint256 k = 0; k < len; k += 1) { + allDeployments[idx] = Deployment({ + deploymentAddress: deploymentAddrs[k], + chainId: chainId, + metadataURI: addressToMetadataUri[chainId][deploymentAddrs[k]] + }); + idx += 1; + } + } + } + + function count(address _deployer) external view returns (uint256 deploymentCount) { + uint256 chainIdsLen = chainIds.length(); + + for (uint256 i = 0; i < chainIdsLen; i += 1) { + uint256 chainId = chainIds.at(i); + + deploymentCount += deployments[_deployer][chainId].length(); + } + } + + function getMetadataUri(uint256 _chainId, address _deployment) external view returns (string memory metadataUri) { + metadataUri = addressToMetadataUri[_chainId][_deployment]; + } + + function _msgSender() internal view virtual override(Context, ERC2771Context, Multicall) returns (address sender) { + return ERC2771Context._msgSender(); + } + + function _msgData() internal view virtual override(Context, ERC2771Context) returns (bytes calldata) { + return ERC2771Context._msgData(); + } +} diff --git a/contracts/infra/TWProxy.sol b/contracts/infra/TWProxy.sol new file mode 100644 index 000000000..cf0dcb9d4 --- /dev/null +++ b/contracts/infra/TWProxy.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +import "@openzeppelin/contracts/proxy/Proxy.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/utils/StorageSlot.sol"; + +contract TWProxy is Proxy { + /** + * @dev Storage slot with the address of the current implementation. + * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is + * validated in the constructor. + */ + bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + constructor(address _logic, bytes memory _data) payable { + assert(_IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)); + StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = _logic; + if (_data.length > 0) { + // slither-disable-next-line unused-return + Address.functionDelegateCall(_logic, _data); + } + } + + /** + * @dev Returns the current implementation address. + */ + function _implementation() internal view override returns (address impl) { + return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; + } +} diff --git a/contracts/infra/TWRegistry.sol b/contracts/infra/TWRegistry.sol new file mode 100644 index 000000000..ff4d46f30 --- /dev/null +++ b/contracts/infra/TWRegistry.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; +import "../extension/Multicall.sol"; +import "../external-deps/openzeppelin/metatx/ERC2771Context.sol"; + +contract TWRegistry is Multicall, ERC2771Context, AccessControlEnumerable { + bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); + + using EnumerableSet for EnumerableSet.AddressSet; + + /// @dev wallet address => [contract addresses] + mapping(address => EnumerableSet.AddressSet) private deployments; + + event Added(address indexed deployer, address indexed deployment); + event Deleted(address indexed deployer, address indexed deployment); + + constructor(address[] memory _trustedForwarders) ERC2771Context(_trustedForwarders) { + _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + // slither-disable-next-line similar-names + function add(address _deployer, address _deployment) external { + require(hasRole(OPERATOR_ROLE, _msgSender()) || _deployer == _msgSender(), "not operator or deployer."); + + bool added = deployments[_deployer].add(_deployment); + require(added, "failed to add"); + + emit Added(_deployer, _deployment); + } + + // slither-disable-next-line similar-names + function remove(address _deployer, address _deployment) external { + require(hasRole(OPERATOR_ROLE, _msgSender()) || _deployer == _msgSender(), "not operator or deployer."); + + bool removed = deployments[_deployer].remove(_deployment); + require(removed, "failed to remove"); + + emit Deleted(_deployer, _deployment); + } + + function getAll(address _deployer) external view returns (address[] memory) { + return deployments[_deployer].values(); + } + + function count(address _deployer) external view returns (uint256) { + return deployments[_deployer].length(); + } + + function _msgSender() internal view virtual override(Context, ERC2771Context, Multicall) returns (address sender) { + return ERC2771Context._msgSender(); + } + + function _msgData() internal view virtual override(Context, ERC2771Context) returns (bytes calldata) { + return ERC2771Context._msgData(); + } +} diff --git a/contracts/infra/TWStatelessFactory.sol b/contracts/infra/TWStatelessFactory.sol new file mode 100644 index 000000000..7e6edb11a --- /dev/null +++ b/contracts/infra/TWStatelessFactory.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +import "../extension/interface/IContractFactory.sol"; + +import "../external-deps/openzeppelin/metatx/ERC2771Context.sol"; +import "../extension/Multicall.sol"; +import "@openzeppelin/contracts/proxy/Clones.sol"; + +contract TWStatelessFactory is Multicall, ERC2771Context, IContractFactory { + /// @dev Emitted when a proxy is deployed. + event ProxyDeployed(address indexed implementation, address proxy, address indexed deployer); + + constructor(address[] memory _trustedForwarders) ERC2771Context(_trustedForwarders) {} + + /// @dev Deploys a proxy that points to the given implementation. + function deployProxyByImplementation( + address _implementation, + bytes memory _data, + bytes32 _salt + ) public override returns (address deployedProxy) { + bytes32 salthash = keccak256(abi.encodePacked(_msgSender(), _salt)); + deployedProxy = Clones.cloneDeterministic(_implementation, salthash); + + emit ProxyDeployed(_implementation, deployedProxy, _msgSender()); + + if (_data.length > 0) { + // slither-disable-next-line unused-return + Address.functionCall(deployedProxy, _data); + } + } + + function _msgSender() internal view virtual override(Multicall, ERC2771Context) returns (address sender) { + return ERC2771Context._msgSender(); + } +} diff --git a/contracts/infra/forwarder/Forwarder.sol b/contracts/infra/forwarder/Forwarder.sol new file mode 100644 index 000000000..ed680c92e --- /dev/null +++ b/contracts/infra/forwarder/Forwarder.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol"; + +/* + * @dev Minimal forwarder for GSNv2 + */ +contract Forwarder is EIP712 { + using ECDSA for bytes32; + + struct ForwardRequest { + address from; + address to; + uint256 value; + uint256 gas; + uint256 nonce; + bytes data; + } + + bytes32 private constant TYPEHASH = + keccak256("ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data)"); + + mapping(address => uint256) private _nonces; + + constructor() EIP712("GSNv2 Forwarder", "0.0.1") {} + + function getNonce(address from) public view returns (uint256) { + return _nonces[from]; + } + + function verify(ForwardRequest calldata req, bytes calldata signature) public view returns (bool) { + address signer = _hashTypedDataV4( + keccak256(abi.encode(TYPEHASH, req.from, req.to, req.value, req.gas, req.nonce, keccak256(req.data))) + ).recover(signature); + + return _nonces[req.from] == req.nonce && signer == req.from; + } + + function execute( + ForwardRequest calldata req, + bytes calldata signature + ) public payable returns (bool, bytes memory) { + require(verify(req, signature), "MinimalForwarder: signature does not match request"); + _nonces[req.from] = req.nonce + 1; + + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory result) = req.to.call{ gas: req.gas, value: req.value }( + abi.encodePacked(req.data, req.from) + ); + + if (!success) { + // Next 5 lines from https://ethereum.stackexchange.com/a/83577 + if (result.length < 68) revert("Transaction reverted silently"); + assembly { + result := add(result, 0x04) + } + revert(abi.decode(result, (string))); + } + // Check gas: https://ronan.eth.link/blog/ethereum-gas-dangers/ + assert(gasleft() > req.gas / 63); + return (success, result); + } +} diff --git a/contracts/infra/forwarder/ForwarderChainlessDomain.sol b/contracts/infra/forwarder/ForwarderChainlessDomain.sol new file mode 100644 index 000000000..5b5f4b34f --- /dev/null +++ b/contracts/infra/forwarder/ForwarderChainlessDomain.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "../../external-deps/openzeppelin/cryptography/EIP712ChainlessDomain.sol"; + +/** + * @dev Simple minimal forwarder to be used together with an ERC2771 compatible contract. See {ERC2771Context}. + */ +contract ForwarderChainlessDomain is EIP712ChainlessDomain { + using ECDSA for bytes32; + + struct ForwardRequest { + address from; + address to; + uint256 value; + uint256 gas; + uint256 nonce; + bytes data; + uint256 chainid; + } + + bytes32 private constant _TYPEHASH = + keccak256( + "ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data,uint256 chainid)" + ); + + mapping(address => uint256) private _nonces; + + constructor() EIP712ChainlessDomain("GSNv2 Forwarder", "0.0.1") {} + + function getNonce(address from) public view returns (uint256) { + return _nonces[from]; + } + + function verify(ForwardRequest calldata req, bytes calldata signature) public view returns (bool) { + address signer = _hashTypedDataV4( + keccak256( + abi.encode( + _TYPEHASH, + req.from, + req.to, + req.value, + req.gas, + req.nonce, + keccak256(req.data), + block.chainid + ) + ) + ).recover(signature); + return _nonces[req.from] == req.nonce && signer == req.from; + } + + function execute( + ForwardRequest calldata req, + bytes calldata signature + ) public payable returns (bool, bytes memory) { + // require(req.chainid == block.chainid, "MinimalForwarder: invalid chainId"); + require(verify(req, signature), "MinimalForwarder: signature does not match request"); + _nonces[req.from] = req.nonce + 1; + + (bool success, bytes memory returndata) = req.to.call{ gas: req.gas, value: req.value }( + abi.encodePacked(req.data, req.from) + ); + + // Validate that the relayer has sent enough gas for the call. + // See https://ronan.eth.link/blog/ethereum-gas-dangers/ + if (gasleft() <= req.gas / 63) { + // We explicitly trigger invalid opcode to consume all gas and bubble-up the effects, since + // neither revert or assert consume all gas since Solidity 0.8.0 + // https://docs.soliditylang.org/en/v0.8.0/control-structures.html#panic-via-assert-and-error-via-require + assembly { + invalid() + } + } + + return (success, returndata); + } +} diff --git a/contracts/infra/forwarder/ForwarderConsumer.sol b/contracts/infra/forwarder/ForwarderConsumer.sol new file mode 100644 index 000000000..f3e8d4fe1 --- /dev/null +++ b/contracts/infra/forwarder/ForwarderConsumer.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +import "../../external-deps/openzeppelin/metatx/ERC2771Context.sol"; + +contract ForwarderConsumer is ERC2771Context { + address public caller; + + constructor(address[] memory trustedForwarders) ERC2771Context(trustedForwarders) {} + + function setCaller() external { + caller = _msgSender(); + } +} diff --git a/contracts/infra/forwarder/ForwarderEOAOnly.sol b/contracts/infra/forwarder/ForwarderEOAOnly.sol new file mode 100644 index 000000000..b0612339f --- /dev/null +++ b/contracts/infra/forwarder/ForwarderEOAOnly.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +import "../../external-deps/openzeppelin/metatx/MinimalForwarderEOAOnly.sol"; + +/* + * @dev Minimal forwarder for GSNv2 + */ +contract ForwarderEOAOnly is MinimalForwarderEOAOnly { + // solhint-disable-next-line no-empty-blocks + constructor() MinimalForwarderEOAOnly() {} +} diff --git a/contracts/interfaces/IContractDeployer.sol b/contracts/infra/interface/IContractDeployer.sol similarity index 100% rename from contracts/interfaces/IContractDeployer.sol rename to contracts/infra/interface/IContractDeployer.sol diff --git a/contracts/interfaces/IContractPublisher.sol b/contracts/infra/interface/IContractPublisher.sol similarity index 78% rename from contracts/interfaces/IContractPublisher.sol rename to contracts/infra/interface/IContractPublisher.sol index d4e57a0e6..27806668f 100644 --- a/contracts/interfaces/IContractPublisher.sol +++ b/contracts/infra/interface/IContractPublisher.sol @@ -41,6 +41,9 @@ interface IContractPublisher { /// @dev Emitted when a contract is unpublished. event ContractUnpublished(address indexed operator, address indexed publisher, string indexed contractId); + /// @dev Emitted when a publisher updates their profile URI. + event PublisherProfileUpdated(address indexed publisher, string prevURI, string newURI); + /** * @notice Returns the latest version of all contracts published by a publisher. * @@ -48,10 +51,9 @@ interface IContractPublisher { * * @return published An array of all contracts published by the publisher. */ - function getAllPublishedContracts(address publisher) - external - view - returns (CustomContractInstance[] memory published); + function getAllPublishedContracts( + address publisher + ) external view returns (CustomContractInstance[] memory published); /** * @notice Returns all versions of a published contract. @@ -61,10 +63,10 @@ interface IContractPublisher { * * @return published The desired contracts published by the publisher. */ - function getPublishedContractVersions(address publisher, string memory contractId) - external - view - returns (CustomContractInstance[] memory published); + function getPublishedContractVersions( + address publisher, + string memory contractId + ) external view returns (CustomContractInstance[] memory published); /** * @notice Returns the latest version of a contract published by a publisher. @@ -74,13 +76,13 @@ interface IContractPublisher { * * @return published The desired contract published by the publisher. */ - function getPublishedContract(address publisher, string memory contractId) - external - view - returns (CustomContractInstance memory published); + function getPublishedContract( + address publisher, + string memory contractId + ) external view returns (CustomContractInstance memory published); /** - * @notice Let's an account publish a contract. The account must be approved by the publisher, or be the publisher. + * @notice Let's an account publish a contract. * * @param publisher The address of the publisher. * @param contractId The identifier for a published contract (that can have multiple verisons). @@ -100,7 +102,7 @@ interface IContractPublisher { ) external; /** - * @notice Lets an account unpublish a contract and all its versions. The account must be approved by the publisher, or be the publisher. + * @notice Lets a publisher unpublish a contract and all its versions. * * @param publisher The address of the publisher. * @param contractId The identifier for a published contract (that can have multiple verisons). @@ -113,15 +115,14 @@ interface IContractPublisher { function setPublisherProfileUri(address publisher, string memory uri) external; /** - * @notice get the publisher profile uri + * @notice Get the publisher profile uri for a given publisher. */ function getPublisherProfileUri(address publisher) external view returns (string memory uri); /** - * @notice Retrieve the published metadata URI from a compiler metadata URI + * @notice Retrieve the published metadata URI from a compiler metadata URI. */ - function getPublishedUriFromCompilerUri(string memory compilerMetadataUri) - external - view - returns (string[] memory publishedMetadataUris); + function getPublishedUriFromCompilerUri( + string memory compilerMetadataUri + ) external view returns (string[] memory publishedMetadataUris); } diff --git a/contracts/interfaces/ITWFee.sol b/contracts/infra/interface/ITWFee.sol similarity index 100% rename from contracts/interfaces/ITWFee.sol rename to contracts/infra/interface/ITWFee.sol diff --git a/contracts/infra/interface/ITWMultichainRegistry.sol b/contracts/infra/interface/ITWMultichainRegistry.sol new file mode 100644 index 000000000..c91ab1fc6 --- /dev/null +++ b/contracts/infra/interface/ITWMultichainRegistry.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +interface ITWMultichainRegistry { + struct Deployment { + address deploymentAddress; + uint256 chainId; + string metadataURI; + } + + event Added(address indexed deployer, address indexed deployment, uint256 indexed chainId, string metadataUri); + event Deleted(address indexed deployer, address indexed deployment, uint256 indexed chainId); + + /// @notice Add a deployment for a deployer. + function add(address _deployer, address _deployment, uint256 _chainId, string memory metadataUri) external; + + /// @notice Remove a deployment for a deployer. + function remove(address _deployer, address _deployment, uint256 _chainId) external; + + /// @notice Get all deployments for a deployer. + function getAll(address _deployer) external view returns (Deployment[] memory allDeployments); + + /// @notice Get the total number of deployments for a deployer. + function count(address _deployer) external view returns (uint256 deploymentCount); + + /// @notice Returns the metadata IPFS URI for a deployment on a given chain if previously registered via add(). + function getMetadataUri(uint256 _chainId, address _deployment) external view returns (string memory metadataUri); +} diff --git a/contracts/infra/interface/ITWRegistry.sol b/contracts/infra/interface/ITWRegistry.sol new file mode 100644 index 000000000..78ebf554f --- /dev/null +++ b/contracts/infra/interface/ITWRegistry.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +interface ITWRegistry { + struct Deployment { + address deploymentAddress; + uint256 chainId; + } + + event Added(address indexed deployer, address indexed deployment, uint256 indexed chainId); + event Deleted(address indexed deployer, address indexed deployment, uint256 indexed chainId); + + /// @notice Add a deployment for a deployer. + function add(address _deployer, address _deployment, uint256 _chainId) external; + + /// @notice Remove a deployment for a deployer. + function remove(address _deployer, address _deployment, uint256 _chainId) external; + + /// @notice Get all deployments for a deployer. + function getAll(address _deployer) external view returns (Deployment[] memory allDeployments); + + /// @notice Get the total number of deployments for a deployer. + function count(address _deployer) external view returns (uint256 deploymentCount); +} diff --git a/contracts/interfaces/IThirdwebContract.sol b/contracts/infra/interface/IThirdwebContract.sol similarity index 100% rename from contracts/interfaces/IThirdwebContract.sol rename to contracts/infra/interface/IThirdwebContract.sol diff --git a/contracts/interfaces/IWETH.sol b/contracts/infra/interface/IWETH.sol similarity index 100% rename from contracts/interfaces/IWETH.sol rename to contracts/infra/interface/IWETH.sol diff --git a/contracts/infra/registry/entrypoint/TWMultichainRegistryRouter.sol b/contracts/infra/registry/entrypoint/TWMultichainRegistryRouter.sol new file mode 100644 index 000000000..0075ca570 --- /dev/null +++ b/contracts/infra/registry/entrypoint/TWMultichainRegistryRouter.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// ========== Internal imports ========== + +import "../../../extension/plugin/PermissionsEnumerableLogic.sol"; +import "../../../extension/plugin/ERC2771ContextLogic.sol"; +import "../../../extension/Multicall.sol"; +import "../../../extension/plugin/Router.sol"; + +/** + * + * "Inherited by entrypoint" extensions. + * - PermissionsEnumerable + * - ERC2771Context + * - Multicall + * + * "NOT inherited by entrypoint" extensions. + * - TWMultichainRegistry + */ + +contract TWMultichainRegistryRouter is PermissionsEnumerableLogic, ERC2771ContextLogic, Router { + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + constructor( + address _pluginMap, + address[] memory _trustedForwarders + ) ERC2771ContextLogic(_trustedForwarders) Router(_pluginMap) { + _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /*/////////////////////////////////////////////////////////////// + Overridable Permissions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns whether plug-in can be set in the given execution context. + function _canSetPlugin() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + function _msgSender() internal view override(ERC2771ContextLogic, PermissionsLogic, Multicall) returns (address) { + return ERC2771ContextLogic._msgSender(); + } + + function _msgData() internal view override(ERC2771ContextLogic, PermissionsLogic) returns (bytes calldata) { + return ERC2771ContextLogic._msgData(); + } +} diff --git a/contracts/infra/registry/registry-extension/TWMultichainRegistryLogic.sol b/contracts/infra/registry/registry-extension/TWMultichainRegistryLogic.sol new file mode 100644 index 000000000..3a0159d23 --- /dev/null +++ b/contracts/infra/registry/registry-extension/TWMultichainRegistryLogic.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +import "../../../extension/plugin/ERC2771ContextConsumer.sol"; +import "../../../extension/plugin/PermissionsEnumerableLogic.sol"; + +import "../../interface/ITWMultichainRegistry.sol"; +import "./TWMultichainRegistryStorage.sol"; + +contract TWMultichainRegistryLogic is ITWMultichainRegistry, ERC2771ContextConsumer { + bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); + + using EnumerableSet for EnumerableSet.AddressSet; + using EnumerableSet for EnumerableSet.UintSet; + + /*/////////////////////////////////////////////////////////////// + Generic contract logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the type of the contract. + function contractType() external pure returns (bytes32) { + return bytes32("TWMultichainRegistry"); + } + + /// @dev Returns the version of the contract. + function contractVersion() external pure returns (uint8) { + return uint8(1); + } + + /*/////////////////////////////////////////////////////////////// + Core Functions + //////////////////////////////////////////////////////////////*/ + + // slither-disable-next-line similar-names + function add(address _deployer, address _deployment, uint256 _chainId, string memory metadataUri) external { + require( + PermissionsEnumerableLogic(address(this)).hasRole(OPERATOR_ROLE, _msgSender()) || _deployer == _msgSender(), + "not operator or deployer." + ); + + TWMultichainRegistryStorage.Data storage data = TWMultichainRegistryStorage.multichainRegistryStorage(); + + bool added = data.deployments[_deployer][_chainId].add(_deployment); + require(added, "failed to add"); + + data.chainIds.add(_chainId); + + if (bytes(metadataUri).length > 0) { + data.addressToMetadataUri[_chainId][_deployment] = metadataUri; + } + + emit Added(_deployer, _deployment, _chainId, metadataUri); + } + + // slither-disable-next-line similar-names + function remove(address _deployer, address _deployment, uint256 _chainId) external { + require( + PermissionsEnumerableLogic(address(this)).hasRole(OPERATOR_ROLE, _msgSender()) || _deployer == _msgSender(), + "not operator or deployer." + ); + + TWMultichainRegistryStorage.Data storage data = TWMultichainRegistryStorage.multichainRegistryStorage(); + + bool removed = data.deployments[_deployer][_chainId].remove(_deployment); + require(removed, "failed to remove"); + + emit Deleted(_deployer, _deployment, _chainId); + } + + function getAll(address _deployer) external view returns (Deployment[] memory allDeployments) { + TWMultichainRegistryStorage.Data storage data = TWMultichainRegistryStorage.multichainRegistryStorage(); + uint256 totalDeployments; + uint256 chainIdsLen = data.chainIds.length(); + + for (uint256 i = 0; i < chainIdsLen; i += 1) { + uint256 chainId = data.chainIds.at(i); + + totalDeployments += data.deployments[_deployer][chainId].length(); + } + + allDeployments = new Deployment[](totalDeployments); + uint256 idx; + + for (uint256 j = 0; j < chainIdsLen; j += 1) { + uint256 chainId = data.chainIds.at(j); + + uint256 len = data.deployments[_deployer][chainId].length(); + address[] memory deploymentAddrs = data.deployments[_deployer][chainId].values(); + + for (uint256 k = 0; k < len; k += 1) { + allDeployments[idx] = Deployment({ + deploymentAddress: deploymentAddrs[k], + chainId: chainId, + metadataURI: data.addressToMetadataUri[chainId][deploymentAddrs[k]] + }); + idx += 1; + } + } + } + + function count(address _deployer) external view returns (uint256 deploymentCount) { + TWMultichainRegistryStorage.Data storage data = TWMultichainRegistryStorage.multichainRegistryStorage(); + uint256 chainIdsLen = data.chainIds.length(); + + for (uint256 i = 0; i < chainIdsLen; i += 1) { + uint256 chainId = data.chainIds.at(i); + + deploymentCount += data.deployments[_deployer][chainId].length(); + } + } + + function getMetadataUri(uint256 _chainId, address _deployment) external view returns (string memory metadataUri) { + TWMultichainRegistryStorage.Data storage data = TWMultichainRegistryStorage.multichainRegistryStorage(); + metadataUri = data.addressToMetadataUri[_chainId][_deployment]; + } +} diff --git a/contracts/infra/registry/registry-extension/TWMultichainRegistryStorage.sol b/contracts/infra/registry/registry-extension/TWMultichainRegistryStorage.sol new file mode 100644 index 000000000..a237b7560 --- /dev/null +++ b/contracts/infra/registry/registry-extension/TWMultichainRegistryStorage.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +import "../../interface/ITWMultichainRegistry.sol"; + +library TWMultichainRegistryStorage { + /// @custom:storage-location erc7201:multichain.registry.storage + /// @dev keccak256(abi.encode(uint256(keccak256("multichain.registry.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant MULTICHAIN_REGISTRY_STORAGE_POSITION = + 0x14e6df431852605a9ea88d8bd521d0d3fa06563ab37f65080e288e5afad4ac00; + + struct Data { + /// @dev wallet address => [contract addresses] + mapping(address => mapping(uint256 => EnumerableSet.AddressSet)) deployments; + /// @dev contract address deployed => imported metadata uri + mapping(uint256 => mapping(address => string)) addressToMetadataUri; + EnumerableSet.UintSet chainIds; + } + + function multichainRegistryStorage() internal pure returns (Data storage multichainRegistryData) { + bytes32 position = MULTICHAIN_REGISTRY_STORAGE_POSITION; + assembly { + multichainRegistryData.slot := position + } + } +} diff --git a/contracts/interfaces/IContractMetadataRegistry.sol b/contracts/interfaces/IContractMetadataRegistry.sol deleted file mode 100644 index 6c922c8e0..000000000 --- a/contracts/interfaces/IContractMetadataRegistry.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -interface IContractMetadataRegistry { - /// @dev Emitted when a contract metadata is registered - event MetadataRegistered(address indexed contractAddress, string metadataUri); - - /// @dev Records `metadataUri` as metadata for the contract at `contractAddress`. - function registerMetadata(address contractAddress, string memory metadataUri) external; -} diff --git a/contracts/interfaces/drop/IDropERC20.sol b/contracts/interfaces/drop/IDropERC20.sol deleted file mode 100644 index fcd9d73b3..000000000 --- a/contracts/interfaces/drop/IDropERC20.sol +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; -import "./IDropClaimCondition.sol"; - -/** - * Thirdweb's 'Drop' contracts are distribution mechanisms for tokens. The - * `DropERC20` contract is a distribution mechanism for ERC20 tokens. - * - * A contract admin (i.e. holder of `DEFAULT_ADMIN_ROLE`) can create claim conditions - * with non-overlapping time windows, and accounts can claim the tokens according to - * restrictions defined in the claim condition that is active at the time of the transaction. - */ - -interface IDropERC20 is IERC20Upgradeable, IDropClaimCondition { - /// @dev Emitted when tokens are claimed. - event TokensClaimed( - uint256 indexed claimConditionIndex, - address indexed claimer, - address indexed receiver, - uint256 quantityClaimed - ); - - /// @dev Emitted when new claim conditions are set. - event ClaimConditionsUpdated(ClaimCondition[] claimConditions); - - /// @dev Emitted when the global max supply of tokens is updated. - event MaxTotalSupplyUpdated(uint256 maxTotalSupply); - - /// @dev Emitted when the wallet claim count for an address is updated. - event WalletClaimCountUpdated(address indexed wallet, uint256 count); - - /// @dev Emitted when the global max wallet claim count is updated. - event MaxWalletClaimCountUpdated(uint256 count); - - /** - * @notice Lets an account claim a given quantity of tokens. - * - * @param receiver The receiver of the tokens to claim. - * @param quantity The quantity of tokens to claim. - * @param currency The currency in which to pay for the claim. - * @param pricePerToken The price per token (i.e. price per 1 ether unit of the token) - * to pay for the claim. - * @param proofs The proof of the claimer's inclusion in the merkle root allowlist - * of the claim conditions that apply. - * @param proofMaxQuantityPerTransaction (Optional) The maximum number of tokens an address included in an - * allowlist can claim. - */ - function claim( - address receiver, - uint256 quantity, - address currency, - uint256 pricePerToken, - bytes32[] calldata proofs, - uint256 proofMaxQuantityPerTransaction - ) external payable; - - /** - * @notice Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. - * - * @param phases Claim conditions in ascending order by `startTimestamp`. - * @param resetClaimEligibility Whether to reset `limitLastClaimTimestamp` and - * `limitMerkleProofClaim` values when setting new - * claim conditions. - */ - function setClaimConditions(ClaimCondition[] calldata phases, bool resetClaimEligibility) external; -} diff --git a/contracts/interfaces/marketplace/IMarketplace.sol b/contracts/interfaces/marketplace/IMarketplace.sol deleted file mode 100644 index 304e72ab1..000000000 --- a/contracts/interfaces/marketplace/IMarketplace.sol +++ /dev/null @@ -1,334 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -import "../IThirdwebContract.sol"; -import "../../extension/interface/IPlatformFee.sol"; - -interface IMarketplace is IThirdwebContract, IPlatformFee { - /// @notice Type of the tokens that can be listed for sale. - enum TokenType { - ERC1155, - ERC721 - } - - /** - * @notice The two types of listings. - * `Direct`: NFTs listed for sale at a fixed price. - * `Auction`: NFTs listed for sale in an auction. - */ - enum ListingType { - Direct, - Auction - } - - /** - * @notice The information related to either (1) an offer on a direct listing, or (2) a bid in an auction. - * - * @dev The type of the listing at ID `lisingId` determins how the `Offer` is interpreted. - * If the listing is of type `Direct`, the `Offer` is interpreted as an offer to a direct listing. - * If the listing is of type `Auction`, the `Offer` is interpreted as a bid in an auction. - * - * @param listingId The uid of the listing the offer is made to. - * @param offeror The account making the offer. - * @param quantityWanted The quantity of tokens from the listing wanted by the offeror. - * This is the entire listing quantity if the listing is an auction. - * @param currency The currency in which the offer is made. - * @param pricePerToken The price per token offered to the lister. - * @param expirationTimestamp The timestamp after which a seller cannot accept this offer. - */ - struct Offer { - uint256 listingId; - address offeror; - uint256 quantityWanted; - address currency; - uint256 pricePerToken; - uint256 expirationTimestamp; - } - - /** - * @dev For use in `createListing` as a parameter type. - * - * @param assetContract The contract address of the NFT to list for sale. - - * @param tokenId The tokenId on `assetContract` of the NFT to list for sale. - - * @param startTime The unix timestamp after which the listing is active. For direct listings: - * 'active' means NFTs can be bought from the listing. For auctions, - * 'active' means bids can be made in the auction. - * - * @param secondsUntilEndTime No. of seconds after `startTime`, after which the listing is inactive. - * For direct listings: 'inactive' means NFTs cannot be bought from the listing. - * For auctions: 'inactive' means bids can no longer be made in the auction. - * - * @param quantityToList The quantity of NFT of ID `tokenId` on the given `assetContract` to list. For - * ERC 721 tokens to list for sale, the contract strictly defaults this to `1`, - * Regardless of the value of `quantityToList` passed. - * - * @param currencyToAccept For direct listings: the currency in which a buyer must pay the listing's fixed price - * to buy the NFT(s). For auctions: the currency in which the bidders must make bids. - * - * @param reservePricePerToken For direct listings: this value is ignored. For auctions: the minimum bid amount of - * the auction is `reservePricePerToken * quantityToList` - * - * @param buyoutPricePerToken For direct listings: interpreted as 'price per token' listed. For auctions: if - * `buyoutPricePerToken` is greater than 0, and a bidder's bid is at least as great as - * `buyoutPricePerToken * quantityToList`, the bidder wins the auction, and the auction - * is closed. - * - * @param listingType The type of listing to create - a direct listing or an auction. - **/ - struct ListingParameters { - address assetContract; - uint256 tokenId; - uint256 startTime; - uint256 secondsUntilEndTime; - uint256 quantityToList; - address currencyToAccept; - uint256 reservePricePerToken; - uint256 buyoutPricePerToken; - ListingType listingType; - } - - /** - * @notice The information related to a listing; either (1) a direct listing, or (2) an auction listing. - * - * @dev For direct listings: - * (1) `reservePricePerToken` is ignored. - * (2) `buyoutPricePerToken` is simply interpreted as 'price per token'. - * - * @param listingId The uid for the listing. - * - * @param tokenOwner The owner of the tokens listed for sale. - * - * @param assetContract The contract address of the NFT to list for sale. - - * @param tokenId The tokenId on `assetContract` of the NFT to list for sale. - - * @param startTime The unix timestamp after which the listing is active. For direct listings: - * 'active' means NFTs can be bought from the listing. For auctions, - * 'active' means bids can be made in the auction. - * - * @param endTime The timestamp after which the listing is inactive. - * For direct listings: 'inactive' means NFTs cannot be bought from the listing. - * For auctions: 'inactive' means bids can no longer be made in the auction. - * - * @param quantity The quantity of NFT of ID `tokenId` on the given `assetContract` listed. For - * ERC 721 tokens to list for sale, the contract strictly defaults this to `1`, - * Regardless of the value of `quantityToList` passed. - * - * @param currency For direct listings: the currency in which a buyer must pay the listing's fixed price - * to buy the NFT(s). For auctions: the currency in which the bidders must make bids. - * - * @param reservePricePerToken For direct listings: this value is ignored. For auctions: the minimum bid amount of - * the auction is `reservePricePerToken * quantityToList` - * - * @param buyoutPricePerToken For direct listings: interpreted as 'price per token' listed. For auctions: if - * `buyoutPricePerToken` is greater than 0, and a bidder's bid is at least as great as - * `buyoutPricePerToken * quantityToList`, the bidder wins the auction, and the auction - * is closed. - * - * @param tokenType The type of the token(s) listed for for sale -- ERC721 or ERC1155 - * - * @param listingType The type of listing to create - a direct listing or an auction. - **/ - struct Listing { - uint256 listingId; - address tokenOwner; - address assetContract; - uint256 tokenId; - uint256 startTime; - uint256 endTime; - uint256 quantity; - address currency; - uint256 reservePricePerToken; - uint256 buyoutPricePerToken; - TokenType tokenType; - ListingType listingType; - } - - /// @dev Emitted when a new listing is created. - event ListingAdded( - uint256 indexed listingId, - address indexed assetContract, - address indexed lister, - Listing listing - ); - - /// @dev Emitted when the parameters of a listing are updated. - event ListingUpdated(uint256 indexed listingId, address indexed listingCreator); - - /// @dev Emitted when a listing is cancelled. - event ListingRemoved(uint256 indexed listingId, address indexed listingCreator); - - /** - * @dev Emitted when a buyer buys from a direct listing, or a lister accepts some - * buyer's offer to their direct listing. - */ - event NewSale( - uint256 indexed listingId, - address indexed assetContract, - address indexed lister, - address buyer, - uint256 quantityBought, - uint256 totalPricePaid - ); - - /// @dev Emitted when (1) a new offer is made to a direct listing, or (2) when a new bid is made in an auction. - event NewOffer( - uint256 indexed listingId, - address indexed offeror, - ListingType indexed listingType, - uint256 quantityWanted, - uint256 totalOfferAmount, - address currency - ); - - /// @dev Emitted when an auction is closed. - event AuctionClosed( - uint256 indexed listingId, - address indexed closer, - bool indexed cancelled, - address auctionCreator, - address winningBidder - ); - - /// @dev Emitted when auction buffers are updated. - event AuctionBuffersUpdated(uint256 timeBuffer, uint256 bidBufferBps); - - /** - * @notice Lets a token owner list tokens (ERC 721 or ERC 1155) for sale in a direct listing, or an auction. - * - * @dev NFTs to list for sale in an auction are escrowed in Marketplace. For direct listings, the contract - * only checks whether the listing's creator owns and has approved Marketplace to transfer the NFTs to list. - * - * @param _params The parameters that govern the listing to be created. - */ - function createListing(ListingParameters memory _params) external; - - /** - * @notice Lets a listing's creator edit the listing's parameters. A direct listing can be edited whenever. - * An auction listing cannot be edited after the auction has started. - * - * @param _listingId The uid of the lisitng to edit. - * - * @param _quantityToList The amount of NFTs to list for sale in the listing. For direct lisitngs, the contract - * only checks whether the listing creator owns and has approved Marketplace to transfer - * `_quantityToList` amount of NFTs to list for sale. For auction listings, the contract - * ensures that exactly `_quantityToList` amount of NFTs to list are escrowed. - * - * @param _reservePricePerToken For direct listings: this value is ignored. For auctions: the minimum bid amount of - * the auction is `reservePricePerToken * quantityToList` - * - * @param _buyoutPricePerToken For direct listings: interpreted as 'price per token' listed. For auctions: if - * `buyoutPricePerToken` is greater than 0, and a bidder's bid is at least as great as - * `buyoutPricePerToken * quantityToList`, the bidder wins the auction, and the auction - * is closed. - * - * @param _currencyToAccept For direct listings: the currency in which a buyer must pay the listing's fixed price - * to buy the NFT(s). For auctions: the currency in which the bidders must make bids. - * - * @param _startTime The unix timestamp after which listing is active. For direct listings: - * 'active' means NFTs can be bought from the listing. For auctions, - * 'active' means bids can be made in the auction. - * - * @param _secondsUntilEndTime No. of seconds after the provided `_startTime`, after which the listing is inactive. - * For direct listings: 'inactive' means NFTs cannot be bought from the listing. - * For auctions: 'inactive' means bids can no longer be made in the auction. - */ - function updateListing( - uint256 _listingId, - uint256 _quantityToList, - uint256 _reservePricePerToken, - uint256 _buyoutPricePerToken, - address _currencyToAccept, - uint256 _startTime, - uint256 _secondsUntilEndTime - ) external; - - /** - * @notice Lets a direct listing creator cancel their listing. - * - * @param _listingId The unique Id of the lisitng to cancel. - */ - function cancelDirectListing(uint256 _listingId) external; - - /** - * @notice Lets someone buy a given quantity of tokens from a direct listing by paying the fixed price. - * - * @param _listingId The uid of the direct lisitng to buy from. - * @param _buyFor The receiver of the NFT being bought. - * @param _quantity The amount of NFTs to buy from the direct listing. - * @param _currency The currency to pay the price in. - * @param _totalPrice The total price to pay for the tokens being bought. - * - * @dev A sale will fail to execute if either: - * (1) buyer does not own or has not approved Marketplace to transfer the appropriate - * amount of currency (or hasn't sent the appropriate amount of native tokens) - * - * (2) the lister does not own or has removed Markeplace's - * approval to transfer the tokens listed for sale. - */ - function buy( - uint256 _listingId, - address _buyFor, - uint256 _quantity, - address _currency, - uint256 _totalPrice - ) external payable; - - /** - * @notice Lets someone make an offer to a direct listing, or bid in an auction. - * - * @dev Each (address, listing ID) pair maps to a single unique offer. So e.g. if a buyer makes - * makes two offers to the same direct listing, the last offer is counted as the buyer's - * offer to that listing. - * - * @param _listingId The unique ID of the lisitng to make an offer/bid to. - * - * @param _quantityWanted For auction listings: the 'quantity wanted' is the total amount of NFTs - * being auctioned, regardless of the value of `_quantityWanted` passed. - * For direct listings: `_quantityWanted` is the quantity of NFTs from the - * listing, for which the offer is being made. - * - * @param _currency For auction listings: the 'currency of the bid' is the currency accepted - * by the auction, regardless of the value of `_currency` passed. For direct - * listings: this is the currency in which the offer is made. - * - * @param _pricePerToken For direct listings: offered price per token. For auction listings: the bid - * amount per token. The total offer/bid amount is `_quantityWanted * _pricePerToken`. - * - * @param _expirationTimestamp For aution listings: inapplicable. For direct listings: The timestamp after which - * the seller can no longer accept the offer. - */ - function offer( - uint256 _listingId, - uint256 _quantityWanted, - address _currency, - uint256 _pricePerToken, - uint256 _expirationTimestamp - ) external payable; - - /** - * @notice Lets a listing's creator accept an offer to their direct listing. - * @param _listingId The unique ID of the listing for which to accept the offer. - * @param _offeror The address of the buyer whose offer is to be accepted. - * @param _currency The currency of the offer that is to be accepted. - * @param _totalPrice The total price of the offer that is to be accepted. - */ - function acceptOffer( - uint256 _listingId, - address _offeror, - address _currency, - uint256 _totalPrice - ) external; - - /** - * @notice Lets any account close an auction on behalf of either the (1) auction's creator, or (2) winning bidder. - * For (1): The auction creator is sent the the winning bid amount. - * For (2): The winning bidder is sent the auctioned NFTs. - * - * @param _listingId The uid of the listing (the auction to close). - * @param _closeFor For whom the auction is being closed - the auction creator or winning bidder. - */ - function closeAuction(uint256 _listingId, address _closeFor) external; -} diff --git a/contracts/legacy-contracts/extension/BatchMintMetadata_V1.sol b/contracts/legacy-contracts/extension/BatchMintMetadata_V1.sol new file mode 100644 index 000000000..7158488dd --- /dev/null +++ b/contracts/legacy-contracts/extension/BatchMintMetadata_V1.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +/** + * @title Batch-mint Metadata + * @notice The `BatchMintMetadata` is a contract extension for any base NFT contract. It lets the smart contract + * using this extension set metadata for `n` number of NFTs all at once. This is enabled by storing a single + * base URI for a batch of `n` NFTs, where the metadata for each NFT in a relevant batch is `baseURI/tokenId`. + */ + +contract BatchMintMetadata_V1 { + /// @dev Largest tokenId of each batch of tokens with the same baseURI. + uint256[] private batchIds; + + /// @dev Mapping from id of a batch of tokens => to base URI for the respective batch of tokens. + mapping(uint256 => string) private baseURI; + + /** + * @notice Returns the count of batches of NFTs. + * @dev Each batch of tokens has an in ID and an associated `baseURI`. + * See {batchIds}. + */ + function getBaseURICount() public view returns (uint256) { + return batchIds.length; + } + + /** + * @notice Returns the ID for the batch of tokens at the given index. + * @dev See {getBaseURICount}. + * @param _index Index of the desired batch in batchIds array. + */ + function getBatchIdAtIndex(uint256 _index) public view returns (uint256) { + if (_index >= getBaseURICount()) { + revert("Invalid index"); + } + return batchIds[_index]; + } + + /// @dev Returns the id for the batch of tokens the given tokenId belongs to. + function _getBatchId(uint256 _tokenId) internal view returns (uint256 batchId, uint256 index) { + uint256 numOfTokenBatches = getBaseURICount(); + uint256[] memory indices = batchIds; + + for (uint256 i = 0; i < numOfTokenBatches; i += 1) { + if (_tokenId < indices[i]) { + index = i; + batchId = indices[i]; + + return (batchId, index); + } + } + + revert("Invalid tokenId"); + } + + /// @dev Returns the baseURI for a token. The intended metadata URI for the token is baseURI + tokenId. + function _getBaseURI(uint256 _tokenId) internal view returns (string memory) { + uint256 numOfTokenBatches = getBaseURICount(); + uint256[] memory indices = batchIds; + + for (uint256 i = 0; i < numOfTokenBatches; i += 1) { + if (_tokenId < indices[i]) { + return baseURI[indices[i]]; + } + } + revert("Invalid tokenId"); + } + + /// @dev Sets the base URI for the batch of tokens with the given batchId. + function _setBaseURI(uint256 _batchId, string memory _baseURI) internal { + baseURI[_batchId] = _baseURI; + } + + /// @dev Mints a batch of tokenIds and associates a common baseURI to all those Ids. + function _batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) internal returns (uint256 nextTokenIdToMint, uint256 batchId) { + batchId = _startId + _amountToMint; + nextTokenIdToMint = batchId; + + batchIds.push(batchId); + + baseURI[batchId] = _baseURIForTokens; + } +} diff --git a/contracts/legacy-contracts/extension/DropSinglePhase1155_V1.sol b/contracts/legacy-contracts/extension/DropSinglePhase1155_V1.sol new file mode 100644 index 000000000..003906bd7 --- /dev/null +++ b/contracts/legacy-contracts/extension/DropSinglePhase1155_V1.sol @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./interface/IDropSinglePhase1155_V1.sol"; +import "../../lib/MerkleProof.sol"; +import "../../lib/BitMaps.sol"; + +abstract contract DropSinglePhase1155_V1 is IDropSinglePhase1155_V1 { + using BitMaps for BitMaps.BitMap; + + /*/////////////////////////////////////////////////////////////// + Mappings + //////////////////////////////////////////////////////////////*/ + + /// @dev Mapping from tokenId => active claim condition for the tokenId. + mapping(uint256 => ClaimCondition) public claimCondition; + + /// @dev Mapping from tokenId => active claim condition's UID. + mapping(uint256 => bytes32) private conditionId; + + /** + * @dev Map from an account and uid for a claim condition, to the last timestamp + * at which the account claimed tokens under that claim condition. + */ + mapping(bytes32 => mapping(address => uint256)) private lastClaimTimestamp; + + /** + * @dev Map from a claim condition uid to whether an address in an allowlist + * has already claimed tokens i.e. used their place in the allowlist. + */ + mapping(bytes32 => BitMaps.BitMap) private usedAllowlistSpot; + + /*/////////////////////////////////////////////////////////////// + Drop logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Lets an account claim tokens. + function claim( + address _receiver, + uint256 _tokenId, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof, + bytes memory _data + ) public payable virtual override { + _beforeClaim(_tokenId, _receiver, _quantity, _currency, _pricePerToken, _allowlistProof, _data); + + ClaimCondition memory condition = claimCondition[_tokenId]; + bytes32 activeConditionId = conditionId[_tokenId]; + + /** + * We make allowlist checks (i.e. verifyClaimMerkleProof) before verifying the claim's general + * validity (i.e. verifyClaim) because we give precedence to the check of allow list quantity + * restriction over the check of the general claim condition's quantityLimitPerTransaction + * restriction. + */ + + // Verify inclusion in allowlist. + (bool validMerkleProof, ) = verifyClaimMerkleProof(_tokenId, _dropMsgSender(), _quantity, _allowlistProof); + + // Verify claim validity. If not valid, revert. + // when there's allowlist present --> verifyClaimMerkleProof will verify the maxQuantityInAllowlist value with hashed leaf in the allowlist + // when there's no allowlist, this check is true --> verifyClaim will check for _quantity being equal/less than the limit + bool toVerifyMaxQuantityPerTransaction = _allowlistProof.maxQuantityInAllowlist == 0 || + condition.merkleRoot == bytes32(0); + + verifyClaim( + _tokenId, + _dropMsgSender(), + _quantity, + _currency, + _pricePerToken, + toVerifyMaxQuantityPerTransaction + ); + + if (validMerkleProof && _allowlistProof.maxQuantityInAllowlist > 0) { + /** + * Mark the claimer's use of their position in the allowlist. A spot in an allowlist + * can be used only once. + */ + usedAllowlistSpot[activeConditionId].set(uint256(uint160(_dropMsgSender()))); + } + + // Update contract state. + condition.supplyClaimed += _quantity; + lastClaimTimestamp[activeConditionId][_dropMsgSender()] = block.timestamp; + claimCondition[_tokenId] = condition; + + // If there's a price, collect price. + _collectPriceOnClaim(address(0), _quantity, _currency, _pricePerToken); + + // Mint the relevant NFTs to claimer. + _transferTokensOnClaim(_receiver, _tokenId, _quantity); + + emit TokensClaimed(_dropMsgSender(), _receiver, _tokenId, _quantity); + + _afterClaim(_tokenId, _receiver, _quantity, _currency, _pricePerToken, _allowlistProof, _data); + } + + /// @dev Lets a contract admin set claim conditions. + function setClaimConditions( + uint256 _tokenId, + ClaimCondition calldata _condition, + bool _resetClaimEligibility + ) external override { + if (!_canSetClaimConditions()) { + revert("Not authorized"); + } + + ClaimCondition memory condition = claimCondition[_tokenId]; + bytes32 targetConditionId = conditionId[_tokenId]; + + uint256 supplyClaimedAlready = condition.supplyClaimed; + + if (targetConditionId == bytes32(0) || _resetClaimEligibility) { + supplyClaimedAlready = 0; + targetConditionId = keccak256(abi.encodePacked(_dropMsgSender(), block.number, _tokenId)); + } + + if (supplyClaimedAlready > _condition.maxClaimableSupply) { + revert("max supply claimed"); + } + + ClaimCondition memory updatedCondition = ClaimCondition({ + startTimestamp: _condition.startTimestamp, + maxClaimableSupply: _condition.maxClaimableSupply, + supplyClaimed: supplyClaimedAlready, + quantityLimitPerTransaction: _condition.quantityLimitPerTransaction, + waitTimeInSecondsBetweenClaims: _condition.waitTimeInSecondsBetweenClaims, + merkleRoot: _condition.merkleRoot, + pricePerToken: _condition.pricePerToken, + currency: _condition.currency + }); + + claimCondition[_tokenId] = updatedCondition; + conditionId[_tokenId] = targetConditionId; + + emit ClaimConditionUpdated(_tokenId, _condition, _resetClaimEligibility); + } + + /// @dev Checks a request to claim NFTs against the active claim condition's criteria. + function verifyClaim( + uint256 _tokenId, + address _claimer, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + bool verifyMaxQuantityPerTransaction + ) public view { + ClaimCondition memory currentClaimPhase = claimCondition[_tokenId]; + + if (_currency != currentClaimPhase.currency || _pricePerToken != currentClaimPhase.pricePerToken) { + revert("Invalid price or currency"); + } + + // If we're checking for an allowlist quantity restriction, ignore the general quantity restriction. + if ( + _quantity == 0 || + (verifyMaxQuantityPerTransaction && _quantity > currentClaimPhase.quantityLimitPerTransaction) + ) { + revert("Invalid quantity"); + } + + if (currentClaimPhase.supplyClaimed + _quantity > currentClaimPhase.maxClaimableSupply) { + revert("exceeds max supply"); + } + + (uint256 lastClaimedAt, uint256 nextValidClaimTimestamp) = getClaimTimestamp(_tokenId, _claimer); + if ( + currentClaimPhase.startTimestamp > block.timestamp || + (lastClaimedAt != 0 && block.timestamp < nextValidClaimTimestamp) + ) { + revert("cant claim yet"); + } + } + + /// @dev Checks whether a claimer meets the claim condition's allowlist criteria. + function verifyClaimMerkleProof( + uint256 _tokenId, + address _claimer, + uint256 _quantity, + AllowlistProof calldata _allowlistProof + ) public view returns (bool validMerkleProof, uint256 merkleProofIndex) { + ClaimCondition memory currentClaimPhase = claimCondition[_tokenId]; + + if (currentClaimPhase.merkleRoot != bytes32(0)) { + (validMerkleProof, merkleProofIndex) = MerkleProof.verify( + _allowlistProof.proof, + currentClaimPhase.merkleRoot, + keccak256(abi.encodePacked(_claimer, _allowlistProof.maxQuantityInAllowlist)) + ); + if (!validMerkleProof) { + revert("not in allowlist"); + } + + if (usedAllowlistSpot[conditionId[_tokenId]].get(uint256(uint160(_claimer)))) { + revert("proof claimed"); + } + + if (_allowlistProof.maxQuantityInAllowlist != 0 && _quantity > _allowlistProof.maxQuantityInAllowlist) { + revert("Invalid qty proof"); + } + } + } + + /// @dev Returns the timestamp for when a claimer is eligible for claiming NFTs again. + function getClaimTimestamp( + uint256 _tokenId, + address _claimer + ) public view returns (uint256 lastClaimedAt, uint256 nextValidClaimTimestamp) { + lastClaimedAt = lastClaimTimestamp[conditionId[_tokenId]][_claimer]; + + unchecked { + nextValidClaimTimestamp = lastClaimedAt + claimCondition[_tokenId].waitTimeInSecondsBetweenClaims; + + if (nextValidClaimTimestamp < lastClaimedAt) { + nextValidClaimTimestamp = type(uint256).max; + } + } + } + + /*//////////////////////////////////////////////////////////////////// + Optional hooks that can be implemented in the derived contract + ///////////////////////////////////////////////////////////////////*/ + + /// @dev Exposes the ability to override the msg sender. + function _dropMsgSender() internal virtual returns (address) { + return msg.sender; + } + + /// @dev Runs before every `claim` function call. + function _beforeClaim( + uint256 _tokenId, + address _receiver, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof, + bytes memory _data + ) internal virtual {} + + /// @dev Runs after every `claim` function call. + function _afterClaim( + uint256 _tokenId, + address _receiver, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof, + bytes memory _data + ) internal virtual {} + + /// @dev Collects and distributes the primary sale value of NFTs being claimed. + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal virtual; + + /// @dev Transfers the NFTs being claimed. + function _transferTokensOnClaim(address _to, uint256 _tokenId, uint256 _quantityBeingClaimed) internal virtual; + + function _canSetClaimConditions() internal view virtual returns (bool); +} diff --git a/contracts/legacy-contracts/extension/DropSinglePhase_V1.sol b/contracts/legacy-contracts/extension/DropSinglePhase_V1.sol new file mode 100644 index 000000000..571a0150b --- /dev/null +++ b/contracts/legacy-contracts/extension/DropSinglePhase_V1.sol @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./interface/IDropSinglePhase_V1.sol"; +import "../../lib/MerkleProof.sol"; +import "../../lib/BitMaps.sol"; + +abstract contract DropSinglePhase_V1 is IDropSinglePhase_V1 { + using BitMaps for BitMaps.BitMap; + + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + /// @dev The active conditions for claiming tokens. + ClaimCondition public claimCondition; + + /// @dev The ID for the active claim condition. + bytes32 private conditionId; + + /*/////////////////////////////////////////////////////////////// + Mappings + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Map from an account and uid for a claim condition, to the last timestamp + * at which the account claimed tokens under that claim condition. + */ + mapping(bytes32 => mapping(address => uint256)) private lastClaimTimestamp; + + /** + * @dev Map from a claim condition uid to whether an address in an allowlist + * has already claimed tokens i.e. used their place in the allowlist. + */ + mapping(bytes32 => BitMaps.BitMap) private usedAllowlistSpot; + + /*/////////////////////////////////////////////////////////////// + Drop logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Lets an account claim tokens. + function claim( + address _receiver, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof, + bytes memory _data + ) public payable virtual override { + _beforeClaim(_receiver, _quantity, _currency, _pricePerToken, _allowlistProof, _data); + + bytes32 activeConditionId = conditionId; + + /** + * We make allowlist checks (i.e. verifyClaimMerkleProof) before verifying the claim's general + * validity (i.e. verifyClaim) because we give precedence to the check of allow list quantity + * restriction over the check of the general claim condition's quantityLimitPerTransaction + * restriction. + */ + + // Verify inclusion in allowlist. + (bool validMerkleProof, ) = verifyClaimMerkleProof(_dropMsgSender(), _quantity, _allowlistProof); + + // Verify claim validity. If not valid, revert. + // when there's allowlist present --> verifyClaimMerkleProof will verify the maxQuantityInAllowlist value with hashed leaf in the allowlist + // when there's no allowlist, this check is true --> verifyClaim will check for _quantity being equal/less than the limit + bool toVerifyMaxQuantityPerTransaction = _allowlistProof.maxQuantityInAllowlist == 0 || + claimCondition.merkleRoot == bytes32(0); + + verifyClaim(_dropMsgSender(), _quantity, _currency, _pricePerToken, toVerifyMaxQuantityPerTransaction); + + if (validMerkleProof && _allowlistProof.maxQuantityInAllowlist > 0) { + /** + * Mark the claimer's use of their position in the allowlist. A spot in an allowlist + * can be used only once. + */ + usedAllowlistSpot[activeConditionId].set(uint256(uint160(_dropMsgSender()))); + } + + // Update contract state. + claimCondition.supplyClaimed += _quantity; + lastClaimTimestamp[activeConditionId][_dropMsgSender()] = block.timestamp; + + // If there's a price, collect price. + _collectPriceOnClaim(address(0), _quantity, _currency, _pricePerToken); + + // Mint the relevant NFTs to claimer. + uint256 startTokenId = _transferTokensOnClaim(_receiver, _quantity); + + emit TokensClaimed(_dropMsgSender(), _receiver, startTokenId, _quantity); + + _afterClaim(_receiver, _quantity, _currency, _pricePerToken, _allowlistProof, _data); + } + + /// @dev Lets a contract admin set claim conditions. + function setClaimConditions(ClaimCondition calldata _condition, bool _resetClaimEligibility) external override { + if (!_canSetClaimConditions()) { + revert("Not authorized"); + } + + bytes32 targetConditionId = conditionId; + uint256 supplyClaimedAlready = claimCondition.supplyClaimed; + + if (_resetClaimEligibility) { + supplyClaimedAlready = 0; + targetConditionId = keccak256(abi.encodePacked(_dropMsgSender(), block.number)); + } + + if (supplyClaimedAlready > _condition.maxClaimableSupply) { + revert("max supply claimed"); + } + + claimCondition = ClaimCondition({ + startTimestamp: _condition.startTimestamp, + maxClaimableSupply: _condition.maxClaimableSupply, + supplyClaimed: supplyClaimedAlready, + quantityLimitPerTransaction: _condition.quantityLimitPerTransaction, + waitTimeInSecondsBetweenClaims: _condition.waitTimeInSecondsBetweenClaims, + merkleRoot: _condition.merkleRoot, + pricePerToken: _condition.pricePerToken, + currency: _condition.currency + }); + conditionId = targetConditionId; + + emit ClaimConditionUpdated(_condition, _resetClaimEligibility); + } + + /// @dev Checks a request to claim NFTs against the active claim condition's criteria. + function verifyClaim( + address _claimer, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + bool verifyMaxQuantityPerTransaction + ) public view { + ClaimCondition memory currentClaimPhase = claimCondition; + + if (_currency != currentClaimPhase.currency || _pricePerToken != currentClaimPhase.pricePerToken) { + revert("Invalid price or currency"); + } + + // If we're checking for an allowlist quantity restriction, ignore the general quantity restriction. + if ( + _quantity == 0 || + (verifyMaxQuantityPerTransaction && _quantity > currentClaimPhase.quantityLimitPerTransaction) + ) { + revert("Invalid quantity"); + } + + if (currentClaimPhase.supplyClaimed + _quantity > currentClaimPhase.maxClaimableSupply) { + revert("exceeds max supply"); + } + + (uint256 lastClaimedAt, uint256 nextValidClaimTimestamp) = getClaimTimestamp(_claimer); + if ( + currentClaimPhase.startTimestamp > block.timestamp || + (lastClaimedAt != 0 && block.timestamp < nextValidClaimTimestamp) + ) { + revert("cant claim yet"); + } + } + + /// @dev Checks whether a claimer meets the claim condition's allowlist criteria. + function verifyClaimMerkleProof( + address _claimer, + uint256 _quantity, + AllowlistProof calldata _allowlistProof + ) public view returns (bool validMerkleProof, uint256 merkleProofIndex) { + ClaimCondition memory currentClaimPhase = claimCondition; + + if (currentClaimPhase.merkleRoot != bytes32(0)) { + (validMerkleProof, merkleProofIndex) = MerkleProof.verify( + _allowlistProof.proof, + currentClaimPhase.merkleRoot, + keccak256(abi.encodePacked(_claimer, _allowlistProof.maxQuantityInAllowlist)) + ); + if (!validMerkleProof) { + revert("not in allowlist"); + } + + if (usedAllowlistSpot[conditionId].get(uint256(uint160(_claimer)))) { + revert("proof claimed"); + } + + if (_allowlistProof.maxQuantityInAllowlist != 0 && _quantity > _allowlistProof.maxQuantityInAllowlist) { + revert("Invalid qty proof"); + } + } + } + + /// @dev Returns the timestamp for when a claimer is eligible for claiming NFTs again. + function getClaimTimestamp( + address _claimer + ) public view returns (uint256 lastClaimedAt, uint256 nextValidClaimTimestamp) { + lastClaimedAt = lastClaimTimestamp[conditionId][_claimer]; + + unchecked { + nextValidClaimTimestamp = lastClaimedAt + claimCondition.waitTimeInSecondsBetweenClaims; + + if (nextValidClaimTimestamp < lastClaimedAt) { + nextValidClaimTimestamp = type(uint256).max; + } + } + } + + /*//////////////////////////////////////////////////////////////////// + Optional hooks that can be implemented in the derived contract + ///////////////////////////////////////////////////////////////////*/ + + /// @dev Exposes the ability to override the msg sender. + function _dropMsgSender() internal virtual returns (address) { + return msg.sender; + } + + /// @dev Runs before every `claim` function call. + function _beforeClaim( + address _receiver, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof, + bytes memory _data + ) internal virtual {} + + /// @dev Runs after every `claim` function call. + function _afterClaim( + address _receiver, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof, + bytes memory _data + ) internal virtual {} + + /// @dev Collects and distributes the primary sale value of NFTs being claimed. + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal virtual; + + /// @dev Transfers the NFTs being claimed. + function _transferTokensOnClaim( + address _to, + uint256 _quantityBeingClaimed + ) internal virtual returns (uint256 startTokenId); + + function _canSetClaimConditions() internal view virtual returns (bool); +} diff --git a/contracts/legacy-contracts/extension/LazyMintWithTier_V1.sol b/contracts/legacy-contracts/extension/LazyMintWithTier_V1.sol new file mode 100644 index 000000000..2acda2134 --- /dev/null +++ b/contracts/legacy-contracts/extension/LazyMintWithTier_V1.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../../extension/interface/ILazyMintWithTier.sol"; +import "./BatchMintMetadata_V1.sol"; + +/** + * The `LazyMint` is a contract extension for any base NFT contract. It lets you 'lazy mint' any number of NFTs + * at once. Here, 'lazy mint' means defining the metadata for particular tokenIds of your NFT contract, without actually + * minting a non-zero balance of NFTs of those tokenIds. + */ + +abstract contract LazyMintWithTier_V1 is ILazyMintWithTier, BatchMintMetadata_V1 { + struct TokenRange { + uint256 startIdInclusive; + uint256 endIdNonInclusive; + } + + struct TierMetadata { + string tier; + TokenRange[] ranges; + string[] baseURIs; + } + + /// @notice The tokenId assigned to the next new NFT to be lazy minted. + uint256 internal nextTokenIdToLazyMint; + + /// @notice Mapping from a tier -> the token IDs grouped under that tier. + mapping(string => TokenRange[]) internal tokensInTier; + + /// @notice A list of tiers used in this contract. + string[] private tiers; + + /** + * @notice Lets an authorized address lazy mint a given amount of NFTs. + * + * @param _amount The number of NFTs to lazy mint. + * @param _baseURIForTokens The base URI for the 'n' number of NFTs being lazy minted, where the metadata for each + * of those NFTs is `${baseURIForTokens}/${tokenId}`. + * @param _data Additional bytes data to be used at the discretion of the consumer of the contract. + * @return batchId A unique integer identifier for the batch of NFTs lazy minted together. + */ + function lazyMint( + uint256 _amount, + string calldata _baseURIForTokens, + string calldata _tier, + bytes calldata _data + ) public virtual override returns (uint256 batchId) { + if (!_canLazyMint()) { + revert("Not authorized"); + } + + if (_amount == 0) { + revert("0 amt"); + } + + uint256 startId = nextTokenIdToLazyMint; + + (nextTokenIdToLazyMint, batchId) = _batchMintMetadata(startId, _amount, _baseURIForTokens); + + // Handle tier info. + if (!(tokensInTier[_tier].length > 0)) { + tiers.push(_tier); + } + tokensInTier[_tier].push(TokenRange(startId, batchId)); + + emit TokensLazyMinted(_tier, startId, startId + _amount - 1, _baseURIForTokens, _data); + + return batchId; + } + + /// @notice Returns all metadata lazy minted for the given tier. + function _getMetadataInTier( + string memory _tier + ) private view returns (TokenRange[] memory tokens, string[] memory baseURIs) { + tokens = tokensInTier[_tier]; + + uint256 len = tokens.length; + baseURIs = new string[](len); + + for (uint256 i = 0; i < len; i += 1) { + baseURIs[i] = _getBaseURI(tokens[i].startIdInclusive); + } + } + + /// @notice Returns all metadata for all tiers created on the contract. + function getMetadataForAllTiers() external view returns (TierMetadata[] memory metadataForAllTiers) { + string[] memory allTiers = tiers; + uint256 len = allTiers.length; + + metadataForAllTiers = new TierMetadata[](len); + + for (uint256 i = 0; i < len; i += 1) { + (TokenRange[] memory tokens, string[] memory baseURIs) = _getMetadataInTier(allTiers[i]); + metadataForAllTiers[i] = TierMetadata(allTiers[i], tokens, baseURIs); + } + } + + /** + * @notice Returns whether any metadata is lazy minted for the given tier. + * + * @param _tier We check whether this given tier is empty. + */ + function isTierEmpty(string memory _tier) internal view returns (bool) { + return tokensInTier[_tier].length == 0; + } + + /// @dev Returns whether lazy minting can be performed in the given execution context. + function _canLazyMint() internal view virtual returns (bool); +} diff --git a/contracts/legacy-contracts/extension/LazyMint_V1.sol b/contracts/legacy-contracts/extension/LazyMint_V1.sol new file mode 100644 index 000000000..820ea4eed --- /dev/null +++ b/contracts/legacy-contracts/extension/LazyMint_V1.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../../extension/interface/ILazyMint.sol"; +import "./BatchMintMetadata_V1.sol"; + +/** + * The `LazyMint` is a contract extension for any base NFT contract. It lets you 'lazy mint' any number of NFTs + * at once. Here, 'lazy mint' means defining the metadata for particular tokenIds of your NFT contract, without actually + * minting a non-zero balance of NFTs of those tokenIds. + */ + +abstract contract LazyMint_V1 is ILazyMint, BatchMintMetadata_V1 { + /// @notice The tokenId assigned to the next new NFT to be lazy minted. + uint256 internal nextTokenIdToLazyMint; + + /** + * @notice Lets an authorized address lazy mint a given amount of NFTs. + * + * @param _amount The number of NFTs to lazy mint. + * @param _baseURIForTokens The base URI for the 'n' number of NFTs being lazy minted, where the metadata for each + * of those NFTs is `${baseURIForTokens}/${tokenId}`. + * @param _data Additional bytes data to be used at the discretion of the consumer of the contract. + * @return batchId A unique integer identifier for the batch of NFTs lazy minted together. + */ + function lazyMint( + uint256 _amount, + string calldata _baseURIForTokens, + bytes calldata _data + ) public virtual override returns (uint256 batchId) { + if (!_canLazyMint()) { + revert("Not authorized"); + } + + if (_amount == 0) { + revert("0 amt"); + } + + uint256 startId = nextTokenIdToLazyMint; + + (nextTokenIdToLazyMint, batchId) = _batchMintMetadata(startId, _amount, _baseURIForTokens); + + emit TokensLazyMinted(startId, startId + _amount - 1, _baseURIForTokens, _data); + + return batchId; + } + + /// @dev Returns whether lazy minting can be performed in the given execution context. + function _canLazyMint() internal view virtual returns (bool); +} diff --git a/contracts/legacy-contracts/extension/PlatformFee_V1.sol b/contracts/legacy-contracts/extension/PlatformFee_V1.sol new file mode 100644 index 000000000..9e3af425f --- /dev/null +++ b/contracts/legacy-contracts/extension/PlatformFee_V1.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./interface/IPlatformFee_V1.sol"; + +/** + * @title Platform Fee + * @notice Thirdweb's `PlatformFee` is a contract extension to be used with any base contract. It exposes functions for setting and reading + * the recipient of platform fee and the platform fee basis points, and lets the inheriting contract perform conditional logic + * that uses information about platform fees, if desired. + */ + +abstract contract PlatformFee is IPlatformFee { + /// @dev The sender is not authorized to perform the action + error PlatformFeeUnauthorized(); + + /// @dev The recipient is invalid + error PlatformFeeInvalidRecipient(address recipient); + + /// @dev The fee bps exceeded the max value + error PlatformFeeExceededMaxFeeBps(uint256 max, uint256 actual); + + /// @dev The address that receives all platform fees from all sales. + address private platformFeeRecipient; + + /// @dev The % of primary sales collected as platform fees. + uint16 private platformFeeBps; + + /// @dev Returns the platform fee recipient and bps. + function getPlatformFeeInfo() public view override returns (address, uint16) { + return (platformFeeRecipient, uint16(platformFeeBps)); + } + + /** + * @notice Updates the platform fee recipient and bps. + * @dev Caller should be authorized to set platform fee info. + * See {_canSetPlatformFeeInfo}. + * Emits {PlatformFeeInfoUpdated Event}; See {_setupPlatformFeeInfo}. + + * @param _platformFeeRecipient Address to be set as new platformFeeRecipient. + * @param _platformFeeBps Updated platformFeeBps. + */ + function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) external override { + if (!_canSetPlatformFeeInfo()) { + revert PlatformFeeUnauthorized(); + } + _setupPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } + + /// @dev Sets the platform fee recipient and bps + function _setupPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) internal { + if (_platformFeeBps > 10_000) { + revert PlatformFeeExceededMaxFeeBps(10_000, _platformFeeBps); + } + if (_platformFeeRecipient == address(0)) { + revert PlatformFeeInvalidRecipient(_platformFeeRecipient); + } + + platformFeeBps = uint16(_platformFeeBps); + platformFeeRecipient = _platformFeeRecipient; + + emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); + } + + /// @dev Returns whether platform fee info can be set in the given execution context. + function _canSetPlatformFeeInfo() internal view virtual returns (bool); +} diff --git a/contracts/legacy-contracts/extension/PrimarySale_V1.sol b/contracts/legacy-contracts/extension/PrimarySale_V1.sol new file mode 100644 index 000000000..165501754 --- /dev/null +++ b/contracts/legacy-contracts/extension/PrimarySale_V1.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./interface/IPrimarySale_V1.sol"; + +/** + * @title Primary Sale + * @notice Thirdweb's `PrimarySale` is a contract extension to be used with any base contract. It exposes functions for setting and reading + * the recipient of primary sales, and lets the inheriting contract perform conditional logic that uses information about + * primary sales, if desired. + */ + +abstract contract PrimarySale is IPrimarySale { + /// @dev The sender is not authorized to perform the action + error PrimarySaleUnauthorized(); + + /// @dev The recipient is invalid + error PrimarySaleInvalidRecipient(address recipient); + + /// @dev The address that receives all primary sales value. + address private recipient; + + /// @dev Returns primary sale recipient address. + function primarySaleRecipient() public view override returns (address) { + return recipient; + } + + /** + * @notice Updates primary sale recipient. + * @dev Caller should be authorized to set primary sales info. + * See {_canSetPrimarySaleRecipient}. + * Emits {PrimarySaleRecipientUpdated Event}; See {_setupPrimarySaleRecipient}. + * + * @param _saleRecipient Address to be set as new recipient of primary sales. + */ + function setPrimarySaleRecipient(address _saleRecipient) external override { + if (!_canSetPrimarySaleRecipient()) { + revert PrimarySaleUnauthorized(); + } + _setupPrimarySaleRecipient(_saleRecipient); + } + + /// @dev Lets a contract admin set the recipient for all primary sales. + function _setupPrimarySaleRecipient(address _saleRecipient) internal { + recipient = _saleRecipient; + emit PrimarySaleRecipientUpdated(_saleRecipient); + } + + /// @dev Returns whether primary sale recipient can be set in the given execution context. + function _canSetPrimarySaleRecipient() internal view virtual returns (bool); +} diff --git a/contracts/legacy-contracts/extension/interface/IClaimCondition_V1.sol b/contracts/legacy-contracts/extension/interface/IClaimCondition_V1.sol new file mode 100644 index 000000000..7615ded78 --- /dev/null +++ b/contracts/legacy-contracts/extension/interface/IClaimCondition_V1.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "../../../lib/BitMaps.sol"; + +/** + * Thirdweb's 'Drop' contracts are distribution mechanisms for tokens. + * + * A contract admin (i.e. a holder of `DEFAULT_ADMIN_ROLE`) can set a series of claim conditions, + * ordered by their respective `startTimestamp`. A claim condition defines criteria under which + * accounts can mint tokens. Claim conditions can be overwritten or added to by the contract admin. + * At any moment, there is only one active claim condition. + */ + +interface IClaimCondition_V1 { + /** + * @notice The criteria that make up a claim condition. + * + * @param startTimestamp The unix timestamp after which the claim condition applies. + * The same claim condition applies until the `startTimestamp` + * of the next claim condition. + * + * @param maxClaimableSupply The maximum total number of tokens that can be claimed under + * the claim condition. + * + * @param supplyClaimed At any given point, the number of tokens that have been claimed + * under the claim condition. + * + * @param quantityLimitPerTransaction The maximum number of tokens that can be claimed in a single + * transaction. + * + * @param waitTimeInSecondsBetweenClaims The least number of seconds an account must wait after claiming + * tokens, to be able to claim tokens again. + * + * @param merkleRoot The allowlist of addresses that can claim tokens under the claim + * condition. + * + * @param pricePerToken The price required to pay per token claimed. + * + * @param currency The currency in which the `pricePerToken` must be paid. + */ + struct ClaimCondition { + uint256 startTimestamp; + uint256 maxClaimableSupply; + uint256 supplyClaimed; + uint256 quantityLimitPerTransaction; + uint256 waitTimeInSecondsBetweenClaims; + bytes32 merkleRoot; + uint256 pricePerToken; + address currency; + } +} diff --git a/contracts/legacy-contracts/extension/interface/IDropSinglePhase1155_V1.sol b/contracts/legacy-contracts/extension/interface/IDropSinglePhase1155_V1.sol new file mode 100644 index 000000000..0cf271b46 --- /dev/null +++ b/contracts/legacy-contracts/extension/interface/IDropSinglePhase1155_V1.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./IClaimCondition_V1.sol"; + +interface IDropSinglePhase1155_V1 is IClaimCondition_V1 { + struct AllowlistProof { + bytes32[] proof; + uint256 maxQuantityInAllowlist; + } + + /// @dev Emitted when tokens are claimed via `claim`. + event TokensClaimed( + address indexed claimer, + address indexed receiver, + uint256 indexed tokenId, + uint256 quantityClaimed + ); + + /// @dev Emitted when the contract's claim conditions are updated. + event ClaimConditionUpdated(uint256 indexed tokenId, ClaimCondition condition, bool resetEligibility); + + /** + * @notice Lets an account claim a given quantity of NFTs. + * + * @param tokenId The tokenId of the NFT to claim. + * @param receiver The receiver of the NFT to claim. + * @param quantity The quantity of the NFT to claim. + * @param currency The currency in which to pay for the claim. + * @param pricePerToken The price per token to pay for the claim. + * @param allowlistProof The proof of the claimer's inclusion in the merkle root allowlist + * of the claim conditions that apply. + * @param data Arbitrary bytes data that can be leveraged in the implementation of this interface. + */ + function claim( + address receiver, + uint256 tokenId, + uint256 quantity, + address currency, + uint256 pricePerToken, + AllowlistProof calldata allowlistProof, + bytes memory data + ) external payable; + + /** + * @notice Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. + * + * @param phase Claim condition to set. + * + * @param resetClaimEligibility Whether to reset `limitLastClaimTimestamp` and `limitMerkleProofClaim` values when setting new + * claim conditions. + * + * @param tokenId The tokenId for which to set the relevant claim condition. + */ + function setClaimConditions(uint256 tokenId, ClaimCondition calldata phase, bool resetClaimEligibility) external; +} diff --git a/contracts/legacy-contracts/extension/interface/IDropSinglePhase_V1.sol b/contracts/legacy-contracts/extension/interface/IDropSinglePhase_V1.sol new file mode 100644 index 000000000..753455b84 --- /dev/null +++ b/contracts/legacy-contracts/extension/interface/IDropSinglePhase_V1.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./IClaimCondition_V1.sol"; + +interface IDropSinglePhase_V1 is IClaimCondition_V1 { + struct AllowlistProof { + bytes32[] proof; + uint256 maxQuantityInAllowlist; + } + + /// @dev Emitted when tokens are claimed via `claim`. + event TokensClaimed( + address indexed claimer, + address indexed receiver, + uint256 indexed startTokenId, + uint256 quantityClaimed + ); + + /// @dev Emitted when the contract's claim conditions are updated. + event ClaimConditionUpdated(ClaimCondition condition, bool resetEligibility); + + /** + * @notice Lets an account claim a given quantity of NFTs. + * + * @param receiver The receiver of the NFTs to claim. + * @param quantity The quantity of NFTs to claim. + * @param currency The currency in which to pay for the claim. + * @param pricePerToken The price per token to pay for the claim. + * @param allowlistProof The proof of the claimer's inclusion in the merkle root allowlist + * of the claim conditions that apply. + * @param data Arbitrary bytes data that can be leveraged in the implementation of this interface. + */ + function claim( + address receiver, + uint256 quantity, + address currency, + uint256 pricePerToken, + AllowlistProof calldata allowlistProof, + bytes memory data + ) external payable; + + /** + * @notice Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. + * + * @param phase Claim condition to set. + * + * @param resetClaimEligibility Whether to reset `limitLastClaimTimestamp` and `limitMerkleProofClaim` values when setting new + * claim conditions. + */ + function setClaimConditions(ClaimCondition calldata phase, bool resetClaimEligibility) external; +} diff --git a/contracts/legacy-contracts/extension/interface/IPlatformFee_V1.sol b/contracts/legacy-contracts/extension/interface/IPlatformFee_V1.sol new file mode 100644 index 000000000..28932effa --- /dev/null +++ b/contracts/legacy-contracts/extension/interface/IPlatformFee_V1.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +/** + * Thirdweb's `PlatformFee` is a contract extension to be used with any base contract. It exposes functions for setting and reading + * the recipient of platform fee and the platform fee basis points, and lets the inheriting contract perform conditional logic + * that uses information about platform fees, if desired. + */ + +interface IPlatformFee { + /// @dev Returns the platform fee bps and recipient. + function getPlatformFeeInfo() external view returns (address, uint16); + + /// @dev Lets a module admin update the fees on primary sales. + function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) external; + + /// @dev Emitted when fee on primary sales is updated. + event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps); +} diff --git a/contracts/legacy-contracts/extension/interface/IPrimarySale_V1.sol b/contracts/legacy-contracts/extension/interface/IPrimarySale_V1.sol new file mode 100644 index 000000000..6ca726842 --- /dev/null +++ b/contracts/legacy-contracts/extension/interface/IPrimarySale_V1.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +/** + * Thirdweb's `Primary` is a contract extension to be used with any base contract. It exposes functions for setting and reading + * the recipient of primary sales, and lets the inheriting contract perform conditional logic that uses information about + * primary sales, if desired. + */ + +interface IPrimarySale { + /// @dev The adress that receives all primary sales value. + function primarySaleRecipient() external view returns (address); + + /// @dev Lets a module admin set the default recipient of all primary sales. + function setPrimarySaleRecipient(address _saleRecipient) external; + + /// @dev Emitted when a new sale recipient is set. + event PrimarySaleRecipientUpdated(address indexed recipient); +} diff --git a/contracts/legacy-contracts/interface/ISignatureMintERC721_V1.sol b/contracts/legacy-contracts/interface/ISignatureMintERC721_V1.sol new file mode 100644 index 000000000..e22061e86 --- /dev/null +++ b/contracts/legacy-contracts/interface/ISignatureMintERC721_V1.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +import "../../prebuilts/interface/token/ITokenERC721.sol"; + +interface ISignatureMintERC721_V1 { + function mintWithSignature( + ITokenERC721.MintRequest calldata _req, + bytes calldata _signature + ) external payable returns (uint256 tokenIdMinted); + + function verify( + ITokenERC721.MintRequest calldata _req, + bytes calldata _signature + ) external view returns (bool, address); +} diff --git a/contracts/legacy-contracts/interface/drop/IDropClaimCondition_V2.sol b/contracts/legacy-contracts/interface/drop/IDropClaimCondition_V2.sol new file mode 100644 index 000000000..240465a0c --- /dev/null +++ b/contracts/legacy-contracts/interface/drop/IDropClaimCondition_V2.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "@openzeppelin/contracts-upgradeable/utils/structs/BitMapsUpgradeable.sol"; + +/** + * Thirdweb's 'Drop' contracts are distribution mechanisms for tokens. + * + * A contract admin (i.e. a holder of `DEFAULT_ADMIN_ROLE`) can set a series of claim conditions, + * ordered by their respective `startTimestamp`. A claim condition defines criteria under which + * accounts can mint tokens. Claim conditions can be overwritten or added to by the contract admin. + * At any moment, there is only one active claim condition. + */ + +interface IDropClaimCondition_V2 { + /** + * @notice The criteria that make up a claim condition. + * + * @param startTimestamp The unix timestamp after which the claim condition applies. + * The same claim condition applies until the `startTimestamp` + * of the next claim condition. + * + * @param maxClaimableSupply The maximum total number of tokens that can be claimed under + * the claim condition. + * + * @param supplyClaimed At any given point, the number of tokens that have been claimed + * under the claim condition. + * + * @param quantityLimitPerTransaction The maximum number of tokens that can be claimed in a single + * transaction. + * + * @param waitTimeInSecondsBetweenClaims The least number of seconds an account must wait after claiming + * tokens, to be able to claim tokens again. + * + * @param merkleRoot The allowlist of addresses that can claim tokens under the claim + * condition. + * + * @param pricePerToken The price required to pay per token claimed. + * + * @param currency The currency in which the `pricePerToken` must be paid. + */ + struct ClaimCondition { + uint256 startTimestamp; + uint256 maxClaimableSupply; + uint256 supplyClaimed; + uint256 quantityLimitPerTransaction; + uint256 waitTimeInSecondsBetweenClaims; + bytes32 merkleRoot; + uint256 pricePerToken; + address currency; + } + + /** + * @notice The set of all claim conditions, at any given moment. + * Claim Phase ID = [currentStartId, currentStartId + length - 1]; + * + * @param currentStartId The uid for the first claim condition amongst the current set of + * claim conditions. The uid for each next claim condition is one + * more than the previous claim condition's uid. + * + * @param count The total number of phases / claim conditions in the list + * of claim conditions. + * + * @param phases The claim conditions at a given uid. Claim conditions + * are ordered in an ascending order by their `startTimestamp`. + * + * @param limitLastClaimTimestamp Map from an account and uid for a claim condition, to the last timestamp + * at which the account claimed tokens under that claim condition. + * + * @param limitMerkleProofClaim Map from a claim condition uid to whether an address in an allowlist + * has already claimed tokens i.e. used their place in the allowlist. + */ + struct ClaimConditionList { + uint256 currentStartId; + uint256 count; + mapping(uint256 => ClaimCondition) phases; + mapping(uint256 => mapping(address => uint256)) limitLastClaimTimestamp; + mapping(uint256 => BitMapsUpgradeable.BitMap) limitMerkleProofClaim; + } +} diff --git a/contracts/legacy-contracts/interface/drop/IDropERC1155_V2.sol b/contracts/legacy-contracts/interface/drop/IDropERC1155_V2.sol new file mode 100644 index 000000000..9184580db --- /dev/null +++ b/contracts/legacy-contracts/interface/drop/IDropERC1155_V2.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +import "@openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155Upgradeable.sol"; +import "./IDropClaimCondition_V2.sol"; + +/** + * Thirdweb's 'Drop' contracts are distribution mechanisms for tokens. The + * `DropERC721` contract is a distribution mechanism for ERC721 tokens. + * + * A minter wallet (i.e. holder of `MINTER_ROLE`) can (lazy)mint 'n' tokens + * at once by providing a single base URI for all tokens being lazy minted. + * The URI for each of the 'n' tokens lazy minted is the provided base URI + + * `{tokenId}` of the respective token. (e.g. "ipsf://Qmece.../1"). + * + * A minter can choose to lazy mint 'delayed-reveal' tokens. More on 'delayed-reveal' + * tokens in [this article](https://blog.thirdweb.com/delayed-reveal-nfts). + * + * A contract admin (i.e. holder of `DEFAULT_ADMIN_ROLE`) can create claim conditions + * with non-overlapping time windows, and accounts can claim the tokens according to + * restrictions defined in the claim condition that is active at the time of the transaction. + */ + +interface IDropERC1155_V2 is IERC1155Upgradeable, IDropClaimCondition_V2 { + /// @dev Emitted when tokens are claimed. + event TokensClaimed( + uint256 indexed claimConditionIndex, + uint256 indexed tokenId, + address indexed claimer, + address receiver, + uint256 quantityClaimed + ); + + /// @dev Emitted when tokens are lazy minted. + event TokensLazyMinted(uint256 startTokenId, uint256 endTokenId, string baseURI); + + /// @dev Emitted when new claim conditions are set for a token. + event ClaimConditionsUpdated(uint256 indexed tokenId, ClaimCondition[] claimConditions); + + /// @dev Emitted when the global max supply of a token is updated. + event MaxTotalSupplyUpdated(uint256 tokenId, uint256 maxTotalSupply); + + /// @dev Emitted when the wallet claim count for a given tokenId and address is updated. + event WalletClaimCountUpdated(uint256 tokenId, address indexed wallet, uint256 count); + + /// @dev Emitted when the max wallet claim count for a given tokenId is updated. + event MaxWalletClaimCountUpdated(uint256 tokenId, uint256 count); + + /// @dev Emitted when the sale recipient for a particular tokenId is updated. + event SaleRecipientForTokenUpdated(uint256 indexed tokenId, address saleRecipient); + + /** + * @notice Lets an account with `MINTER_ROLE` lazy mint 'n' NFTs. + * The URIs for each token is the provided `_baseURIForTokens` + `{tokenId}`. + * + * @param amount The amount of NFTs to lazy mint. + * @param baseURIForTokens The URI for the NFTs to lazy mint. + */ + function lazyMint(uint256 amount, string calldata baseURIForTokens) external; + + /** + * @notice Lets an account claim a given quantity of NFTs. + * + * @param receiver The receiver of the NFTs to claim. + * @param tokenId The unique ID of the token to claim. + * @param quantity The quantity of NFTs to claim. + * @param currency The currency in which to pay for the claim. + * @param pricePerToken The price per token to pay for the claim. + * @param proofs The proof of the claimer's inclusion in the merkle root allowlist + * of the claim conditions that apply. + * @param proofMaxQuantityPerTransaction (Optional) The maximum number of NFTs an address included in an + * allowlist can claim. + */ + function claim( + address receiver, + uint256 tokenId, + uint256 quantity, + address currency, + uint256 pricePerToken, + bytes32[] calldata proofs, + uint256 proofMaxQuantityPerTransaction + ) external payable; + + /** + * @notice Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. + * + * @param tokenId The token ID for which to set mint conditions. + * @param phases Claim conditions in ascending order by `startTimestamp`. + * @param resetClaimEligibility Whether to reset `limitLastClaimTimestamp` and + * `limitMerkleProofClaim` values when setting new + * claim conditions. + */ + function setClaimConditions(uint256 tokenId, ClaimCondition[] calldata phases, bool resetClaimEligibility) external; +} diff --git a/contracts/legacy-contracts/interface/drop/IDropERC20_V2.sol b/contracts/legacy-contracts/interface/drop/IDropERC20_V2.sol new file mode 100644 index 000000000..d0b40e526 --- /dev/null +++ b/contracts/legacy-contracts/interface/drop/IDropERC20_V2.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "./IDropClaimCondition_V2.sol"; + +/** + * Thirdweb's 'Drop' contracts are distribution mechanisms for tokens. The + * `DropERC20` contract is a distribution mechanism for ERC20 tokens. + * + * A contract admin (i.e. holder of `DEFAULT_ADMIN_ROLE`) can create claim conditions + * with non-overlapping time windows, and accounts can claim the tokens according to + * restrictions defined in the claim condition that is active at the time of the transaction. + */ + +interface IDropERC20_V2 is IERC20Upgradeable, IDropClaimCondition_V2 { + /// @dev Emitted when tokens are claimed. + event TokensClaimed( + uint256 indexed claimConditionIndex, + address indexed claimer, + address indexed receiver, + uint256 quantityClaimed + ); + + /// @dev Emitted when new claim conditions are set. + event ClaimConditionsUpdated(ClaimCondition[] claimConditions); + + /// @dev Emitted when the global max supply of tokens is updated. + event MaxTotalSupplyUpdated(uint256 maxTotalSupply); + + /// @dev Emitted when the wallet claim count for an address is updated. + event WalletClaimCountUpdated(address indexed wallet, uint256 count); + + /// @dev Emitted when the global max wallet claim count is updated. + event MaxWalletClaimCountUpdated(uint256 count); + + /// @dev Emitted when the contract URI is updated. + event ContractURIUpdated(string prevURI, string newURI); + + /** + * @notice Lets an account claim a given quantity of tokens. + * + * @param receiver The receiver of the tokens to claim. + * @param quantity The quantity of tokens to claim. + * @param currency The currency in which to pay for the claim. + * @param pricePerToken The price per token (i.e. price per 1 ether unit of the token) + * to pay for the claim. + * @param proofs The proof of the claimer's inclusion in the merkle root allowlist + * of the claim conditions that apply. + * @param proofMaxQuantityPerTransaction (Optional) The maximum number of tokens an address included in an + * allowlist can claim. + */ + function claim( + address receiver, + uint256 quantity, + address currency, + uint256 pricePerToken, + bytes32[] calldata proofs, + uint256 proofMaxQuantityPerTransaction + ) external payable; + + /** + * @notice Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. + * + * @param phases Claim conditions in ascending order by `startTimestamp`. + * @param resetClaimEligibility Whether to reset `limitLastClaimTimestamp` and + * `limitMerkleProofClaim` values when setting new + * claim conditions. + */ + function setClaimConditions(ClaimCondition[] calldata phases, bool resetClaimEligibility) external; +} diff --git a/contracts/legacy-contracts/interface/drop/IDropERC721_V3.sol b/contracts/legacy-contracts/interface/drop/IDropERC721_V3.sol new file mode 100644 index 000000000..7148c2b4d --- /dev/null +++ b/contracts/legacy-contracts/interface/drop/IDropERC721_V3.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; +import "./IDropClaimCondition_V2.sol"; + +/** + * Thirdweb's 'Drop' contracts are distribution mechanisms for tokens. The + * `DropERC721` contract is a distribution mechanism for ERC721 tokens. + * + * A minter wallet (i.e. holder of `MINTER_ROLE`) can (lazy)mint 'n' tokens + * at once by providing a single base URI for all tokens being lazy minted. + * The URI for each of the 'n' tokens lazy minted is the provided base URI + + * `{tokenId}` of the respective token. (e.g. "ipsf://Qmece.../1"). + * + * A minter can choose to lazy mint 'delayed-reveal' tokens. More on 'delayed-reveal' + * tokens in [this article](https://blog.thirdweb.com/delayed-reveal-nfts). + * + * A contract admin (i.e. holder of `DEFAULT_ADMIN_ROLE`) can create claim conditions + * with non-overlapping time windows, and accounts can claim the tokens according to + * restrictions defined in the claim condition that is active at the time of the transaction. + */ + +interface IDropERC721_V3 is IERC721Upgradeable, IDropClaimCondition_V2 { + /// @dev Emitted when tokens are claimed. + event TokensClaimed( + uint256 indexed claimConditionIndex, + address indexed claimer, + address indexed receiver, + uint256 startTokenId, + uint256 quantityClaimed + ); + + /// @dev Emitted when tokens are lazy minted. + event TokensLazyMinted(uint256 startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI); + + /// @dev Emitted when the URI for a batch of 'delayed-reveal' NFTs is revealed. + event NFTRevealed(uint256 endTokenId, string revealedURI); + + /// @dev Emitted when new claim conditions are set. + event ClaimConditionsUpdated(ClaimCondition[] claimConditions); + + /// @dev Emitted when the global max supply of tokens is updated. + event MaxTotalSupplyUpdated(uint256 maxTotalSupply); + + /// @dev Emitted when the wallet claim count for an address is updated. + event WalletClaimCountUpdated(address indexed wallet, uint256 count); + + /// @dev Emitted when the global max wallet claim count is updated. + event MaxWalletClaimCountUpdated(uint256 count); + + /** + * @notice Lets an account with `MINTER_ROLE` lazy mint 'n' NFTs. + * The URIs for each token is the provided `_baseURIForTokens` + `{tokenId}`. + * + * @param amount The amount of NFTs to lazy mint. + * @param baseURIForTokens The URI for the NFTs to lazy mint. If lazy minting + * 'delayed-reveal' NFTs, the is a URI for NFTs in the + * un-revealed state. + * @param encryptedBaseURI If lazy minting 'delayed-reveal' NFTs, this is the + * result of encrypting the URI of the NFTs in the revealed + * state. + */ + function lazyMint(uint256 amount, string calldata baseURIForTokens, bytes calldata encryptedBaseURI) external; + + /** + * @notice Lets an account claim a given quantity of NFTs. + * + * @param receiver The receiver of the NFTs to claim. + * @param quantity The quantity of NFTs to claim. + * @param currency The currency in which to pay for the claim. + * @param pricePerToken The price per token to pay for the claim. + * @param proofs The proof of the claimer's inclusion in the merkle root allowlist + * of the claim conditions that apply. + * @param proofMaxQuantityPerTransaction (Optional) The maximum number of NFTs an address included in an + * allowlist can claim. + */ + function claim( + address receiver, + uint256 quantity, + address currency, + uint256 pricePerToken, + bytes32[] calldata proofs, + uint256 proofMaxQuantityPerTransaction + ) external payable; + + /** + * @notice Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. + * + * @param phases Claim conditions in ascending order by `startTimestamp`. + * @param resetClaimEligibility Whether to reset `limitLastClaimTimestamp` and + * `limitMerkleProofClaim` values when setting new + * claim conditions. + */ + function setClaimConditions(ClaimCondition[] calldata phases, bool resetClaimEligibility) external; +} diff --git a/contracts/legacy-contracts/pre-builts/DropERC1155_V2.sol b/contracts/legacy-contracts/pre-builts/DropERC1155_V2.sol new file mode 100644 index 000000000..2d8f04971 --- /dev/null +++ b/contracts/legacy-contracts/pre-builts/DropERC1155_V2.sol @@ -0,0 +1,731 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// ========== External imports ========== + +import "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; + +import "@openzeppelin/contracts-upgradeable/utils/structs/BitMapsUpgradeable.sol"; +import "../../extension/Multicall.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; + +import "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; + +// ========== Internal imports ========== + +import "../../infra/interface/IThirdwebContract.sol"; + +// ========== Features ========== + +import "../../extension/interface/IPlatformFee.sol"; +import "../../extension/interface/IPrimarySale.sol"; +import "../../extension/interface/IRoyalty.sol"; +import "../../extension/interface/IOwnable.sol"; + +import { IDropERC1155_V2 } from "../interface/drop/IDropERC1155_V2.sol"; + +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; + +import "../../lib/CurrencyTransferLib.sol"; +import "../../lib/FeeType.sol"; +import "../../lib/MerkleProof.sol"; + +contract DropERC1155_V2 is + Initializable, + IThirdwebContract, + IOwnable, + IRoyalty, + IPrimarySale, + IPlatformFee, + ReentrancyGuardUpgradeable, + ERC2771ContextUpgradeable, + Multicall, + AccessControlEnumerableUpgradeable, + ERC1155Upgradeable, + IDropERC1155_V2 +{ + using BitMapsUpgradeable for BitMapsUpgradeable.BitMap; + using StringsUpgradeable for uint256; + + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + bytes32 private constant MODULE_TYPE = bytes32("DropERC1155"); + uint256 private constant VERSION = 2; + + // Token name + string public name; + + // Token symbol + string public symbol; + + /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. + bytes32 private constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE"); + /// @dev Only MINTER_ROLE holders can lazy mint NFTs. + bytes32 private constant MINTER_ROLE = keccak256("MINTER_ROLE"); + + /// @dev Max bps in the thirdweb system + uint256 private constant MAX_BPS = 10_000; + + /// @dev Owner of the contract (purpose: OpenSea compatibility) + address private _owner; + + // @dev The next token ID of the NFT to "lazy mint". + uint256 public nextTokenIdToMint; + + /// @dev The address that receives all primary sales value. + address public primarySaleRecipient; + + /// @dev The address that receives all platform fees from all sales. + address private platformFeeRecipient; + + /// @dev The % of primary sales collected as platform fees. + uint16 private platformFeeBps; + + /// @dev The recipient of who gets the royalty. + address private royaltyRecipient; + + /// @dev The (default) address that receives all royalty value. + uint16 private royaltyBps; + + /// @dev Contract level metadata. + string public contractURI; + + /// @dev Largest tokenId of each batch of tokens with the same baseURI + uint256[] private baseURIIndices; + + /*/////////////////////////////////////////////////////////////// + Mappings + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Mapping from 'Largest tokenId of a batch of tokens with the same baseURI' + * to base URI for the respective batch of tokens. + **/ + mapping(uint256 => string) private baseURI; + + /// @dev Mapping from token ID => total circulating supply of tokens with that ID. + mapping(uint256 => uint256) public totalSupply; + + /// @dev Mapping from token ID => maximum possible total circulating supply of tokens with that ID. + mapping(uint256 => uint256) public maxTotalSupply; + + /// @dev Mapping from token ID => the set of all claim conditions, at any given moment, for tokens of the token ID. + mapping(uint256 => ClaimConditionList) public claimCondition; + + /// @dev Mapping from token ID => the address of the recipient of primary sales. + mapping(uint256 => address) public saleRecipient; + + /// @dev Mapping from token ID => royalty recipient and bps for tokens of the token ID. + mapping(uint256 => RoyaltyInfo) private royaltyInfoForToken; + + /// @dev Mapping from token ID => claimer wallet address => total number of NFTs of the token ID a wallet has claimed. + mapping(uint256 => mapping(address => uint256)) public walletClaimCount; + + /// @dev Mapping from token ID => the max number of NFTs of the token ID a wallet can claim. + mapping(uint256 => uint256) public maxWalletClaimCount; + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + constructor() initializer {} + + /// @dev Initializes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _royaltyRecipient, + uint128 _royaltyBps, + uint128 _platformFeeBps, + address _platformFeeRecipient + ) external initializer { + // Initialize inherited contracts, most base-like -> most derived. + __ReentrancyGuard_init(); + __ERC2771Context_init_unchained(_trustedForwarders); + __ERC1155_init_unchained(""); + + // Initialize this contract's state. + name = _name; + symbol = _symbol; + royaltyRecipient = _royaltyRecipient; + royaltyBps = uint16(_royaltyBps); + platformFeeRecipient = _platformFeeRecipient; + primarySaleRecipient = _saleRecipient; + contractURI = _contractURI; + platformFeeBps = uint16(_platformFeeBps); + _owner = _defaultAdmin; + + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + _setupRole(MINTER_ROLE, _defaultAdmin); + _setupRole(TRANSFER_ROLE, _defaultAdmin); + _setupRole(TRANSFER_ROLE, address(0)); + } + + /*/////////////////////////////////////////////////////////////// + Generic contract logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the type of the contract. + function contractType() external pure returns (bytes32) { + return MODULE_TYPE; + } + + /// @dev Returns the version of the contract. + function contractVersion() external pure returns (uint8) { + return uint8(VERSION); + } + + /** + * @dev Returns the address of the current owner. + */ + function owner() public view returns (address) { + return hasRole(DEFAULT_ADMIN_ROLE, _owner) ? _owner : address(0); + } + + /*/////////////////////////////////////////////////////////////// + ERC 165 / 1155 / 2981 logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the URI for a given tokenId. + function uri(uint256 _tokenId) public view override returns (string memory _tokenURI) { + for (uint256 i = 0; i < baseURIIndices.length; i += 1) { + if (_tokenId < baseURIIndices[i]) { + return string(abi.encodePacked(baseURI[baseURIIndices[i]], _tokenId.toString())); + } + } + + return ""; + } + + /// @dev See ERC 165 + function supportsInterface( + bytes4 interfaceId + ) + public + view + virtual + override(ERC1155Upgradeable, AccessControlEnumerableUpgradeable, IERC165Upgradeable, IERC165) + returns (bool) + { + return super.supportsInterface(interfaceId) || type(IERC2981Upgradeable).interfaceId == interfaceId; + } + + /// @dev Returns the royalty recipient and amount, given a tokenId and sale price. + function royaltyInfo( + uint256 tokenId, + uint256 salePrice + ) external view virtual returns (address receiver, uint256 royaltyAmount) { + (address recipient, uint256 bps) = getRoyaltyInfoForToken(tokenId); + receiver = recipient; + royaltyAmount = (salePrice * bps) / MAX_BPS; + } + + /*/////////////////////////////////////////////////////////////// + Minting logic + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Lets an account with `MINTER_ROLE` lazy mint 'n' NFTs. + * The URIs for each token is the provided `_baseURIForTokens` + `{tokenId}`. + */ + function lazyMint(uint256 _amount, string calldata _baseURIForTokens) external onlyRole(MINTER_ROLE) { + uint256 startId = nextTokenIdToMint; + uint256 baseURIIndex = startId + _amount; + + nextTokenIdToMint = baseURIIndex; + baseURI[baseURIIndex] = _baseURIForTokens; + baseURIIndices.push(baseURIIndex); + + emit TokensLazyMinted(startId, startId + _amount - 1, _baseURIForTokens); + } + + /*/////////////////////////////////////////////////////////////// + Claim logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Lets an account claim a given quantity of NFTs, of a single tokenId. + function claim( + address _receiver, + uint256 _tokenId, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + bytes32[] calldata _proofs, + uint256 _proofMaxQuantityPerTransaction + ) external payable nonReentrant { + require(isTrustedForwarder(msg.sender) || _msgSender() == tx.origin, "BOT"); + + // Get the active claim condition index. + uint256 activeConditionId = getActiveClaimConditionId(_tokenId); + + /** + * We make allowlist checks (i.e. verifyClaimMerkleProof) before verifying the claim's general + * validity (i.e. verifyClaim) because we give precedence to the check of allow list quantity + * restriction over the check of the general claim condition's quantityLimitPerTransaction + * restriction. + */ + + // Verify inclusion in allowlist. + (bool validMerkleProof, ) = verifyClaimMerkleProof( + activeConditionId, + _msgSender(), + _tokenId, + _quantity, + _proofs, + _proofMaxQuantityPerTransaction + ); + + // Verify claim validity. If not valid, revert. + // when there's allowlist present --> verifyClaimMerkleProof will verify the _proofMaxQuantityPerTransaction value with hashed leaf in the allowlist + // when there's no allowlist, this check is true --> verifyClaim will check for _quantity being less/equal than the limit + bool toVerifyMaxQuantityPerTransaction = _proofMaxQuantityPerTransaction == 0 || + claimCondition[_tokenId].phases[activeConditionId].merkleRoot == bytes32(0); + verifyClaim( + activeConditionId, + _msgSender(), + _tokenId, + _quantity, + _currency, + _pricePerToken, + toVerifyMaxQuantityPerTransaction + ); + + if (validMerkleProof && _proofMaxQuantityPerTransaction > 0) { + /** + * Mark the claimer's use of their position in the allowlist. A spot in an allowlist + * can be used only once. + */ + claimCondition[_tokenId].limitMerkleProofClaim[activeConditionId].set(uint256(uint160(_msgSender()))); + } + + // If there's a price, collect price. + collectClaimPrice(_quantity, _currency, _pricePerToken, _tokenId); + + // Mint the relevant tokens to claimer. + transferClaimedTokens(_receiver, activeConditionId, _tokenId, _quantity); + + emit TokensClaimed(activeConditionId, _tokenId, _msgSender(), _receiver, _quantity); + } + + /// @dev Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions, for a tokenId. + function setClaimConditions( + uint256 _tokenId, + ClaimCondition[] calldata _phases, + bool _resetClaimEligibility + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + ClaimConditionList storage condition = claimCondition[_tokenId]; + uint256 existingStartIndex = condition.currentStartId; + uint256 existingPhaseCount = condition.count; + + /** + * `limitLastClaimTimestamp` and `limitMerkleProofClaim` are mappings that use a + * claim condition's UID as a key. + * + * If `_resetClaimEligibility == true`, we assign completely new UIDs to the claim + * conditions in `_phases`, effectively resetting the restrictions on claims expressed + * by `limitLastClaimTimestamp` and `limitMerkleProofClaim`. + */ + uint256 newStartIndex = existingStartIndex; + if (_resetClaimEligibility) { + newStartIndex = existingStartIndex + existingPhaseCount; + } + + condition.count = _phases.length; + condition.currentStartId = newStartIndex; + + uint256 lastConditionStartTimestamp; + for (uint256 i = 0; i < _phases.length; i++) { + require( + i == 0 || lastConditionStartTimestamp < _phases[i].startTimestamp, + "startTimestamp must be in ascending order." + ); + + uint256 supplyClaimedAlready = condition.phases[newStartIndex + i].supplyClaimed; + require(supplyClaimedAlready <= _phases[i].maxClaimableSupply, "max supply claimed already"); + + condition.phases[newStartIndex + i] = _phases[i]; + condition.phases[newStartIndex + i].supplyClaimed = supplyClaimedAlready; + + lastConditionStartTimestamp = _phases[i].startTimestamp; + } + + /** + * Gas refunds (as much as possible) + * + * If `_resetClaimEligibility == true`, we assign completely new UIDs to the claim + * conditions in `_phases`. So, we delete claim conditions with UID < `newStartIndex`. + * + * If `_resetClaimEligibility == false`, and there are more existing claim conditions + * than in `_phases`, we delete the existing claim conditions that don't get replaced + * by the conditions in `_phases`. + */ + if (_resetClaimEligibility) { + for (uint256 i = existingStartIndex; i < newStartIndex; i++) { + delete condition.phases[i]; + delete condition.limitMerkleProofClaim[i]; + } + } else { + if (existingPhaseCount > _phases.length) { + for (uint256 i = _phases.length; i < existingPhaseCount; i++) { + delete condition.phases[newStartIndex + i]; + delete condition.limitMerkleProofClaim[newStartIndex + i]; + } + } + } + + emit ClaimConditionsUpdated(_tokenId, _phases); + } + + /// @dev Collects and distributes the primary sale value of NFTs being claimed. + function collectClaimPrice( + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken, + uint256 _tokenId + ) internal { + if (_pricePerToken == 0) { + return; + } + + uint256 totalPrice = _quantityToClaim * _pricePerToken; + uint256 platformFees = (totalPrice * platformFeeBps) / MAX_BPS; + + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { + require(msg.value == totalPrice, "must send total price."); + } + + address recipient = saleRecipient[_tokenId] == address(0) ? primarySaleRecipient : saleRecipient[_tokenId]; + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), platformFeeRecipient, platformFees); + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), recipient, totalPrice - platformFees); + } + + /// @dev Transfers the NFTs being claimed. + function transferClaimedTokens( + address _to, + uint256 _conditionId, + uint256 _tokenId, + uint256 _quantityBeingClaimed + ) internal { + // Update the supply minted under mint condition. + claimCondition[_tokenId].phases[_conditionId].supplyClaimed += _quantityBeingClaimed; + + // if transfer claimed tokens is called when to != msg.sender, it'd use msg.sender's limits. + // behavior would be similar to msg.sender mint for itself, then transfer to `to`. + claimCondition[_tokenId].limitLastClaimTimestamp[_conditionId][_msgSender()] = block.timestamp; + + walletClaimCount[_tokenId][_msgSender()] += _quantityBeingClaimed; + + _mint(_to, _tokenId, _quantityBeingClaimed, ""); + } + + /// @dev Checks a request to claim NFTs against the active claim condition's criteria. + function verifyClaim( + uint256 _conditionId, + address _claimer, + uint256 _tokenId, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + bool verifyMaxQuantityPerTransaction + ) public view { + ClaimCondition memory currentClaimPhase = claimCondition[_tokenId].phases[_conditionId]; + + require( + _currency == currentClaimPhase.currency && _pricePerToken == currentClaimPhase.pricePerToken, + "invalid currency or price specified." + ); + // If we're checking for an allowlist quantity restriction, ignore the general quantity restriction. + require( + _quantity > 0 && + (!verifyMaxQuantityPerTransaction || _quantity <= currentClaimPhase.quantityLimitPerTransaction), + "invalid quantity claimed." + ); + require( + currentClaimPhase.supplyClaimed + _quantity <= currentClaimPhase.maxClaimableSupply, + "exceed max mint supply." + ); + require( + maxTotalSupply[_tokenId] == 0 || totalSupply[_tokenId] + _quantity <= maxTotalSupply[_tokenId], + "exceed max total supply" + ); + require( + maxWalletClaimCount[_tokenId] == 0 || + walletClaimCount[_tokenId][_claimer] + _quantity <= maxWalletClaimCount[_tokenId], + "exceed claim limit for wallet" + ); + + (uint256 lastClaimTimestamp, uint256 nextValidClaimTimestamp) = getClaimTimestamp( + _tokenId, + _conditionId, + _claimer + ); + require(lastClaimTimestamp == 0 || block.timestamp >= nextValidClaimTimestamp, "cannot claim yet."); + } + + /// @dev Checks whether a claimer meets the claim condition's allowlist criteria. + function verifyClaimMerkleProof( + uint256 _conditionId, + address _claimer, + uint256 _tokenId, + uint256 _quantity, + bytes32[] calldata _proofs, + uint256 _proofMaxQuantityPerTransaction + ) public view returns (bool validMerkleProof, uint256 merkleProofIndex) { + ClaimCondition memory currentClaimPhase = claimCondition[_tokenId].phases[_conditionId]; + + if (currentClaimPhase.merkleRoot != bytes32(0)) { + (validMerkleProof, merkleProofIndex) = MerkleProof.verify( + _proofs, + currentClaimPhase.merkleRoot, + keccak256(abi.encodePacked(_claimer, _proofMaxQuantityPerTransaction)) + ); + require(validMerkleProof, "not in whitelist."); + require( + !claimCondition[_tokenId].limitMerkleProofClaim[_conditionId].get(uint256(uint160(_claimer))), + "proof claimed." + ); + require( + _proofMaxQuantityPerTransaction == 0 || _quantity <= _proofMaxQuantityPerTransaction, + "invalid quantity proof." + ); + } + } + + /*/////////////////////////////////////////////////////////////// + Getter functions + //////////////////////////////////////////////////////////////*/ + + /// @dev At any given moment, returns the uid for the active claim condition, for a given tokenId. + function getActiveClaimConditionId(uint256 _tokenId) public view returns (uint256) { + ClaimConditionList storage conditionList = claimCondition[_tokenId]; + for (uint256 i = conditionList.currentStartId + conditionList.count; i > conditionList.currentStartId; i--) { + if (block.timestamp >= conditionList.phases[i - 1].startTimestamp) { + return i - 1; + } + } + + revert("no active mint condition."); + } + + /// @dev Returns the platform fee recipient and bps. + function getPlatformFeeInfo() external view returns (address, uint16) { + return (platformFeeRecipient, uint16(platformFeeBps)); + } + + /// @dev Returns the default royalty recipient and bps. + function getDefaultRoyaltyInfo() external view returns (address, uint16) { + return (royaltyRecipient, uint16(royaltyBps)); + } + + /// @dev Returns the royalty recipient and bps for a particular token Id. + function getRoyaltyInfoForToken(uint256 _tokenId) public view returns (address, uint16) { + RoyaltyInfo memory royaltyForToken = royaltyInfoForToken[_tokenId]; + + return + royaltyForToken.recipient == address(0) + ? (royaltyRecipient, uint16(royaltyBps)) + : (royaltyForToken.recipient, uint16(royaltyForToken.bps)); + } + + /// @dev Returns the timestamp for when a claimer is eligible for claiming NFTs again. + function getClaimTimestamp( + uint256 _tokenId, + uint256 _conditionId, + address _claimer + ) public view returns (uint256 lastClaimTimestamp, uint256 nextValidClaimTimestamp) { + lastClaimTimestamp = claimCondition[_tokenId].limitLastClaimTimestamp[_conditionId][_claimer]; + + unchecked { + nextValidClaimTimestamp = + lastClaimTimestamp + + claimCondition[_tokenId].phases[_conditionId].waitTimeInSecondsBetweenClaims; + + if (nextValidClaimTimestamp < lastClaimTimestamp) { + nextValidClaimTimestamp = type(uint256).max; + } + } + } + + /// @dev Returns the claim condition at the given uid. + function getClaimConditionById( + uint256 _tokenId, + uint256 _conditionId + ) external view returns (ClaimCondition memory condition) { + condition = claimCondition[_tokenId].phases[_conditionId]; + } + + /*/////////////////////////////////////////////////////////////// + Setter functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Lets a contract admin set a claim count for a wallet. + function setWalletClaimCount( + uint256 _tokenId, + address _claimer, + uint256 _count + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + walletClaimCount[_tokenId][_claimer] = _count; + emit WalletClaimCountUpdated(_tokenId, _claimer, _count); + } + + /// @dev Lets a contract admin set a maximum number of NFTs of a tokenId that can be claimed by any wallet. + function setMaxWalletClaimCount(uint256 _tokenId, uint256 _count) external onlyRole(DEFAULT_ADMIN_ROLE) { + maxWalletClaimCount[_tokenId] = _count; + emit MaxWalletClaimCountUpdated(_tokenId, _count); + } + + /// @dev Lets a module admin set a max total supply for token. + function setMaxTotalSupply(uint256 _tokenId, uint256 _maxTotalSupply) external onlyRole(DEFAULT_ADMIN_ROLE) { + maxTotalSupply[_tokenId] = _maxTotalSupply; + emit MaxTotalSupplyUpdated(_tokenId, _maxTotalSupply); + } + + /// @dev Lets a contract admin set the recipient for all primary sales. + function setPrimarySaleRecipient(address _saleRecipient) external onlyRole(DEFAULT_ADMIN_ROLE) { + primarySaleRecipient = _saleRecipient; + emit PrimarySaleRecipientUpdated(_saleRecipient); + } + + /// @dev Lets a contract admin set the recipient for all primary sales. + function setSaleRecipientForToken(uint256 _tokenId, address _saleRecipient) external onlyRole(DEFAULT_ADMIN_ROLE) { + saleRecipient[_tokenId] = _saleRecipient; + emit SaleRecipientForTokenUpdated(_tokenId, _saleRecipient); + } + + /// @dev Lets a contract admin update the default royalty recipient and bps. + function setDefaultRoyaltyInfo( + address _royaltyRecipient, + uint256 _royaltyBps + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(_royaltyBps <= MAX_BPS, "exceed royalty bps"); + + royaltyRecipient = _royaltyRecipient; + royaltyBps = uint16(_royaltyBps); + + emit DefaultRoyalty(_royaltyRecipient, _royaltyBps); + } + + /// @dev Lets a contract admin set the royalty recipient and bps for a particular token Id. + function setRoyaltyInfoForToken( + uint256 _tokenId, + address _recipient, + uint256 _bps + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(_bps <= MAX_BPS, "exceed royalty bps"); + + royaltyInfoForToken[_tokenId] = RoyaltyInfo({ recipient: _recipient, bps: _bps }); + + emit RoyaltyForToken(_tokenId, _recipient, _bps); + } + + /// @dev Lets a contract admin update the platform fee recipient and bps + function setPlatformFeeInfo( + address _platformFeeRecipient, + uint256 _platformFeeBps + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(_platformFeeBps <= MAX_BPS, "bps <= 10000."); + + platformFeeBps = uint16(_platformFeeBps); + platformFeeRecipient = _platformFeeRecipient; + + emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); + } + + /// @dev Lets a contract admin set a new owner for the contract. The new owner must be a contract admin. + function setOwner(address _newOwner) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(hasRole(DEFAULT_ADMIN_ROLE, _newOwner), "new owner not module admin."); + emit OwnerUpdated(_owner, _newOwner); + _owner = _newOwner; + } + + /// @dev Lets a contract admin set the URI for contract-level metadata. + function setContractURI(string calldata _uri) external onlyRole(DEFAULT_ADMIN_ROLE) { + contractURI = _uri; + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + /// @dev Lets a token owner burn the tokens they own (i.e. destroy for good) + function burn(address account, uint256 id, uint256 value) public virtual { + require( + account == _msgSender() || isApprovedForAll(account, _msgSender()), + "ERC1155: caller is not owner nor approved." + ); + + _burn(account, id, value); + } + + /// @dev Lets a token owner burn multiple tokens they own at once (i.e. destroy for good) + function burnBatch(address account, uint256[] memory ids, uint256[] memory values) public virtual { + require( + account == _msgSender() || isApprovedForAll(account, _msgSender()), + "ERC1155: caller is not owner nor approved." + ); + + _burnBatch(account, ids, values); + } + + /** + * @dev See {ERC1155-_beforeTokenTransfer}. + */ + function _beforeTokenTransfer( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal virtual override { + super._beforeTokenTransfer(operator, from, to, ids, amounts, data); + + // if transfer is restricted on the contract, we still want to allow burning and minting + if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { + require(hasRole(TRANSFER_ROLE, from) || hasRole(TRANSFER_ROLE, to), "restricted to TRANSFER_ROLE holders."); + } + + if (from == address(0)) { + for (uint256 i = 0; i < ids.length; ++i) { + totalSupply[ids[i]] += amounts[i]; + } + } + + if (to == address(0)) { + for (uint256 i = 0; i < ids.length; ++i) { + totalSupply[ids[i]] -= amounts[i]; + } + } + } + + function _msgSender() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } + + function _msgData() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable) + returns (bytes calldata) + { + return ERC2771ContextUpgradeable._msgData(); + } +} diff --git a/contracts/legacy-contracts/pre-builts/DropERC20_V2.sol b/contracts/legacy-contracts/pre-builts/DropERC20_V2.sol new file mode 100644 index 000000000..72310658a --- /dev/null +++ b/contracts/legacy-contracts/pre-builts/DropERC20_V2.sol @@ -0,0 +1,521 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// ========== External imports ========== + +import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20VotesUpgradeable.sol"; + +import "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; + +import "@openzeppelin/contracts-upgradeable/utils/structs/BitMapsUpgradeable.sol"; +import "../../extension/Multicall.sol"; + +// ========== Internal imports ========== + +import "../../infra/interface/IThirdwebContract.sol"; + +// ========== Features ========== + +import "../../extension/interface/IPlatformFee.sol"; +import "../../extension/interface/IPrimarySale.sol"; + +import { IDropERC20_V2 } from "../interface/drop/IDropERC20_V2.sol"; + +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; + +import "../../lib/MerkleProof.sol"; +import "../../lib/CurrencyTransferLib.sol"; +import "../../lib/FeeType.sol"; + +contract DropERC20_V2 is + Initializable, + IThirdwebContract, + IPrimarySale, + IPlatformFee, + ReentrancyGuardUpgradeable, + ERC2771ContextUpgradeable, + Multicall, + AccessControlEnumerableUpgradeable, + ERC20BurnableUpgradeable, + ERC20VotesUpgradeable, + IDropERC20_V2 +{ + using BitMapsUpgradeable for BitMapsUpgradeable.BitMap; + + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + bytes32 private constant MODULE_TYPE = bytes32("DropERC20"); + uint128 private constant VERSION = 2; + + /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. + bytes32 private constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE"); + + /// @dev Contract level metadata. + string public contractURI; + + /// @dev Max bps in the thirdweb system. + uint128 internal constant MAX_BPS = 10_000; + + /// @dev The % of primary sales collected as platform fees. + uint128 internal platformFeeBps; + + /// @dev The address that receives all platform fees from all sales. + address internal platformFeeRecipient; + + /// @dev The address that receives all primary sales value. + address public primarySaleRecipient; + + /// @dev The max number of tokens a wallet can claim. + uint256 public maxWalletClaimCount; + + /// @dev Global max total supply of tokens. + uint256 public maxTotalSupply; + + /// @dev The set of all claim conditions, at any given moment. + ClaimConditionList public claimCondition; + + /*/////////////////////////////////////////////////////////////// + Mappings + //////////////////////////////////////////////////////////////*/ + + /// @dev Mapping from address => number of tokens a wallet has claimed. + mapping(address => uint256) public walletClaimCount; + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + constructor() initializer {} + + /// @dev Initializes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _primarySaleRecipient, + address _platformFeeRecipient, + uint256 _platformFeeBps + ) external initializer { + // Initialize inherited contracts, most base-like -> most derived. + __ERC2771Context_init_unchained(_trustedForwarders); + __ERC20Permit_init(_name); + __ERC20_init_unchained(_name, _symbol); + + // Initialize this contract's state. + contractURI = _contractURI; + primarySaleRecipient = _primarySaleRecipient; + platformFeeRecipient = _platformFeeRecipient; + platformFeeBps = uint128(_platformFeeBps); + + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + _setupRole(TRANSFER_ROLE, _defaultAdmin); + _setupRole(TRANSFER_ROLE, address(0)); + } + + /*/////////////////////////////////////////////////////////////// + Generic contract logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the type of the contract. + function contractType() external pure returns (bytes32) { + return MODULE_TYPE; + } + + /// @dev Returns the version of the contract. + function contractVersion() external pure returns (uint8) { + return uint8(VERSION); + } + + /*/////////////////////////////////////////////////////////////// + ERC 165 + ERC20 transfer hooks + //////////////////////////////////////////////////////////////*/ + + /// @dev See ERC 165 + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(AccessControlEnumerableUpgradeable) returns (bool) { + return super.supportsInterface(interfaceId); + } + + function _afterTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual override(ERC20Upgradeable, ERC20VotesUpgradeable) { + super._afterTokenTransfer(from, to, amount); + } + + /// @dev Runs on every transfer. + function _beforeTokenTransfer(address from, address to, uint256 amount) internal override(ERC20Upgradeable) { + super._beforeTokenTransfer(from, to, amount); + + if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { + require(hasRole(TRANSFER_ROLE, from) || hasRole(TRANSFER_ROLE, to), "transfers restricted."); + } + } + + /*/////////////////////////////////////////////////////////////// + Claim logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Lets an account claim tokens. + function claim( + address _receiver, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + bytes32[] calldata _proofs, + uint256 _proofMaxQuantityPerTransaction + ) external payable nonReentrant { + require(isTrustedForwarder(msg.sender) || _msgSender() == tx.origin, "BOT"); + + // Get the claim conditions. + uint256 activeConditionId = getActiveClaimConditionId(); + + /** + * We make allowlist checks (i.e. verifyClaimMerkleProof) before verifying the claim's general + * validity (i.e. verifyClaim) because we give precedence to the check of allow list quantity + * restriction over the check of the general claim condition's quantityLimitPerTransaction + * restriction. + */ + + // Verify inclusion in allowlist. + (bool validMerkleProof, ) = verifyClaimMerkleProof( + activeConditionId, + _msgSender(), + _quantity, + _proofs, + _proofMaxQuantityPerTransaction + ); + + // Verify claim validity. If not valid, revert. + // when there's allowlist present --> verifyClaimMerkleProof will verify the _proofMaxQuantityPerTransaction value with hashed leaf in the allowlist + // when there's no allowlist, this check is true --> verifyClaim will check for _quantity being less/equal than the limit + bool toVerifyMaxQuantityPerTransaction = _proofMaxQuantityPerTransaction == 0 || + claimCondition.phases[activeConditionId].merkleRoot == bytes32(0); + verifyClaim( + activeConditionId, + _msgSender(), + _quantity, + _currency, + _pricePerToken, + toVerifyMaxQuantityPerTransaction + ); + + if (validMerkleProof && _proofMaxQuantityPerTransaction > 0) { + /** + * Mark the claimer's use of their position in the allowlist. A spot in an allowlist + * can be used only once. + */ + claimCondition.limitMerkleProofClaim[activeConditionId].set(uint256(uint160(_msgSender()))); + } + + // If there's a price, collect price. + collectClaimPrice(_quantity, _currency, _pricePerToken); + + // Mint the relevant NFTs to claimer. + transferClaimedTokens(_receiver, activeConditionId, _quantity); + + emit TokensClaimed(activeConditionId, _msgSender(), _receiver, _quantity); + } + + /// @dev Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. + function setClaimConditions( + ClaimCondition[] calldata _phases, + bool _resetClaimEligibility + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + uint256 existingStartIndex = claimCondition.currentStartId; + uint256 existingPhaseCount = claimCondition.count; + + /** + * `limitLastClaimTimestamp` and `limitMerkleProofClaim` are mappings that use a + * claim condition's UID as a key. + * + * If `_resetClaimEligibility == true`, we assign completely new UIDs to the claim + * conditions in `_phases`, effectively resetting the restrictions on claims expressed + * by `limitLastClaimTimestamp` and `limitMerkleProofClaim`. + */ + uint256 newStartIndex = existingStartIndex; + if (_resetClaimEligibility) { + newStartIndex = existingStartIndex + existingPhaseCount; + } + + claimCondition.count = _phases.length; + claimCondition.currentStartId = newStartIndex; + + uint256 lastConditionStartTimestamp; + for (uint256 i = 0; i < _phases.length; i++) { + require( + i == 0 || lastConditionStartTimestamp < _phases[i].startTimestamp, + "startTimestamp must be in ascending order." + ); + + uint256 supplyClaimedAlready = claimCondition.phases[newStartIndex + i].supplyClaimed; + require(supplyClaimedAlready <= _phases[i].maxClaimableSupply, "max supply claimed already"); + + claimCondition.phases[newStartIndex + i] = _phases[i]; + claimCondition.phases[newStartIndex + i].supplyClaimed = supplyClaimedAlready; + + lastConditionStartTimestamp = _phases[i].startTimestamp; + } + + /** + * Gas refunds (as much as possible) + * + * If `_resetClaimEligibility == true`, we assign completely new UIDs to the claim + * conditions in `_phases`. So, we delete claim conditions with UID < `newStartIndex`. + * + * If `_resetClaimEligibility == false`, and there are more existing claim conditions + * than in `_phases`, we delete the existing claim conditions that don't get replaced + * by the conditions in `_phases`. + */ + if (_resetClaimEligibility) { + for (uint256 i = existingStartIndex; i < newStartIndex; i++) { + delete claimCondition.phases[i]; + delete claimCondition.limitMerkleProofClaim[i]; + } + } else { + if (existingPhaseCount > _phases.length) { + for (uint256 i = _phases.length; i < existingPhaseCount; i++) { + delete claimCondition.phases[newStartIndex + i]; + delete claimCondition.limitMerkleProofClaim[newStartIndex + i]; + } + } + } + + emit ClaimConditionsUpdated(_phases); + } + + /// @dev Collects and distributes the primary sale value of tokens being claimed. + function collectClaimPrice(uint256 _quantityToClaim, address _currency, uint256 _pricePerToken) internal { + if (_pricePerToken == 0) { + return; + } + + // `_pricePerToken` is interpreted as price per 1 ether unit of the ERC20 tokens. + uint256 totalPrice = (_quantityToClaim * _pricePerToken) / 1 ether; + require(totalPrice > 0, "quantity too low"); + + uint256 platformFees = (totalPrice * platformFeeBps) / MAX_BPS; + + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { + require(msg.value == totalPrice, "must send total price."); + } + + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), platformFeeRecipient, platformFees); + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), primarySaleRecipient, totalPrice - platformFees); + } + + /// @dev Transfers the tokens being claimed. + function transferClaimedTokens(address _to, uint256 _conditionId, uint256 _quantityBeingClaimed) internal { + // Update the supply minted under mint condition. + claimCondition.phases[_conditionId].supplyClaimed += _quantityBeingClaimed; + + // if transfer claimed tokens is called when to != msg.sender, it'd use msg.sender's limits. + // behavior would be similar to msg.sender mint for itself, then transfer to `to`. + claimCondition.limitLastClaimTimestamp[_conditionId][_msgSender()] = block.timestamp; + walletClaimCount[_msgSender()] += _quantityBeingClaimed; + + _mint(_to, _quantityBeingClaimed); + } + + /// @dev Checks a request to claim tokens against the active claim condition's criteria. + function verifyClaim( + uint256 _conditionId, + address _claimer, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + bool verifyMaxQuantityPerTransaction + ) public view { + ClaimCondition memory currentClaimPhase = claimCondition.phases[_conditionId]; + + require( + _currency == currentClaimPhase.currency && _pricePerToken == currentClaimPhase.pricePerToken, + "invalid currency or price specified." + ); + // If we're checking for an allowlist quantity restriction, ignore the general quantity restriction. + require( + _quantity > 0 && + (!verifyMaxQuantityPerTransaction || _quantity <= currentClaimPhase.quantityLimitPerTransaction), + "invalid quantity claimed." + ); + require( + currentClaimPhase.supplyClaimed + _quantity <= currentClaimPhase.maxClaimableSupply, + "exceed max mint supply." + ); + + uint256 _maxTotalSupply = maxTotalSupply; + uint256 _maxWalletClaimCount = maxWalletClaimCount; + require(_maxTotalSupply == 0 || totalSupply() + _quantity <= _maxTotalSupply, "exceed max total supply."); + require( + _maxWalletClaimCount == 0 || walletClaimCount[_claimer] + _quantity <= _maxWalletClaimCount, + "exceed claim limit for wallet" + ); + + (, uint256 nextValidClaimTimestamp) = getClaimTimestamp(_conditionId, _claimer); + require(block.timestamp >= nextValidClaimTimestamp, "cannot claim yet."); + } + + /// @dev Checks whether a claimer meets the claim condition's allowlist criteria. + function verifyClaimMerkleProof( + uint256 _conditionId, + address _claimer, + uint256 _quantity, + bytes32[] calldata _proofs, + uint256 _proofMaxQuantityPerTransaction + ) public view returns (bool validMerkleProof, uint256 merkleProofIndex) { + ClaimCondition memory currentClaimPhase = claimCondition.phases[_conditionId]; + + if (currentClaimPhase.merkleRoot != bytes32(0)) { + (validMerkleProof, merkleProofIndex) = MerkleProof.verify( + _proofs, + currentClaimPhase.merkleRoot, + keccak256(abi.encodePacked(_claimer, _proofMaxQuantityPerTransaction)) + ); + require(validMerkleProof, "not in whitelist."); + require( + !claimCondition.limitMerkleProofClaim[_conditionId].get(uint256(uint160(_claimer))), + "proof claimed." + ); + require( + _proofMaxQuantityPerTransaction == 0 || _quantity <= _proofMaxQuantityPerTransaction, + "invalid quantity proof." + ); + } + } + + /*/////////////////////////////////////////////////////////////// + Getter functions + //////////////////////////////////////////////////////////////*/ + + /// @dev At any given moment, returns the uid for the active claim condition. + function getActiveClaimConditionId() public view returns (uint256) { + for (uint256 i = claimCondition.currentStartId + claimCondition.count; i > claimCondition.currentStartId; i--) { + if (block.timestamp >= claimCondition.phases[i - 1].startTimestamp) { + return i - 1; + } + } + + revert("no active mint condition."); + } + + /// @dev Returns the timestamp for when a claimer is eligible for claiming tokens again. + function getClaimTimestamp( + uint256 _conditionId, + address _claimer + ) public view returns (uint256 lastClaimTimestamp, uint256 nextValidClaimTimestamp) { + lastClaimTimestamp = claimCondition.limitLastClaimTimestamp[_conditionId][_claimer]; + + if (lastClaimTimestamp != 0) { + unchecked { + nextValidClaimTimestamp = + lastClaimTimestamp + + claimCondition.phases[_conditionId].waitTimeInSecondsBetweenClaims; + + if (nextValidClaimTimestamp < lastClaimTimestamp) { + nextValidClaimTimestamp = type(uint256).max; + } + } + } + } + + /// @dev Returns the claim condition at the given uid. + function getClaimConditionById(uint256 _conditionId) external view returns (ClaimCondition memory condition) { + condition = claimCondition.phases[_conditionId]; + } + + /// @dev Returns the platform fee recipient and bps. + function getPlatformFeeInfo() external view returns (address, uint16) { + return (platformFeeRecipient, uint16(platformFeeBps)); + } + + /*/////////////////////////////////////////////////////////////// + Setter functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Lets a contract admin set a claim count for a wallet. + function setWalletClaimCount(address _claimer, uint256 _count) external onlyRole(DEFAULT_ADMIN_ROLE) { + walletClaimCount[_claimer] = _count; + emit WalletClaimCountUpdated(_claimer, _count); + } + + /// @dev Set a maximum number of tokens that can be claimed by any wallet. Must be parsed to 18 decimals when setting, by adding 18 zeros after the desired value. + function setMaxWalletClaimCount(uint256 _count) external onlyRole(DEFAULT_ADMIN_ROLE) { + maxWalletClaimCount = _count; + emit MaxWalletClaimCountUpdated(_count); + } + + /// @dev Set global maximum supply. Must be parsed to 18 decimals when setting, by adding 18 zeros after the desired value. + function setMaxTotalSupply(uint256 _maxTotalSupply) external onlyRole(DEFAULT_ADMIN_ROLE) { + maxTotalSupply = _maxTotalSupply; + emit MaxTotalSupplyUpdated(_maxTotalSupply); + } + + /// @dev Lets a contract admin set the recipient for all primary sales. + function setPrimarySaleRecipient(address _saleRecipient) external onlyRole(DEFAULT_ADMIN_ROLE) { + primarySaleRecipient = _saleRecipient; + emit PrimarySaleRecipientUpdated(_saleRecipient); + } + + /// @dev Lets a contract admin update the platform fee recipient and bps + function setPlatformFeeInfo( + address _platformFeeRecipient, + uint256 _platformFeeBps + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(_platformFeeBps <= MAX_BPS, "bps <= 10000."); + + platformFeeBps = uint64(_platformFeeBps); + platformFeeRecipient = _platformFeeRecipient; + + emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); + } + + /// @dev Lets a contract admin set the URI for contract-level metadata. + function setContractURI(string calldata _uri) external onlyRole(DEFAULT_ADMIN_ROLE) { + string memory prevURI = contractURI; + contractURI = _uri; + + emit ContractURIUpdated(prevURI, _uri); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function _mint(address account, uint256 amount) internal virtual override(ERC20Upgradeable, ERC20VotesUpgradeable) { + super._mint(account, amount); + } + + function _burn(address account, uint256 amount) internal virtual override(ERC20Upgradeable, ERC20VotesUpgradeable) { + super._burn(account, amount); + } + + function _msgSender() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } + + function _msgData() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable) + returns (bytes calldata) + { + return ERC2771ContextUpgradeable._msgData(); + } +} diff --git a/contracts/legacy-contracts/pre-builts/DropERC721_V3.sol b/contracts/legacy-contracts/pre-builts/DropERC721_V3.sol new file mode 100644 index 000000000..2c7534d6e --- /dev/null +++ b/contracts/legacy-contracts/pre-builts/DropERC721_V3.sol @@ -0,0 +1,745 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// ========== External imports ========== + +import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; + +import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; + +import "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/structs/BitMapsUpgradeable.sol"; +import "../../extension/Multicall.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; + +// ========== Internal imports ========== + +import { IDropERC721_V3 } from "../interface/drop/IDropERC721_V3.sol"; +import "../../infra/interface/IThirdwebContract.sol"; + +// ========== Features ========== + +import "../../extension/interface/IPlatformFee.sol"; +import "../../extension/interface/IPrimarySale.sol"; +import "../../extension/interface/IRoyalty.sol"; +import "../../extension/interface/IOwnable.sol"; + +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; + +import "../../lib/CurrencyTransferLib.sol"; +import "../../lib/FeeType.sol"; +import "../../lib/MerkleProof.sol"; + +contract DropERC721_V3 is + Initializable, + IThirdwebContract, + IOwnable, + IRoyalty, + IPrimarySale, + IPlatformFee, + ReentrancyGuardUpgradeable, + ERC2771ContextUpgradeable, + Multicall, + AccessControlEnumerableUpgradeable, + ERC721EnumerableUpgradeable, + IDropERC721_V3 +{ + using BitMapsUpgradeable for BitMapsUpgradeable.BitMap; + using StringsUpgradeable for uint256; + + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + bytes32 private constant MODULE_TYPE = bytes32("DropERC721"); + uint256 private constant VERSION = 3; + + /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. + bytes32 private constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE"); + /// @dev Only MINTER_ROLE holders can lazy mint NFTs. + bytes32 private constant MINTER_ROLE = keccak256("MINTER_ROLE"); + + /// @dev Max bps in the thirdweb system. + uint256 private constant MAX_BPS = 10_000; + + /// @dev Owner of the contract (purpose: OpenSea compatibility) + address private _owner; + + /// @dev The next token ID of the NFT to "lazy mint". + uint256 public nextTokenIdToMint; + + /// @dev The next token ID of the NFT that can be claimed. + uint256 public nextTokenIdToClaim; + + /// @dev The address that receives all primary sales value. + address public primarySaleRecipient; + + /// @dev The max number of NFTs a wallet can claim. + uint256 public maxWalletClaimCount; + + /// @dev Global max total supply of NFTs. + uint256 public maxTotalSupply; + + /// @dev The address that receives all platform fees from all sales. + address private platformFeeRecipient; + + /// @dev The % of primary sales collected as platform fees. + uint16 private platformFeeBps; + + /// @dev The (default) address that receives all royalty value. + address private royaltyRecipient; + + /// @dev The (default) % of a sale to take as royalty (in basis points). + uint16 private royaltyBps; + + /// @dev Contract level metadata. + string public contractURI; + + /// @dev Largest tokenId of each batch of tokens with the same baseURI + uint256[] public baseURIIndices; + + /// @dev The set of all claim conditions, at any given moment. + ClaimConditionList public claimCondition; + + /*/////////////////////////////////////////////////////////////// + Mappings + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Mapping from 'Largest tokenId of a batch of tokens with the same baseURI' + * to base URI for the respective batch of tokens. + **/ + mapping(uint256 => string) private baseURI; + + /** + * @dev Mapping from 'Largest tokenId of a batch of 'delayed-reveal' tokens with + * the same baseURI' to encrypted base URI for the respective batch of tokens. + **/ + mapping(uint256 => bytes) public encryptedData; + + /// @dev Mapping from address => total number of NFTs a wallet has claimed. + mapping(address => uint256) public walletClaimCount; + + /// @dev Token ID => royalty recipient and bps for token + mapping(uint256 => RoyaltyInfo) private royaltyInfoForToken; + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + constructor() initializer {} + + /// @dev Initializes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _royaltyRecipient, + uint128 _royaltyBps, + uint128 _platformFeeBps, + address _platformFeeRecipient + ) external initializer { + // Initialize inherited contracts, most base-like -> most derived. + __ReentrancyGuard_init(); + __ERC2771Context_init(_trustedForwarders); + __ERC721_init(_name, _symbol); + + // Initialize this contract's state. + royaltyRecipient = _royaltyRecipient; + royaltyBps = uint16(_royaltyBps); + platformFeeRecipient = _platformFeeRecipient; + platformFeeBps = uint16(_platformFeeBps); + primarySaleRecipient = _saleRecipient; + contractURI = _contractURI; + _owner = _defaultAdmin; + + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + _setupRole(MINTER_ROLE, _defaultAdmin); + _setupRole(TRANSFER_ROLE, _defaultAdmin); + _setupRole(TRANSFER_ROLE, address(0)); + } + + /*/////////////////////////////////////////////////////////////// + Generic contract logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the type of the contract. + function contractType() external pure returns (bytes32) { + return MODULE_TYPE; + } + + /// @dev Returns the version of the contract. + function contractVersion() external pure returns (uint8) { + return uint8(VERSION); + } + + /** + * @dev Returns the address of the current owner. + */ + function owner() public view returns (address) { + return hasRole(DEFAULT_ADMIN_ROLE, _owner) ? _owner : address(0); + } + + /*/////////////////////////////////////////////////////////////// + ERC 165 / 721 / 2981 logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the URI for a given tokenId. + function tokenURI(uint256 _tokenId) public view override returns (string memory) { + for (uint256 i = 0; i < baseURIIndices.length; i += 1) { + if (_tokenId < baseURIIndices[i]) { + if (encryptedData[baseURIIndices[i]].length != 0) { + return string(abi.encodePacked(baseURI[baseURIIndices[i]], "0")); + } else { + return string(abi.encodePacked(baseURI[baseURIIndices[i]], _tokenId.toString())); + } + } + } + + return ""; + } + + /// @dev See ERC 165 + function supportsInterface( + bytes4 interfaceId + ) + public + view + virtual + override(ERC721EnumerableUpgradeable, AccessControlEnumerableUpgradeable, IERC165Upgradeable, IERC165) + returns (bool) + { + return super.supportsInterface(interfaceId) || type(IERC2981Upgradeable).interfaceId == interfaceId; + } + + /// @dev Returns the royalty recipient and amount, given a tokenId and sale price. + function royaltyInfo( + uint256 tokenId, + uint256 salePrice + ) external view virtual returns (address receiver, uint256 royaltyAmount) { + (address recipient, uint256 bps) = getRoyaltyInfoForToken(tokenId); + receiver = recipient; + royaltyAmount = (salePrice * bps) / MAX_BPS; + } + + /*/////////////////////////////////////////////////////////////// + Minting + delayed-reveal logic + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Lets an account with `MINTER_ROLE` lazy mint 'n' NFTs. + * The URIs for each token is the provided `_baseURIForTokens` + `{tokenId}`. + */ + function lazyMint( + uint256 _amount, + string calldata _baseURIForTokens, + bytes calldata _data + ) external onlyRole(MINTER_ROLE) { + uint256 startId = nextTokenIdToMint; + uint256 baseURIIndex = startId + _amount; + + nextTokenIdToMint = baseURIIndex; + baseURI[baseURIIndex] = _baseURIForTokens; + baseURIIndices.push(baseURIIndex); + + if (_data.length > 0) { + (bytes memory encryptedURI, bytes32 provenanceHash) = abi.decode(_data, (bytes, bytes32)); + + if (encryptedURI.length != 0 && provenanceHash != "") { + encryptedData[baseURIIndex] = _data; + } + } + + emit TokensLazyMinted(startId, startId + _amount - 1, _baseURIForTokens, _data); + } + + /// @dev Lets an account with `MINTER_ROLE` reveal the URI for a batch of 'delayed-reveal' NFTs. + function reveal( + uint256 index, + bytes calldata _key + ) external onlyRole(MINTER_ROLE) returns (string memory revealedURI) { + require(index < baseURIIndices.length, "invalid index."); + + uint256 _index = baseURIIndices[index]; + bytes memory data = encryptedData[_index]; + (bytes memory encryptedURI, bytes32 provenanceHash) = abi.decode(data, (bytes, bytes32)); + + require(encryptedURI.length != 0, "nothing to reveal."); + + revealedURI = string(encryptDecrypt(encryptedURI, _key)); + + require(keccak256(abi.encodePacked(revealedURI, _key, block.chainid)) == provenanceHash, "Incorrect key"); + + baseURI[_index] = revealedURI; + delete encryptedData[_index]; + + emit NFTRevealed(_index, revealedURI); + + return revealedURI; + } + + /// @dev See: https://ethereum.stackexchange.com/questions/69825/decrypt-message-on-chain + function encryptDecrypt(bytes memory data, bytes calldata key) public pure returns (bytes memory result) { + // Store data length on stack for later use + uint256 length = data.length; + + // solhint-disable-next-line no-inline-assembly + assembly { + // Set result to free memory pointer + result := mload(0x40) + // Increase free memory pointer by lenght + 32 + mstore(0x40, add(add(result, length), 32)) + // Set result length + mstore(result, length) + } + + // Iterate over the data stepping by 32 bytes + for (uint256 i = 0; i < length; i += 32) { + // Generate hash of the key and offset + bytes32 hash = keccak256(abi.encodePacked(key, i)); + + bytes32 chunk; + // solhint-disable-next-line no-inline-assembly + assembly { + // Read 32-bytes data chunk + chunk := mload(add(data, add(i, 32))) + } + // XOR the chunk with hash + chunk ^= hash; + // solhint-disable-next-line no-inline-assembly + assembly { + // Write 32-byte encrypted chunk + mstore(add(result, add(i, 32)), chunk) + } + } + } + + /*/////////////////////////////////////////////////////////////// + Claim logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Lets an account claim NFTs. + function claim( + address _receiver, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + bytes32[] calldata _proofs, + uint256 _proofMaxQuantityPerTransaction + ) external payable nonReentrant { + require(isTrustedForwarder(msg.sender) || _msgSender() == tx.origin, "BOT"); + + uint256 tokenIdToClaim = nextTokenIdToClaim; + + // Get the claim conditions. + uint256 activeConditionId = getActiveClaimConditionId(); + + /** + * We make allowlist checks (i.e. verifyClaimMerkleProof) before verifying the claim's general + * validity (i.e. verifyClaim) because we give precedence to the check of allow list quantity + * restriction over the check of the general claim condition's quantityLimitPerTransaction + * restriction. + */ + + // Verify inclusion in allowlist. + (bool validMerkleProof, ) = verifyClaimMerkleProof( + activeConditionId, + _msgSender(), + _quantity, + _proofs, + _proofMaxQuantityPerTransaction + ); + + // Verify claim validity. If not valid, revert. + // when there's allowlist present --> verifyClaimMerkleProof will verify the _proofMaxQuantityPerTransaction value with hashed leaf in the allowlist + // when there's no allowlist, this check is true --> verifyClaim will check for _quantity being less/equal than the limit + bool toVerifyMaxQuantityPerTransaction = _proofMaxQuantityPerTransaction == 0 || + claimCondition.phases[activeConditionId].merkleRoot == bytes32(0); + verifyClaim( + activeConditionId, + _msgSender(), + _quantity, + _currency, + _pricePerToken, + toVerifyMaxQuantityPerTransaction + ); + + if (validMerkleProof && _proofMaxQuantityPerTransaction > 0) { + /** + * Mark the claimer's use of their position in the allowlist. A spot in an allowlist + * can be used only once. + */ + claimCondition.limitMerkleProofClaim[activeConditionId].set(uint256(uint160(_msgSender()))); + } + + // If there's a price, collect price. + collectClaimPrice(_quantity, _currency, _pricePerToken); + + // Mint the relevant NFTs to claimer. + transferClaimedTokens(_receiver, activeConditionId, _quantity); + + emit TokensClaimed(activeConditionId, _msgSender(), _receiver, tokenIdToClaim, _quantity); + } + + /// @dev Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. + function setClaimConditions( + ClaimCondition[] calldata _phases, + bool _resetClaimEligibility + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + uint256 existingStartIndex = claimCondition.currentStartId; + uint256 existingPhaseCount = claimCondition.count; + + /** + * `limitLastClaimTimestamp` and `limitMerkleProofClaim` are mappings that use a + * claim condition's UID as a key. + * + * If `_resetClaimEligibility == true`, we assign completely new UIDs to the claim + * conditions in `_phases`, effectively resetting the restrictions on claims expressed + * by `limitLastClaimTimestamp` and `limitMerkleProofClaim`. + */ + uint256 newStartIndex = existingStartIndex; + if (_resetClaimEligibility) { + newStartIndex = existingStartIndex + existingPhaseCount; + } + + claimCondition.count = _phases.length; + claimCondition.currentStartId = newStartIndex; + + uint256 lastConditionStartTimestamp; + for (uint256 i = 0; i < _phases.length; i++) { + require(i == 0 || lastConditionStartTimestamp < _phases[i].startTimestamp, "ST"); + + uint256 supplyClaimedAlready = claimCondition.phases[newStartIndex + i].supplyClaimed; + require(supplyClaimedAlready <= _phases[i].maxClaimableSupply, "max supply claimed already"); + + claimCondition.phases[newStartIndex + i] = _phases[i]; + claimCondition.phases[newStartIndex + i].supplyClaimed = supplyClaimedAlready; + + lastConditionStartTimestamp = _phases[i].startTimestamp; + } + + /** + * Gas refunds (as much as possible) + * + * If `_resetClaimEligibility == true`, we assign completely new UIDs to the claim + * conditions in `_phases`. So, we delete claim conditions with UID < `newStartIndex`. + * + * If `_resetClaimEligibility == false`, and there are more existing claim conditions + * than in `_phases`, we delete the existing claim conditions that don't get replaced + * by the conditions in `_phases`. + */ + if (_resetClaimEligibility) { + for (uint256 i = existingStartIndex; i < newStartIndex; i++) { + delete claimCondition.phases[i]; + delete claimCondition.limitMerkleProofClaim[i]; + } + } else { + if (existingPhaseCount > _phases.length) { + for (uint256 i = _phases.length; i < existingPhaseCount; i++) { + delete claimCondition.phases[newStartIndex + i]; + delete claimCondition.limitMerkleProofClaim[newStartIndex + i]; + } + } + } + + emit ClaimConditionsUpdated(_phases); + } + + /// @dev Collects and distributes the primary sale value of NFTs being claimed. + function collectClaimPrice(uint256 _quantityToClaim, address _currency, uint256 _pricePerToken) internal { + if (_pricePerToken == 0) { + return; + } + + uint256 totalPrice = _quantityToClaim * _pricePerToken; + uint256 platformFees = (totalPrice * platformFeeBps) / MAX_BPS; + + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { + require(msg.value == totalPrice, "must send total price."); + } + + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), platformFeeRecipient, platformFees); + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), primarySaleRecipient, totalPrice - platformFees); + } + + /// @dev Transfers the NFTs being claimed. + function transferClaimedTokens(address _to, uint256 _conditionId, uint256 _quantityBeingClaimed) internal { + // Update the supply minted under mint condition. + claimCondition.phases[_conditionId].supplyClaimed += _quantityBeingClaimed; + + // if transfer claimed tokens is called when `to != msg.sender`, it'd use msg.sender's limits. + // behavior would be similar to `msg.sender` mint for itself, then transfer to `_to`. + claimCondition.limitLastClaimTimestamp[_conditionId][_msgSender()] = block.timestamp; + walletClaimCount[_msgSender()] += _quantityBeingClaimed; + + uint256 tokenIdToClaim = nextTokenIdToClaim; + + for (uint256 i = 0; i < _quantityBeingClaimed; i += 1) { + _mint(_to, tokenIdToClaim); + tokenIdToClaim += 1; + } + + nextTokenIdToClaim = tokenIdToClaim; + } + + /// @dev Checks a request to claim NFTs against the active claim condition's criteria. + function verifyClaim( + uint256 _conditionId, + address _claimer, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + bool verifyMaxQuantityPerTransaction + ) public view { + ClaimCondition memory currentClaimPhase = claimCondition.phases[_conditionId]; + + require( + _currency == currentClaimPhase.currency && _pricePerToken == currentClaimPhase.pricePerToken, + "invalid currency or price." + ); + + // If we're checking for an allowlist quantity restriction, ignore the general quantity restriction. + require( + _quantity > 0 && + (!verifyMaxQuantityPerTransaction || _quantity <= currentClaimPhase.quantityLimitPerTransaction), + "invalid quantity." + ); + require( + currentClaimPhase.supplyClaimed + _quantity <= currentClaimPhase.maxClaimableSupply, + "exceed max claimable supply." + ); + require(nextTokenIdToClaim + _quantity <= nextTokenIdToMint, "not enough minted tokens."); + require(maxTotalSupply == 0 || nextTokenIdToClaim + _quantity <= maxTotalSupply, "exceed max total supply."); + require( + maxWalletClaimCount == 0 || walletClaimCount[_claimer] + _quantity <= maxWalletClaimCount, + "exceed claim limit" + ); + + (uint256 lastClaimTimestamp, uint256 nextValidClaimTimestamp) = getClaimTimestamp(_conditionId, _claimer); + require(lastClaimTimestamp == 0 || block.timestamp >= nextValidClaimTimestamp, "cannot claim."); + } + + /// @dev Checks whether a claimer meets the claim condition's allowlist criteria. + function verifyClaimMerkleProof( + uint256 _conditionId, + address _claimer, + uint256 _quantity, + bytes32[] calldata _proofs, + uint256 _proofMaxQuantityPerTransaction + ) public view returns (bool validMerkleProof, uint256 merkleProofIndex) { + ClaimCondition memory currentClaimPhase = claimCondition.phases[_conditionId]; + + if (currentClaimPhase.merkleRoot != bytes32(0)) { + (validMerkleProof, merkleProofIndex) = MerkleProof.verify( + _proofs, + currentClaimPhase.merkleRoot, + keccak256(abi.encodePacked(_claimer, _proofMaxQuantityPerTransaction)) + ); + require(validMerkleProof, "not in whitelist."); + require( + !claimCondition.limitMerkleProofClaim[_conditionId].get(uint256(uint160(_claimer))), + "proof claimed." + ); + require( + _proofMaxQuantityPerTransaction == 0 || _quantity <= _proofMaxQuantityPerTransaction, + "invalid quantity proof." + ); + } + } + + /*/////////////////////////////////////////////////////////////// + Getter functions + //////////////////////////////////////////////////////////////*/ + + /// @dev At any given moment, returns the uid for the active claim condition. + function getActiveClaimConditionId() public view returns (uint256) { + for (uint256 i = claimCondition.currentStartId + claimCondition.count; i > claimCondition.currentStartId; i--) { + if (block.timestamp >= claimCondition.phases[i - 1].startTimestamp) { + return i - 1; + } + } + + revert("!CONDITION."); + } + + /// @dev Returns the royalty recipient and bps for a particular token Id. + function getRoyaltyInfoForToken(uint256 _tokenId) public view returns (address, uint16) { + RoyaltyInfo memory royaltyForToken = royaltyInfoForToken[_tokenId]; + + return + royaltyForToken.recipient == address(0) + ? (royaltyRecipient, uint16(royaltyBps)) + : (royaltyForToken.recipient, uint16(royaltyForToken.bps)); + } + + /// @dev Returns the platform fee recipient and bps. + function getPlatformFeeInfo() external view returns (address, uint16) { + return (platformFeeRecipient, uint16(platformFeeBps)); + } + + /// @dev Returns the default royalty recipient and bps. + function getDefaultRoyaltyInfo() external view returns (address, uint16) { + return (royaltyRecipient, uint16(royaltyBps)); + } + + /// @dev Returns the timestamp for when a claimer is eligible for claiming NFTs again. + function getClaimTimestamp( + uint256 _conditionId, + address _claimer + ) public view returns (uint256 lastClaimTimestamp, uint256 nextValidClaimTimestamp) { + lastClaimTimestamp = claimCondition.limitLastClaimTimestamp[_conditionId][_claimer]; + + unchecked { + nextValidClaimTimestamp = + lastClaimTimestamp + + claimCondition.phases[_conditionId].waitTimeInSecondsBetweenClaims; + + if (nextValidClaimTimestamp < lastClaimTimestamp) { + nextValidClaimTimestamp = type(uint256).max; + } + } + } + + /// @dev Returns the claim condition at the given uid. + function getClaimConditionById(uint256 _conditionId) external view returns (ClaimCondition memory condition) { + condition = claimCondition.phases[_conditionId]; + } + + /// @dev Returns the amount of stored baseURIs + function getBaseURICount() external view returns (uint256) { + return baseURIIndices.length; + } + + /*/////////////////////////////////////////////////////////////// + Setter functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Lets a contract admin set a claim count for a wallet. + function setWalletClaimCount(address _claimer, uint256 _count) external onlyRole(DEFAULT_ADMIN_ROLE) { + walletClaimCount[_claimer] = _count; + emit WalletClaimCountUpdated(_claimer, _count); + } + + /// @dev Lets a contract admin set a maximum number of NFTs that can be claimed by any wallet. + function setMaxWalletClaimCount(uint256 _count) external onlyRole(DEFAULT_ADMIN_ROLE) { + maxWalletClaimCount = _count; + emit MaxWalletClaimCountUpdated(_count); + } + + /// @dev Lets a contract admin set the global maximum supply for collection's NFTs. + function setMaxTotalSupply(uint256 _maxTotalSupply) external onlyRole(DEFAULT_ADMIN_ROLE) { + maxTotalSupply = _maxTotalSupply; + emit MaxTotalSupplyUpdated(_maxTotalSupply); + } + + /// @dev Lets a contract admin set the recipient for all primary sales. + function setPrimarySaleRecipient(address _saleRecipient) external onlyRole(DEFAULT_ADMIN_ROLE) { + primarySaleRecipient = _saleRecipient; + emit PrimarySaleRecipientUpdated(_saleRecipient); + } + + /// @dev Lets a contract admin update the default royalty recipient and bps. + function setDefaultRoyaltyInfo( + address _royaltyRecipient, + uint256 _royaltyBps + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(_royaltyBps <= MAX_BPS, "> MAX_BPS"); + + royaltyRecipient = _royaltyRecipient; + royaltyBps = uint16(_royaltyBps); + + emit DefaultRoyalty(_royaltyRecipient, _royaltyBps); + } + + /// @dev Lets a contract admin set the royalty recipient and bps for a particular token Id. + function setRoyaltyInfoForToken( + uint256 _tokenId, + address _recipient, + uint256 _bps + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(_bps <= MAX_BPS, "> MAX_BPS"); + + royaltyInfoForToken[_tokenId] = RoyaltyInfo({ recipient: _recipient, bps: _bps }); + + emit RoyaltyForToken(_tokenId, _recipient, _bps); + } + + /// @dev Lets a contract admin update the platform fee recipient and bps + function setPlatformFeeInfo( + address _platformFeeRecipient, + uint256 _platformFeeBps + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(_platformFeeBps <= MAX_BPS, "> MAX_BPS."); + + platformFeeBps = uint16(_platformFeeBps); + platformFeeRecipient = _platformFeeRecipient; + + emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); + } + + /// @dev Lets a contract admin set a new owner for the contract. The new owner must be a contract admin. + function setOwner(address _newOwner) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(hasRole(DEFAULT_ADMIN_ROLE, _newOwner), "!ADMIN"); + address _prevOwner = _owner; + _owner = _newOwner; + + emit OwnerUpdated(_prevOwner, _newOwner); + } + + /// @dev Lets a contract admin set the URI for contract-level metadata. + function setContractURI(string calldata _uri) external onlyRole(DEFAULT_ADMIN_ROLE) { + contractURI = _uri; + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + /// @dev Burns `tokenId`. See {ERC721-_burn}. + function burn(uint256 tokenId) public virtual { + //solhint-disable-next-line max-line-length + require(_isApprovedOrOwner(_msgSender(), tokenId), "caller not owner nor approved"); + _burn(tokenId); + } + + /// @dev See {ERC721-_beforeTokenTransfer}. + function _beforeTokenTransfer( + address from, + address to, + uint256 tokenId, + uint256 batchSize + ) internal virtual override(ERC721EnumerableUpgradeable) { + super._beforeTokenTransfer(from, to, tokenId, batchSize); + + // if transfer is restricted on the contract, we still want to allow burning and minting + if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { + require(hasRole(TRANSFER_ROLE, from) || hasRole(TRANSFER_ROLE, to), "!TRANSFER_ROLE"); + } + } + + function _msgSender() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } + + function _msgData() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable) + returns (bytes calldata) + { + return ERC2771ContextUpgradeable._msgData(); + } +} diff --git a/contracts/legacy-contracts/pre-builts/SignatureDrop_V4.sol b/contracts/legacy-contracts/pre-builts/SignatureDrop_V4.sol new file mode 100644 index 000000000..4b3e313bc --- /dev/null +++ b/contracts/legacy-contracts/pre-builts/SignatureDrop_V4.sol @@ -0,0 +1,360 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// ========== External imports ========== + +import "../../extension/Multicall.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; + +import "erc721a-upgradeable/contracts/ERC721AUpgradeable.sol"; + +// ========== Internal imports ========== + +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; +import "../../lib/CurrencyTransferLib.sol"; + +// ========== Features ========== + +import "../../extension/ContractMetadata.sol"; +import "../../extension/PlatformFee.sol"; +import "../../extension/Royalty.sol"; +import "../../extension/PrimarySale.sol"; +import "../../extension/Ownable.sol"; +import "../../extension/DelayedReveal.sol"; +import "../extension/LazyMint_V1.sol"; +import "../../extension/PermissionsEnumerable.sol"; +import "../extension/DropSinglePhase_V1.sol"; +import "../../extension/SignatureMintERC721Upgradeable.sol"; + +contract SignatureDrop_V4 is + Initializable, + ContractMetadata, + PlatformFee, + Royalty, + PrimarySale, + Ownable, + DelayedReveal, + LazyMint_V1, + PermissionsEnumerable, + DropSinglePhase_V1, + SignatureMintERC721Upgradeable, + ERC2771ContextUpgradeable, + Multicall, + ERC721AUpgradeable +{ + using StringsUpgradeable for uint256; + + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. + bytes32 private transferRole; + /// @dev Only MINTER_ROLE holders can sign off on `MintRequest`s and lazy mint tokens. + bytes32 private minterRole; + + /// @dev Max bps in the thirdweb system. + uint256 private constant MAX_BPS = 10_000; + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Initializes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _royaltyRecipient, + uint128 _royaltyBps, + uint128 _platformFeeBps, + address _platformFeeRecipient + ) external initializer { + transferRole = keccak256("TRANSFER_ROLE"); + minterRole = keccak256("MINTER_ROLE"); + + // Initialize inherited contracts, most base-like -> most derived. + __ERC2771Context_init(_trustedForwarders); + __ERC721A_init(_name, _symbol); + __SignatureMintERC721_init(); + + _setupContractURI(_contractURI); + _setupOwner(_defaultAdmin); + + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + _setupRole(minterRole, _defaultAdmin); + _setupRole(transferRole, _defaultAdmin); + _setupRole(transferRole, address(0)); + + _setupPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + _setupPrimarySaleRecipient(_saleRecipient); + } + + /*/////////////////////////////////////////////////////////////// + ERC 165 / 721 / 2981 logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the URI for a given tokenId. + function tokenURI(uint256 _tokenId) public view override returns (string memory) { + (uint256 batchId, ) = _getBatchId(_tokenId); + string memory batchUri = _getBaseURI(_tokenId); + + if (isEncryptedBatch(batchId)) { + return string(abi.encodePacked(batchUri, "0")); + } else { + return string(abi.encodePacked(batchUri, _tokenId.toString())); + } + } + + /// @dev See ERC 165 + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC721AUpgradeable, IERC165) returns (bool) { + return super.supportsInterface(interfaceId) || type(IERC2981Upgradeable).interfaceId == interfaceId; + } + + function contractType() external pure returns (bytes32) { + return bytes32("SignatureDrop"); + } + + function contractVersion() external pure returns (uint8) { + return uint8(4); + } + + /*/////////////////////////////////////////////////////////////// + Lazy minting + delayed-reveal logic + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Lets an account with `MINTER_ROLE` lazy mint 'n' NFTs. + * The URIs for each token is the provided `_baseURIForTokens` + `{tokenId}`. + */ + function lazyMint( + uint256 _amount, + string calldata _baseURIForTokens, + bytes calldata _data + ) public override returns (uint256 batchId) { + if (_data.length > 0) { + (bytes memory encryptedURI, bytes32 provenanceHash) = abi.decode(_data, (bytes, bytes32)); + if (encryptedURI.length != 0 && provenanceHash != "") { + _setEncryptedData(nextTokenIdToLazyMint + _amount, _data); + } + } + + return super.lazyMint(_amount, _baseURIForTokens, _data); + } + + /// @dev Lets an account with `MINTER_ROLE` reveal the URI for a batch of 'delayed-reveal' NFTs. + function reveal( + uint256 _index, + bytes calldata _key + ) external onlyRole(minterRole) returns (string memory revealedURI) { + uint256 batchId = getBatchIdAtIndex(_index); + revealedURI = getRevealURI(batchId, _key); + + _setEncryptedData(batchId, ""); + _setBaseURI(batchId, revealedURI); + + emit TokenURIRevealed(_index, revealedURI); + } + + /*/////////////////////////////////////////////////////////////// + Claiming lazy minted tokens logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Claim lazy minted tokens via signature. + function mintWithSignature( + MintRequest calldata _req, + bytes calldata _signature + ) external payable returns (address signer) { + uint256 tokenIdToMint = _currentIndex; + if (tokenIdToMint + _req.quantity > nextTokenIdToLazyMint) { + revert("Not enough tokens"); + } + + // Verify and process payload. + signer = _processRequest(_req, _signature); + + address receiver = _req.to; + + // Collect price + _collectPriceOnClaim(_req.primarySaleRecipient, _req.quantity, _req.currency, _req.pricePerToken); + + // Set royalties, if applicable. + if (_req.royaltyRecipient != address(0) && _req.royaltyBps != 0) { + _setupRoyaltyInfoForToken(tokenIdToMint, _req.royaltyRecipient, _req.royaltyBps); + } + + // Mint tokens. + _safeMint(receiver, _req.quantity); + + emit TokensMintedWithSignature(signer, receiver, tokenIdToMint, _req); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Runs before every `claim` function call. + function _beforeClaim( + address, + uint256 _quantity, + address, + uint256, + AllowlistProof calldata, + bytes memory + ) internal view override { + bool bot = isTrustedForwarder(msg.sender) || _msgSender() == tx.origin; + require(bot, "BOT"); + require(_currentIndex + _quantity <= nextTokenIdToLazyMint, "Not enough tokens"); + } + + /// @dev Collects and distributes the primary sale value of NFTs being claimed. + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override { + if (_pricePerToken == 0) { + return; + } + + (address platformFeeRecipient, uint16 platformFeeBps) = getPlatformFeeInfo(); + + address saleRecipient = _primarySaleRecipient == address(0) ? primarySaleRecipient() : _primarySaleRecipient; + + uint256 totalPrice = _quantityToClaim * _pricePerToken; + uint256 platformFees = (totalPrice * platformFeeBps) / MAX_BPS; + + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { + if (msg.value != totalPrice) { + revert("Must send total price"); + } + } + + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), platformFeeRecipient, platformFees); + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), saleRecipient, totalPrice - platformFees); + } + + /// @dev Transfers the NFTs being claimed. + function _transferTokensOnClaim( + address _to, + uint256 _quantityBeingClaimed + ) internal override returns (uint256 startTokenId) { + startTokenId = _currentIndex; + _safeMint(_to, _quantityBeingClaimed); + } + + /// @dev Returns whether a given address is authorized to sign mint requests. + function _isAuthorizedSigner(address _signer) internal view override returns (bool) { + return hasRole(minterRole, _signer); + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function _canSetPlatformFeeInfo() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether primary sale recipient can be set in the given execution context. + function _canSetPrimarySaleRecipient() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether owner can be set in the given execution context. + function _canSetOwner() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether royalty info can be set in the given execution context. + function _canSetRoyaltyInfo() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function _canSetClaimConditions() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Returns whether lazy minting can be done in the given execution context. + function _canLazyMint() internal view virtual override returns (bool) { + return hasRole(minterRole, _msgSender()); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + /** + * Returns the total amount of tokens minted in the contract. + */ + function totalMinted() external view returns (uint256) { + unchecked { + return _currentIndex - _startTokenId(); + } + } + + /// @dev The tokenId of the next NFT that will be minted / lazy minted. + function nextTokenIdToMint() external view returns (uint256) { + return nextTokenIdToLazyMint; + } + + /// @dev Burns `tokenId`. See {ERC721-_burn}. + function burn(uint256 tokenId) external virtual { + // note: ERC721AUpgradeable's `_burn(uint256,bool)` internally checks for token approvals. + _burn(tokenId, true); + } + + /// @dev See {ERC721-_beforeTokenTransfer}. + function _beforeTokenTransfers( + address from, + address to, + uint256 startTokenId, + uint256 quantity + ) internal virtual override { + super._beforeTokenTransfers(from, to, startTokenId, quantity); + + // if transfer is restricted on the contract, we still want to allow burning and minting + if (!hasRole(transferRole, address(0)) && from != address(0) && to != address(0)) { + if (!hasRole(transferRole, from) && !hasRole(transferRole, to)) { + revert("!Transfer-Role"); + } + } + } + + function _dropMsgSender() internal view virtual override returns (address) { + return _msgSender(); + } + + function _msgSender() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } + + function _msgData() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable) + returns (bytes calldata) + { + return ERC2771ContextUpgradeable._msgData(); + } +} diff --git a/contracts/legacy-contracts/smart-wallet/interface/IAccountPermissions_V1.sol b/contracts/legacy-contracts/smart-wallet/interface/IAccountPermissions_V1.sol new file mode 100644 index 000000000..3a2124861 --- /dev/null +++ b/contracts/legacy-contracts/smart-wallet/interface/IAccountPermissions_V1.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +interface IAccountPermissions_V1 { + /*/////////////////////////////////////////////////////////////// + Types + //////////////////////////////////////////////////////////////*/ + + /** + * @notice The payload that must be signed by an authorized wallet to set permissions for a signer to use the smart wallet. + * + * @param signer The addres of the signer to give permissions. + * @param approvedTargets The list of approved targets that a role holder can call using the smart wallet. + * @param nativeTokenLimitPerTransaction The maximum value that can be transferred by a role holder in a single transaction. + * @param permissionStartTimestamp The UNIX timestamp at and after which a signer has permission to use the smart wallet. + * @param permissionEndTimestamp The UNIX timestamp at and after which a signer no longer has permission to use the smart wallet. + * @param reqValidityStartTimestamp The UNIX timestamp at and after which a signature is valid. + * @param reqValidityEndTimestamp The UNIX timestamp at and after which a signature is invalid/expired. + * @param uid A unique non-repeatable ID for the payload. + */ + struct SignerPermissionRequest { + address signer; + address[] approvedTargets; + uint256 nativeTokenLimitPerTransaction; + uint128 permissionStartTimestamp; + uint128 permissionEndTimestamp; + uint128 reqValidityStartTimestamp; + uint128 reqValidityEndTimestamp; + bytes32 uid; + } + + /** + * @notice The permissions that a signer has to use the smart wallet. + * + * @param signer The address of the signer. + * @param approvedTargets The list of approved targets that a role holder can call using the smart wallet. + * @param nativeTokenLimitPerTransaction The maximum value that can be transferred by a role holder in a single transaction. + * @param startTimestamp The UNIX timestamp at and after which a signer has permission to use the smart wallet. + * @param endTimestamp The UNIX timestamp at and after which a signer no longer has permission to use the smart wallet. + */ + struct SignerPermissions { + address signer; + address[] approvedTargets; + uint256 nativeTokenLimitPerTransaction; + uint128 startTimestamp; + uint128 endTimestamp; + } + + /** + * @notice Internal struct for storing permissions for a signer (without approved targets). + * + * @param nativeTokenLimitPerTransaction The maximum value that can be transferred by a role holder in a single transaction. + * @param startTimestamp The UNIX timestamp at and after which a signer has permission to use the smart wallet. + * @param endTimestamp The UNIX timestamp at and after which a signer no longer has permission to use the smart wallet. + */ + struct SignerPermissionsStatic { + uint256 nativeTokenLimitPerTransaction; + uint128 startTimestamp; + uint128 endTimestamp; + } + + /*/////////////////////////////////////////////////////////////// + Events + //////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when permissions for a signer are updated. + event SignerPermissionsUpdated( + address indexed authorizingSigner, + address indexed targetSigner, + SignerPermissionRequest permissions + ); + + /// @notice Emitted when an admin is set or removed. + event AdminUpdated(address indexed signer, bool isAdmin); + + /*/////////////////////////////////////////////////////////////// + View functions + //////////////////////////////////////////////////////////////*/ + + /// @notice Returns whether the given account is an admin. + function isAdmin(address signer) external view returns (bool); + + /// @notice Returns whether the given account is an active signer on the account. + function isActiveSigner(address signer) external view returns (bool); + + /// @notice Returns the restrictions under which a signer can use the smart wallet. + function getPermissionsForSigner(address signer) external view returns (SignerPermissions memory permissions); + + /// @notice Returns all active and inactive signers of the account. + function getAllSigners() external view returns (SignerPermissions[] memory signers); + + /// @notice Returns all signers with active permissions to use the account. + function getAllActiveSigners() external view returns (SignerPermissions[] memory signers); + + /// @notice Returns all admins of the account. + function getAllAdmins() external view returns (address[] memory admins); + + /// @dev Verifies that a request is signed by an authorized account. + function verifySignerPermissionRequest( + SignerPermissionRequest calldata req, + bytes calldata signature + ) external view returns (bool success, address signer); + + /*/////////////////////////////////////////////////////////////// + External functions + //////////////////////////////////////////////////////////////*/ + + /// @notice Adds / removes an account as an admin. + function setAdmin(address account, bool isAdmin) external; + + /// @notice Sets the permissions for a given signer. + function setPermissionsForSigner(SignerPermissionRequest calldata req, bytes calldata signature) external; +} diff --git a/contracts/lib/Address.sol b/contracts/lib/Address.sol new file mode 100644 index 000000000..bd1e6ce2f --- /dev/null +++ b/contracts/lib/Address.sol @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.1; + +/// @author thirdweb, OpenZeppelin Contracts (v4.9.0) + +/** + * @dev Collection of functions related to the address type + */ +library Address { + /** + * @dev Returns true if `account` is a contract. + * + * [IMPORTANT] + * ==== + * It is unsafe to assume that an address for which this function returns + * false is an externally-owned account (EOA) and not a contract. + * + * Among others, `isContract` will return false for the following + * types of addresses: + * + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * + * Furthermore, `isContract` will also return true if the target contract within + * the same transaction is already scheduled for destruction by `SELFDESTRUCT`, + * which only has an effect at the end of a transaction. + * ==== + * + * [IMPORTANT] + * ==== + * You shouldn't rely on `isContract` to protect against flash loan attacks! + * + * Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets + * like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract + * constructor. + * ==== + */ + function isContract(address account) internal view returns (bool) { + // This method relies on extcodesize/address.code.length, which returns 0 + // for contracts in construction, since the code is only stored at the end + // of the constructor execution. + + return account.code.length > 0; + } + + /** + * @dev Replacement for Solidity's `transfer`: sends `amount` wei to + * `recipient`, forwarding all available gas and reverting on errors. + * + * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost + * of certain opcodes, possibly making contracts go over the 2300 gas limit + * imposed by `transfer`, making them unable to receive funds via + * `transfer`. {sendValue} removes this limitation. + * + * https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more]. + * + * IMPORTANT: because control is transferred to `recipient`, care must be + * taken to not create reentrancy vulnerabilities. Consider using + * {ReentrancyGuard} or the + * https://solidity.readthedocs.io/en/v0.8.0/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + */ + function sendValue(address payable recipient, uint256 amount) internal { + require(address(this).balance >= amount, "Address: insufficient balance"); + + (bool success, ) = recipient.call{ value: amount }(""); + require(success, "Address: unable to send value, recipient may have reverted"); + } + + /** + * @dev Performs a Solidity function call using a low level `call`. A + * plain `call` is an unsafe replacement for a function call: use this + * function instead. + * + * If `target` reverts with a revert reason, it is bubbled up by this + * function (like regular Solidity function calls). + * + * Returns the raw returned data. To convert to the expected return value, + * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. + * + * Requirements: + * + * - `target` must be a contract. + * - calling `target` with `data` must not revert. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data) internal returns (bytes memory) { + return functionCallWithValue(target, data, 0, "Address: low-level call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with + * `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCall( + address target, + bytes memory data, + string memory errorMessage + ) internal returns (bytes memory) { + return functionCallWithValue(target, data, 0, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but also transferring `value` wei to `target`. + * + * Requirements: + * + * - the calling contract must have an ETH balance of at least `value`. + * - the called Solidity function must be `payable`. + * + * _Available since v3.1._ + */ + function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { + return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); + } + + /** + * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but + * with `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCallWithValue( + address target, + bytes memory data, + uint256 value, + string memory errorMessage + ) internal returns (bytes memory) { + require(address(this).balance >= value, "Address: insufficient balance for call"); + (bool success, bytes memory returndata) = target.call{ value: value }(data); + return verifyCallResultFromTarget(target, success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { + return functionStaticCall(target, data, "Address: low-level static call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall( + address target, + bytes memory data, + string memory errorMessage + ) internal view returns (bytes memory) { + (bool success, bytes memory returndata) = target.staticcall(data); + return verifyCallResultFromTarget(target, success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { + return functionDelegateCall(target, data, "Address: low-level delegate call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall( + address target, + bytes memory data, + string memory errorMessage + ) internal returns (bytes memory) { + (bool success, bytes memory returndata) = target.delegatecall(data); + return verifyCallResultFromTarget(target, success, returndata, errorMessage); + } + + /** + * @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling + * the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract. + * + * _Available since v4.8._ + */ + function verifyCallResultFromTarget( + address target, + bool success, + bytes memory returndata, + string memory errorMessage + ) internal view returns (bytes memory) { + if (success) { + if (returndata.length == 0) { + // only check isContract if the call was successful and the return data is empty + // otherwise we already know that it was a contract + require(isContract(target), "Address: call to non-contract"); + } + return returndata; + } else { + _revert(returndata, errorMessage); + } + } + + /** + * @dev Tool to verify that a low level call was successful, and revert if it wasn't, either by bubbling the + * revert reason or using the provided one. + * + * _Available since v4.3._ + */ + function verifyCallResult( + bool success, + bytes memory returndata, + string memory errorMessage + ) internal pure returns (bytes memory) { + if (success) { + return returndata; + } else { + _revert(returndata, errorMessage); + } + } + + function _revert(bytes memory returndata, string memory errorMessage) private pure { + // Look for revert reason and bubble it up if present + if (returndata.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + /// @solidity memory-safe-assembly + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert(errorMessage); + } + } +} diff --git a/contracts/lib/BitMaps.sol b/contracts/lib/BitMaps.sol new file mode 100644 index 000000000..00336dc07 --- /dev/null +++ b/contracts/lib/BitMaps.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +/** + * @dev Library for managing uint256 to bool mapping in a compact and efficient way, providing the keys are sequential. + * Largely inspired by Uniswap's [merkle-distributor](https://github.com/Uniswap/merkle-distributor/blob/master/contracts/MerkleDistributor.sol). + */ +library BitMaps { + struct BitMap { + mapping(uint256 => uint256) _data; + } + + /** + * @dev Returns whether the bit at `index` is set. + */ + function get(BitMap storage bitmap, uint256 index) internal view returns (bool) { + uint256 bucket = index >> 8; + uint256 mask = 1 << (index & 0xff); + return bitmap._data[bucket] & mask != 0; + } + + /** + * @dev Sets the bit at `index` to the boolean `value`. + */ + function setTo(BitMap storage bitmap, uint256 index, bool value) internal { + if (value) { + set(bitmap, index); + } else { + unset(bitmap, index); + } + } + + /** + * @dev Sets the bit at `index`. + */ + function set(BitMap storage bitmap, uint256 index) internal { + uint256 bucket = index >> 8; + uint256 mask = 1 << (index & 0xff); + bitmap._data[bucket] |= mask; + } + + /** + * @dev Unsets the bit at `index`. + */ + function unset(BitMap storage bitmap, uint256 index) internal { + uint256 bucket = index >> 8; + uint256 mask = 1 << (index & 0xff); + bitmap._data[bucket] &= ~mask; + } +} diff --git a/contracts/lib/BytesLib.sol b/contracts/lib/BytesLib.sol new file mode 100644 index 000000000..6cc1ebdaf --- /dev/null +++ b/contracts/lib/BytesLib.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb +/// Credits: https://github.com/GNSPS/solidity-bytes-utils/blob/master/contracts/BytesLib.sol + +library BytesLib { + function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) { + require(_bytes.length >= _start + 20, "toAddress_outOfBounds"); + address tempAddress; + + assembly { + tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000) + } + + return tempAddress; + } +} diff --git a/contracts/lib/CurrencyTransferLib.sol b/contracts/lib/CurrencyTransferLib.sol index 7c452edc4..531020ffa 100644 --- a/contracts/lib/CurrencyTransferLib.sol +++ b/contracts/lib/CurrencyTransferLib.sol @@ -1,24 +1,23 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; -// Helper interfaces -import { IWETH } from "../interfaces/IWETH.sol"; +/// @author thirdweb -import "../openzeppelin-presets/token/ERC20/utils/SafeERC20.sol"; +// Helper interfaces +import { IWETH } from "../infra/interface/IWETH.sol"; +import { SafeERC20, IERC20 } from "../external-deps/openzeppelin/token/ERC20/utils/SafeERC20.sol"; library CurrencyTransferLib { using SafeERC20 for IERC20; + error CurrencyTransferLibMismatchedValue(uint256 expected, uint256 actual); + error CurrencyTransferLibFailedNativeTransfer(address recipient, uint256 value); + /// @dev The address interpreted as native token of the chain. address public constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; /// @dev Transfers a given amount of currency. - function transferCurrency( - address _currency, - address _from, - address _to, - uint256 _amount - ) internal { + function transferCurrency(address _currency, address _from, address _to, uint256 _amount) internal { if (_amount == 0) { return; } @@ -49,7 +48,9 @@ library CurrencyTransferLib { safeTransferNativeTokenWithWrapper(_to, _amount, _nativeTokenWrapper); } else if (_to == address(this)) { // store native currency in weth - require(_amount == msg.value, "msg.value != amount"); + if (_amount != msg.value) { + revert CurrencyTransferLibMismatchedValue(msg.value, _amount); + } IWETH(_nativeTokenWrapper).deposit{ value: _amount }(); } else { safeTransferNativeTokenWithWrapper(_to, _amount, _nativeTokenWrapper); @@ -60,12 +61,7 @@ library CurrencyTransferLib { } /// @dev Transfer `amount` of ERC20 token from `from` to `to`. - function safeTransferERC20( - address _currency, - address _from, - address _to, - uint256 _amount - ) internal { + function safeTransferERC20(address _currency, address _from, address _to, uint256 _amount) internal { if (_from == _to) { return; } @@ -82,15 +78,13 @@ library CurrencyTransferLib { // solhint-disable avoid-low-level-calls // slither-disable-next-line low-level-calls (bool success, ) = to.call{ value: value }(""); - require(success, "native token transfer failed"); + if (!success) { + revert CurrencyTransferLibFailedNativeTransfer(to, value); + } } /// @dev Transfers `amount` of native token to `to`. (With native token wrapping) - function safeTransferNativeTokenWithWrapper( - address to, - uint256 value, - address _nativeTokenWrapper - ) internal { + function safeTransferNativeTokenWithWrapper(address to, uint256 value, address _nativeTokenWrapper) internal { // solhint-disable avoid-low-level-calls // slither-disable-next-line low-level-calls (bool success, ) = to.call{ value: value }(""); diff --git a/contracts/lib/FeeType.sol b/contracts/lib/FeeType.sol index 6872f9bf1..12f77227e 100644 --- a/contracts/lib/FeeType.sol +++ b/contracts/lib/FeeType.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.11; +/// @author thirdweb + library FeeType { uint256 internal constant PRIMARY_SALE = 0; uint256 internal constant MARKET_SALE = 1; diff --git a/contracts/lib/MerkleProof.sol b/contracts/lib/MerkleProof.sol index 31da449ad..f8a703761 100644 --- a/contracts/lib/MerkleProof.sol +++ b/contracts/lib/MerkleProof.sol @@ -1,32 +1,10 @@ -// SPDX-License-Identifier: MIT -// Modified from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.3.0/contracts/utils/cryptography/MerkleProof.sol -// Copied from https://github.com/ensdomains/governance/blob/master/contracts/MerkleProof.sol - +// SPDX-License-Identifier: Apache 2.0 pragma solidity ^0.8.0; -/** - * @dev These functions deal with verification of Merkle Trees proofs. - * - * The proofs can be generated using the JavaScript library - * https://github.com/miguelmota/merkletreejs[merkletreejs]. - * Note: the hashing algorithm should be keccak256 and pair sorting should be enabled. - * - * See `test/utils/cryptography/MerkleProof.test.js` for some examples. - * - * Source: https://github.com/ensdomains/governance/blob/master/contracts/MerkleProof.sol - */ +/// @author OpenZeppelin, thirdweb + library MerkleProof { - /** - * @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree - * defined by `root`. For this, a `proof` must be provided, containing - * sibling hashes on the branch from the leaf to the root of the tree. Each - * pair of leaves and each pair of pre-images are assumed to be sorted. - */ - function verify( - bytes32[] memory proof, - bytes32 root, - bytes32 leaf - ) internal pure returns (bool, uint256) { + function verify(bytes32[] calldata proof, bytes32 root, bytes32 leaf) internal pure returns (bool, uint256) { bytes32 computedHash = leaf; uint256 index = 0; @@ -36,10 +14,10 @@ library MerkleProof { if (computedHash <= proofElement) { // Hash(current computed hash + current element of the proof) - computedHash = keccak256(abi.encodePacked(computedHash, proofElement)); + computedHash = _efficientHash(computedHash, proofElement); } else { // Hash(current element of the proof + current computed hash) - computedHash = keccak256(abi.encodePacked(proofElement, computedHash)); + computedHash = _efficientHash(proofElement, computedHash); index += 1; } } @@ -47,4 +25,16 @@ library MerkleProof { // Check if the computed hash (root) is equal to the provided root return (computedHash == root, index); } + + /** + * @dev Implementation of keccak256(abi.encode(a, b)) that doesn't allocate or expand memory. + */ + function _efficientHash(bytes32 a, bytes32 b) private pure returns (bytes32 value) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, a) + mstore(0x20, b) + value := keccak256(0x00, 0x40) + } + } } diff --git a/contracts/lib/NFTMetadataRenderer.sol b/contracts/lib/NFTMetadataRenderer.sol new file mode 100644 index 000000000..4fd657159 --- /dev/null +++ b/contracts/lib/NFTMetadataRenderer.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/* solhint-disable quotes */ + +/// @author thirdweb +/// credits: Zora + +import "./Strings.sol"; +import "../external-deps/openzeppelin/utils/Base64.sol"; + +/// NFT metadata library for rendering metadata associated with editions +library NFTMetadataRenderer { + /** + * @notice Generate edition metadata from storage information as base64-json blob + * @dev Combines the media data and metadata + * @param name Name of NFT in metadata + * @param description Description of NFT in metadata + * @param imageURI URI of image to render for edition + * @param animationURI URI of animation to render for edition + * @param tokenOfEdition Token ID for specific token + */ + function createMetadataEdition( + string memory name, + string memory description, + string memory imageURI, + string memory animationURI, + uint256 tokenOfEdition + ) internal pure returns (string memory) { + string memory _tokenMediaData = tokenMediaData(imageURI, animationURI); + bytes memory json = createMetadataJSON(name, description, _tokenMediaData, tokenOfEdition); + return encodeMetadataJSON(json); + } + + /** + * @param name Name of NFT in metadata + * @param description Description of NFT in metadata + * @param mediaData Data for media to include in json object + * @param tokenOfEdition Token ID for specific token + */ + function createMetadataJSON( + string memory name, + string memory description, + string memory mediaData, + uint256 tokenOfEdition + ) internal pure returns (bytes memory) { + return + abi.encodePacked( + '{"name": "', + name, + " ", + Strings.toString(tokenOfEdition), + '", "', + 'description": "', + description, + '", "', + mediaData, + 'properties": {"number": ', + Strings.toString(tokenOfEdition), + ', "name": "', + name, + '"}}' + ); + } + + /// Encodes the argument json bytes into base64-data uri format + /// @param json Raw json to base64 and turn into a data-uri + function encodeMetadataJSON(bytes memory json) internal pure returns (string memory) { + return string(abi.encodePacked("data:application/json;base64,", Base64.encode(json))); + } + + /// Generates edition metadata from storage information as base64-json blob + /// Combines the media data and metadata + /// @param imageUrl URL of image to render for edition + /// @param animationUrl URL of animation to render for edition + function tokenMediaData(string memory imageUrl, string memory animationUrl) internal pure returns (string memory) { + bool hasImage = bytes(imageUrl).length > 0; + bool hasAnimation = bytes(animationUrl).length > 0; + if (hasImage && hasAnimation) { + return string(abi.encodePacked('image": "', imageUrl, '", "animation_url": "', animationUrl, '", "')); + } + if (hasImage) { + return string(abi.encodePacked('image": "', imageUrl, '", "')); + } + if (hasAnimation) { + return string(abi.encodePacked('animation_url": "', animationUrl, '", "')); + } + + return ""; + } +} diff --git a/contracts/lib/StorageSlot.sol b/contracts/lib/StorageSlot.sol new file mode 100644 index 000000000..9fe0bb02e --- /dev/null +++ b/contracts/lib/StorageSlot.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +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) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /// @dev Returns an `BooleanSlot` with member `value` located at `slot`. + function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /// @dev Returns an `Bytes32Slot` with member `value` located at `slot`. + function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /// @dev Returns an `Uint256Slot` with member `value` located at `slot`. + function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } +} diff --git a/contracts/lib/StringSet.sol b/contracts/lib/StringSet.sol new file mode 100644 index 000000000..630300bd1 --- /dev/null +++ b/contracts/lib/StringSet.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +library StringSet { + /** + * @param _values storage of set values + * @param _indexes position of the value in the array + 1. (Note: index 0 means a value is not in the set.) + */ + struct Set { + string[] _values; + mapping(string => uint256) _indexes; + } + + /// @dev Add a value to a set. + /// Returns `true` if the value is not already present in set. + function _add(Set storage set, string memory value) private returns (bool) { + if (!_contains(set, value)) { + set._values.push(value); + + set._indexes[value] = set._values.length; + return true; + } else { + return false; + } + } + + /// @dev Removes a value from a set. + /// Returns `true` if the value was present and so, successfully removed from the set. + function _remove(Set storage set, string memory value) private returns (bool) { + uint256 valueIndex = set._indexes[value]; + + if (valueIndex != 0) { + uint256 toDeleteIndex = valueIndex - 1; + uint256 lastIndex = set._values.length - 1; + + if (lastIndex != toDeleteIndex) { + string memory lastValue = set._values[lastIndex]; + + set._values[toDeleteIndex] = lastValue; + + set._indexes[lastValue] = valueIndex; + } + + set._values.pop(); + + delete set._indexes[value]; + + return true; + } else { + return false; + } + } + + /// @dev Returns whether `value` is in the set. + function _contains(Set storage set, string memory value) private view returns (bool) { + return set._indexes[value] != 0; + } + + /// @dev Returns the number of elements in the set. + function _length(Set storage set) private view returns (uint256) { + return set._values.length; + } + + /// @dev Returns the element stored at position `index` in the set. + /// Note: the ordering of elements is not guaranteed to be fixed. It is unsafe to rely on + /// or compute based on the index of set elements. + function _at(Set storage set, uint256 index) private view returns (string memory) { + return set._values[index]; + } + + /// @dev Returns the values stored in the set. + function _values(Set storage set) private view returns (string[] memory) { + return set._values; + } + + /// @dev Add `value` to the set. + function add(Set storage set, string memory value) internal returns (bool) { + return _add(set, value); + } + + /// @dev Remove `value` from the set. + function remove(Set storage set, string memory value) internal returns (bool) { + return _remove(set, value); + } + + /// @dev Returns whether `value` is in the set. + function contains(Set storage set, string memory value) internal view returns (bool) { + return _contains(set, value); + } + + /// @dev Returns the number of elements in the set. + function length(Set storage set) internal view returns (uint256) { + return _length(set); + } + + /// @dev Returns the element stored at position `index` in the set. + function at(Set storage set, uint256 index) internal view returns (string memory) { + return _at(set, index); + } + + /// @dev Returns the values stored in the set. + function values(Set storage set) internal view returns (string[] memory) { + return _values(set); + } +} diff --git a/contracts/lib/Strings.sol b/contracts/lib/Strings.sol new file mode 100644 index 000000000..499df4583 --- /dev/null +++ b/contracts/lib/Strings.sol @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +/** + * @dev String operations. + */ +library Strings { + bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef"; + + /** + * @dev Converts a `uint256` to its ASCII `string` decimal representation. + */ + function toString(uint256 value) internal pure returns (string memory) { + // Inspired by OraclizeAPI's implementation - MIT licence + // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol + + if (value == 0) { + return "0"; + } + uint256 temp = value; + uint256 digits; + while (temp != 0) { + digits++; + temp /= 10; + } + bytes memory buffer = new bytes(digits); + while (value != 0) { + digits -= 1; + buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); + value /= 10; + } + return string(buffer); + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation. + */ + function toHexString(uint256 value) internal pure returns (string memory) { + if (value == 0) { + return "0x00"; + } + uint256 temp = value; + uint256 length = 0; + while (temp != 0) { + length++; + temp >>= 8; + } + return toHexString(value, length); + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. + */ + function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { + bytes memory buffer = new bytes(2 * length + 2); + buffer[0] = "0"; + buffer[1] = "x"; + for (uint256 i = 2 * length + 1; i > 1; --i) { + buffer[i] = _HEX_SYMBOLS[value & 0xf]; + value >>= 4; + } + require(value == 0, "Strings: hex length insufficient"); + return string(buffer); + } + + /// @dev Returns the hexadecimal representation of `value`. + /// The output is prefixed with "0x", encoded using 2 hexadecimal digits per byte, + /// and the alphabets are capitalized conditionally according to + /// https://eips.ethereum.org/EIPS/eip-55 + function toHexStringChecksummed(address value) internal pure returns (string memory str) { + str = toHexString(value); + /// @solidity memory-safe-assembly + assembly { + let mask := shl(6, div(not(0), 255)) // `0b010000000100000000 ...` + let o := add(str, 0x22) + let hashed := and(keccak256(o, 40), mul(34, mask)) // `0b10001000 ... ` + let t := shl(240, 136) // `0b10001000 << 240` + for { + let i := 0 + } 1 { + + } { + mstore(add(i, i), mul(t, byte(i, hashed))) + i := add(i, 1) + if eq(i, 20) { + break + } + } + mstore(o, xor(mload(o), shr(1, and(mload(0x00), and(mload(o), mask))))) + o := add(o, 0x20) + mstore(o, xor(mload(o), shr(1, and(mload(0x20), and(mload(o), mask))))) + } + } + + /// @dev Returns the hexadecimal representation of `value`. + /// The output is prefixed with "0x" and encoded using 2 hexadecimal digits per byte. + function toHexString(address value) internal pure returns (string memory str) { + str = toHexStringNoPrefix(value); + /// @solidity memory-safe-assembly + assembly { + let strLength := add(mload(str), 2) // Compute the length. + mstore(str, 0x3078) // Write the "0x" prefix. + str := sub(str, 2) // Move the pointer. + mstore(str, strLength) // Write the length. + } + } + + /// @dev Returns the hexadecimal representation of `value`. + /// The output is encoded using 2 hexadecimal digits per byte. + function toHexStringNoPrefix(address value) internal pure returns (string memory str) { + /// @solidity memory-safe-assembly + assembly { + str := mload(0x40) + + // Allocate the memory. + // We need 0x20 bytes for the trailing zeros padding, 0x20 bytes for the length, + // 0x02 bytes for the prefix, and 0x28 bytes for the digits. + // The next multiple of 0x20 above (0x20 + 0x20 + 0x02 + 0x28) is 0x80. + mstore(0x40, add(str, 0x80)) + + // Store "0123456789abcdef" in scratch space. + mstore(0x0f, 0x30313233343536373839616263646566) + + str := add(str, 2) + mstore(str, 40) + + let o := add(str, 0x20) + mstore(add(o, 40), 0) + + value := shl(96, value) + + // We write the string from rightmost digit to leftmost digit. + // The following is essentially a do-while loop that also handles the zero case. + for { + let i := 0 + } 1 { + + } { + let p := add(o, add(i, i)) + let temp := byte(i, value) + mstore8(add(p, 1), mload(and(temp, 15))) + mstore8(p, mload(shr(4, temp))) + i := add(i, 1) + if eq(i, 20) { + break + } + } + } + } + + /// @dev Returns the hex encoded string from the raw bytes. + /// The output is encoded using 2 hexadecimal digits per byte. + function toHexString(bytes memory raw) internal pure returns (string memory str) { + str = toHexStringNoPrefix(raw); + /// @solidity memory-safe-assembly + assembly { + let strLength := add(mload(str), 2) // Compute the length. + mstore(str, 0x3078) // Write the "0x" prefix. + str := sub(str, 2) // Move the pointer. + mstore(str, strLength) // Write the length. + } + } + + /// @dev Returns the hex encoded string from the raw bytes. + /// The output is encoded using 2 hexadecimal digits per byte. + function toHexStringNoPrefix(bytes memory raw) internal pure returns (string memory str) { + /// @solidity memory-safe-assembly + assembly { + let length := mload(raw) + str := add(mload(0x40), 2) // Skip 2 bytes for the optional prefix. + mstore(str, add(length, length)) // Store the length of the output. + + // Store "0123456789abcdef" in scratch space. + mstore(0x0f, 0x30313233343536373839616263646566) + + let o := add(str, 0x20) + let end := add(raw, length) + + for { + + } iszero(eq(raw, end)) { + + } { + raw := add(raw, 1) + mstore8(add(o, 1), mload(and(mload(raw), 15))) + mstore8(o, mload(and(shr(4, mload(raw)), 15))) + o := add(o, 2) + } + mstore(o, 0) // Zeroize the slot after the string. + mstore(0x40, add(o, 0x20)) // Allocate the memory. + } + } +} diff --git a/contracts/lib/TWAddress.sol b/contracts/lib/TWAddress.sol deleted file mode 100644 index cd804a809..000000000 --- a/contracts/lib/TWAddress.sol +++ /dev/null @@ -1,222 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.5.0) (utils/Address.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Collection of functions related to the address type - */ -library TWAddress { - /** - * @dev Returns true if `account` is a contract. - * - * [IMPORTANT] - * ==== - * It is unsafe to assume that an address for which this function returns - * false is an externally-owned account (EOA) and not a contract. - * - * Among others, `isContract` will return false for the following - * types of addresses: - * - * - an externally-owned account - * - a contract in construction - * - an address where a contract will be created - * - an address where a contract lived, but was destroyed - * ==== - * - * [IMPORTANT] - * ==== - * You shouldn't rely on `isContract` to protect against flash loan attacks! - * - * Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets - * like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract - * constructor. - * ==== - */ - function isContract(address account) internal view returns (bool) { - // This method relies on extcodesize/address.code.length, which returns 0 - // for contracts in construction, since the code is only stored at the end - // of the constructor execution. - - return account.code.length > 0; - } - - /** - * @dev Replacement for Solidity's `transfer`: sends `amount` wei to - * `recipient`, forwarding all available gas and reverting on errors. - * - * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost - * of certain opcodes, possibly making contracts go over the 2300 gas limit - * imposed by `transfer`, making them unable to receive funds via - * `transfer`. {sendValue} removes this limitation. - * - * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. - * - * IMPORTANT: because control is transferred to `recipient`, care must be - * taken to not create reentrancy vulnerabilities. Consider using - * {ReentrancyGuard} or the - * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. - */ - function sendValue(address payable recipient, uint256 amount) internal { - require(address(this).balance >= amount, "Address: insufficient balance"); - - (bool success, ) = recipient.call{ value: amount }(""); - require(success, "Address: unable to send value, recipient may have reverted"); - } - - /** - * @dev Performs a Solidity function call using a low level `call`. A - * plain `call` is an unsafe replacement for a function call: use this - * function instead. - * - * If `target` reverts with a revert reason, it is bubbled up by this - * function (like regular Solidity function calls). - * - * Returns the raw returned data. To convert to the expected return value, - * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. - * - * Requirements: - * - * - `target` must be a contract. - * - calling `target` with `data` must not revert. - * - * _Available since v3.1._ - */ - function functionCall(address target, bytes memory data) internal returns (bytes memory) { - return functionCall(target, data, "Address: low-level call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with - * `errorMessage` as a fallback revert reason when `target` reverts. - * - * _Available since v3.1._ - */ - function functionCall( - address target, - bytes memory data, - string memory errorMessage - ) internal returns (bytes memory) { - return functionCallWithValue(target, data, 0, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but also transferring `value` wei to `target`. - * - * Requirements: - * - * - the calling contract must have an ETH balance of at least `value`. - * - the called Solidity function must be `payable`. - * - * _Available since v3.1._ - */ - function functionCallWithValue( - address target, - bytes memory data, - uint256 value - ) internal returns (bytes memory) { - return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); - } - - /** - * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but - * with `errorMessage` as a fallback revert reason when `target` reverts. - * - * _Available since v3.1._ - */ - function functionCallWithValue( - address target, - bytes memory data, - uint256 value, - string memory errorMessage - ) internal returns (bytes memory) { - require(address(this).balance >= value, "Address: insufficient balance for call"); - require(isContract(target), "Address: call to non-contract"); - - (bool success, bytes memory returndata) = target.call{ value: value }(data); - return verifyCallResult(success, returndata, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but performing a static call. - * - * _Available since v3.3._ - */ - function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { - return functionStaticCall(target, data, "Address: low-level static call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], - * but performing a static call. - * - * _Available since v3.3._ - */ - function functionStaticCall( - address target, - bytes memory data, - string memory errorMessage - ) internal view returns (bytes memory) { - require(isContract(target), "Address: static call to non-contract"); - - (bool success, bytes memory returndata) = target.staticcall(data); - return verifyCallResult(success, returndata, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but performing a delegate call. - * - * _Available since v3.4._ - */ - function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { - return functionDelegateCall(target, data, "Address: low-level delegate call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], - * but performing a delegate call. - * - * _Available since v3.4._ - */ - function functionDelegateCall( - address target, - bytes memory data, - string memory errorMessage - ) internal returns (bytes memory) { - require(isContract(target), "Address: delegate call to non-contract"); - - (bool success, bytes memory returndata) = target.delegatecall(data); - return verifyCallResult(success, returndata, errorMessage); - } - - /** - * @dev Tool to verifies that a low level call was successful, and revert if it wasn't, either by bubbling the - * revert reason using the provided one. - * - * _Available since v4.3._ - */ - function verifyCallResult( - bool success, - bytes memory returndata, - string memory errorMessage - ) internal pure returns (bytes memory) { - if (success) { - return returndata; - } else { - // Look for revert reason and bubble it up if present - if (returndata.length > 0) { - // The easiest way to bubble the revert reason is using memory via assembly - - assembly { - let returndata_size := mload(returndata) - revert(add(32, returndata), returndata_size) - } - } else { - revert(errorMessage); - } - } - } -} diff --git a/contracts/lib/TWBitMaps.sol b/contracts/lib/TWBitMaps.sol deleted file mode 100644 index 6c6a71874..000000000 --- a/contracts/lib/TWBitMaps.sol +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (utils/structs/BitMaps.sol) -pragma solidity ^0.8.0; - -/** - * @dev Library for managing uint256 to bool mapping in a compact and efficient way, providing the keys are sequential. - * Largelly inspired by Uniswap's https://github.com/Uniswap/merkle-distributor/blob/master/contracts/MerkleDistributor.sol[merkle-distributor]. - */ -library TWBitMaps { - struct BitMap { - mapping(uint256 => uint256) _data; - } - - /** - * @dev Returns whether the bit at `index` is set. - */ - function get(BitMap storage bitmap, uint256 index) internal view returns (bool) { - uint256 bucket = index >> 8; - uint256 mask = 1 << (index & 0xff); - return bitmap._data[bucket] & mask != 0; - } - - /** - * @dev Sets the bit at `index` to the boolean `value`. - */ - function setTo( - BitMap storage bitmap, - uint256 index, - bool value - ) internal { - if (value) { - set(bitmap, index); - } else { - unset(bitmap, index); - } - } - - /** - * @dev Sets the bit at `index`. - */ - function set(BitMap storage bitmap, uint256 index) internal { - uint256 bucket = index >> 8; - uint256 mask = 1 << (index & 0xff); - bitmap._data[bucket] |= mask; - } - - /** - * @dev Unsets the bit at `index`. - */ - function unset(BitMap storage bitmap, uint256 index) internal { - uint256 bucket = index >> 8; - uint256 mask = 1 << (index & 0xff); - bitmap._data[bucket] &= ~mask; - } -} diff --git a/contracts/lib/TWStrings.sol b/contracts/lib/TWStrings.sol deleted file mode 100644 index 8d8f00370..000000000 --- a/contracts/lib/TWStrings.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (utils/Strings.sol) - -pragma solidity ^0.8.0; - -/** - * @dev String operations. - */ -library TWStrings { - bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef"; - - /** - * @dev Converts a `uint256` to its ASCII `string` decimal representation. - */ - function toString(uint256 value) internal pure returns (string memory) { - // Inspired by OraclizeAPI's implementation - MIT licence - // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol - - if (value == 0) { - return "0"; - } - uint256 temp = value; - uint256 digits; - while (temp != 0) { - digits++; - temp /= 10; - } - bytes memory buffer = new bytes(digits); - while (value != 0) { - digits -= 1; - buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); - value /= 10; - } - return string(buffer); - } - - /** - * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation. - */ - function toHexString(uint256 value) internal pure returns (string memory) { - if (value == 0) { - return "0x00"; - } - uint256 temp = value; - uint256 length = 0; - while (temp != 0) { - length++; - temp >>= 8; - } - return toHexString(value, length); - } - - /** - * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. - */ - function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { - bytes memory buffer = new bytes(2 * length + 2); - buffer[0] = "0"; - buffer[1] = "x"; - for (uint256 i = 2 * length + 1; i > 1; --i) { - buffer[i] = _HEX_SYMBOLS[value & 0xf]; - value >>= 4; - } - require(value == 0, "Strings: hex length insufficient"); - return string(buffer); - } -} diff --git a/contracts/pack/Pack.sol b/contracts/pack/Pack.sol deleted file mode 100644 index 5797cde2e..000000000 --- a/contracts/pack/Pack.sol +++ /dev/null @@ -1,404 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -// ========== External imports ========== - -import "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/ERC1155PausableUpgradeable.sol"; - -import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; - -// ========== Internal imports ========== - -import "../interfaces/IPack.sol"; -import "../openzeppelin-presets/metatx/ERC2771ContextUpgradeable.sol"; - -// ========== Features ========== - -import "../extension/ContractMetadata.sol"; -import "../extension/Royalty.sol"; -import "../extension/Ownable.sol"; -import "../extension/PermissionsEnumerable.sol"; -import { TokenStore, ERC1155Receiver } from "../extension/TokenStore.sol"; - -contract Pack is - Initializable, - ContractMetadata, - Ownable, - Royalty, - PermissionsEnumerable, - TokenStore, - ReentrancyGuardUpgradeable, - ERC2771ContextUpgradeable, - MulticallUpgradeable, - ERC1155PausableUpgradeable, - IPack -{ - /*/////////////////////////////////////////////////////////////// - State variables - //////////////////////////////////////////////////////////////*/ - - bytes32 private constant MODULE_TYPE = bytes32("Pack"); - uint256 private constant VERSION = 1; - - // Token name - string public name; - - // Token symbol - string public symbol; - - /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. - bytes32 private constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE"); - /// @dev Only MINTER_ROLE holders can create packs. - bytes32 private constant MINTER_ROLE = keccak256("MINTER_ROLE"); - /// @dev Only assets with ASSET_ROLE can be packed, when packing is restricted to particular assets. - bytes32 private constant ASSET_ROLE = keccak256("ASSET_ROLE"); - - /// @dev The token Id of the next set of packs to be minted. - uint256 public nextTokenIdToMint; - - /*/////////////////////////////////////////////////////////////// - Mappings - //////////////////////////////////////////////////////////////*/ - - /// @dev Mapping from token ID => total circulating supply of token with that ID. - mapping(uint256 => uint256) public totalSupply; - - /// @dev Mapping from pack ID => The state of that set of packs. - mapping(uint256 => PackInfo) private packInfo; - - /*/////////////////////////////////////////////////////////////// - Constructor + initializer logic - //////////////////////////////////////////////////////////////*/ - - constructor(address _nativeTokenWrapper) TokenStore(_nativeTokenWrapper) initializer {} - - /// @dev Initiliazes the contract, like a constructor. - function initialize( - address _defaultAdmin, - string memory _name, - string memory _symbol, - string memory _contractURI, - address[] memory _trustedForwarders, - address _royaltyRecipient, - uint256 _royaltyBps - ) external initializer { - // Initialize inherited contracts, most base-like -> most derived. - __ReentrancyGuard_init(); - __ERC2771Context_init(_trustedForwarders); - __ERC1155Pausable_init(); - __ERC1155_init(_contractURI); - - name = _name; - symbol = _symbol; - - _setupContractURI(_contractURI); - _setupOwner(_defaultAdmin); - - _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); - _setupRole(TRANSFER_ROLE, _defaultAdmin); - _setupRole(MINTER_ROLE, _defaultAdmin); - _setupRole(TRANSFER_ROLE, address(0)); - - // note: see `onlyRoleWithSwitch` for ASSET_ROLE behaviour. - _setupRole(ASSET_ROLE, address(0)); - - _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); - } - - receive() external payable { - require(_msgSender() == nativeTokenWrapper, "Caller is not native token wrapper."); - } - - /*/////////////////////////////////////////////////////////////// - Modifiers - //////////////////////////////////////////////////////////////*/ - - modifier onlyRoleWithSwitch(bytes32 role) { - _checkRoleWithSwitch(role, _msgSender()); - _; - } - - /*/////////////////////////////////////////////////////////////// - Generic contract logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns the type of the contract. - function contractType() external pure returns (bytes32) { - return MODULE_TYPE; - } - - /// @dev Returns the version of the contract. - function contractVersion() external pure returns (uint8) { - return uint8(VERSION); - } - - /// @dev Pauses / unpauses contract. - function pause(bool _toPause) internal onlyRole(DEFAULT_ADMIN_ROLE) { - if (_toPause) { - _pause(); - } else { - _unpause(); - } - } - - /*/////////////////////////////////////////////////////////////// - ERC 165 / 1155 / 2981 logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns the URI for a given tokenId. - function uri(uint256 _tokenId) public view override returns (string memory) { - return getUriOfBundle(_tokenId); - } - - /// @dev See ERC 165 - function supportsInterface(bytes4 interfaceId) - public - view - virtual - override(ERC1155Receiver, ERC1155Upgradeable, IERC165) - returns (bool) - { - return super.supportsInterface(interfaceId) || type(IERC2981Upgradeable).interfaceId == interfaceId; - } - - /*/////////////////////////////////////////////////////////////// - Pack logic: create | open packs. - //////////////////////////////////////////////////////////////*/ - - /// @dev Creates a pack with the stated contents. - function createPack( - Token[] calldata _contents, - uint256[] calldata _numOfRewardUnits, - string calldata _packUri, - uint128 _openStartTimestamp, - uint128 _amountDistributedPerOpen, - address _recipient - ) - external - payable - onlyRoleWithSwitch(MINTER_ROLE) - nonReentrant - whenNotPaused - returns (uint256 packId, uint256 packTotalSupply) - { - require(_contents.length > 0, "nothing to pack"); - require(_contents.length == _numOfRewardUnits.length, "invalid reward units"); - - if (!hasRole(ASSET_ROLE, address(0))) { - for (uint256 i = 0; i < _contents.length; i += 1) { - _checkRole(ASSET_ROLE, _contents[i].assetContract); - } - } - - packId = nextTokenIdToMint; - nextTokenIdToMint += 1; - - packTotalSupply = escrowPackContents(_contents, _numOfRewardUnits, _packUri, packId, _amountDistributedPerOpen); - - packInfo[packId].openStartTimestamp = _openStartTimestamp; - packInfo[packId].amountDistributedPerOpen = _amountDistributedPerOpen; - - _mint(_recipient, packId, packTotalSupply, ""); - - emit PackCreated(packId, _msgSender(), _recipient, packTotalSupply); - } - - /// @notice Lets a pack owner open packs and receive the packs' reward units. - function openPack(uint256 _packId, uint256 _amountToOpen) - external - nonReentrant - whenNotPaused - returns (Token[] memory) - { - address opener = _msgSender(); - - require(opener == tx.origin, "opener must be eoa"); - require(balanceOf(opener, _packId) >= _amountToOpen, "opening more than owned"); - - PackInfo memory pack = packInfo[_packId]; - require(pack.openStartTimestamp < block.timestamp, "cannot open yet"); - - Token[] memory rewardUnits = getRewardUnits(_packId, _amountToOpen, pack.amountDistributedPerOpen, pack); - - _burn(_msgSender(), _packId, _amountToOpen); - - _transferTokenBatch(address(this), _msgSender(), rewardUnits); - - emit PackOpened(_packId, _msgSender(), _amountToOpen, rewardUnits); - - return rewardUnits; - } - - function escrowPackContents( - Token[] calldata _contents, - uint256[] calldata _numOfRewardUnits, - string calldata _packUri, - uint256 packId, - uint256 amountPerOpen - ) internal returns (uint256 packTotalSupply) { - uint256 totalRewardUnits; - - for (uint256 i = 0; i < _contents.length; i += 1) { - require(_contents[i].totalAmount % _numOfRewardUnits[i] == 0, "invalid reward units"); - require( - _contents[i].tokenType != TokenType.ERC721 || _contents[i].totalAmount == 1, - "invalid erc721 rewards" - ); - - totalRewardUnits += _numOfRewardUnits[i]; - - packInfo[packId].perUnitAmounts.push(_contents[i].totalAmount / _numOfRewardUnits[i]); - } - - require(totalRewardUnits % amountPerOpen == 0, "invalid amount to distribute per open"); - packTotalSupply = totalRewardUnits / amountPerOpen; - - _storeTokens(_msgSender(), _contents, _packUri, packId); - } - - /// @dev Returns the reward units to distribute. - function getRewardUnits( - uint256 _packId, - uint256 _numOfPacksToOpen, - uint256 _rewardUnitsPerOpen, - PackInfo memory pack - ) internal returns (Token[] memory rewardUnits) { - uint256 numOfRewardUnitsToDistribute = _numOfPacksToOpen * _rewardUnitsPerOpen; - rewardUnits = new Token[](numOfRewardUnitsToDistribute); - - uint256 totalRewardUnits = totalSupply[_packId] * _rewardUnitsPerOpen; - uint256 totalRewardKinds = getTokenCountOfBundle(_packId); - - uint256 random = generateRandomValue(); - - for (uint256 i = 0; i < numOfRewardUnitsToDistribute; i += 1) { - uint256 randomVal = uint256(keccak256(abi.encode(random, i))); - uint256 target = randomVal % totalRewardUnits; - uint256 step; - - for (uint256 j = 0; j < totalRewardKinds; j += 1) { - uint256 id = _packId; - - Token memory _token = getTokenOfBundle(id, j); - uint256 totalRewardUnitsOfKind = _token.totalAmount / pack.perUnitAmounts[j]; - - if (target < step + totalRewardUnitsOfKind) { - _token.totalAmount -= pack.perUnitAmounts[j]; - _updateTokenInBundle(_token, id, j); - rewardUnits[i] = _token; - rewardUnits[i].totalAmount = pack.perUnitAmounts[j]; - - totalRewardUnits -= 1; - - break; - } else { - step += totalRewardUnitsOfKind; - } - } - } - } - - /*/////////////////////////////////////////////////////////////// - Getter functions - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns the underlying contents of a pack. - function getPackContents(uint256 _packId) - external - view - returns (Token[] memory contents, uint256[] memory perUnitAmounts) - { - PackInfo memory pack = packInfo[_packId]; - uint256 total = getTokenCountOfBundle(_packId); - contents = new Token[](total); - perUnitAmounts = new uint256[](total); - - for (uint256 i = 0; i < total; i += 1) { - contents[i] = getTokenOfBundle(_packId, i); - perUnitAmounts[i] = pack.perUnitAmounts[i]; - } - } - - /*/////////////////////////////////////////////////////////////// - Internal functions - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns whether owner can be set in the given execution context. - function _canSetOwner() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Returns whether royalty info can be set in the given execution context. - function _canSetRoyaltyInfo() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Returns whether contract metadata can be set in the given execution context. - function _canSetContractURI() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /*/////////////////////////////////////////////////////////////// - Miscellaneous - //////////////////////////////////////////////////////////////*/ - - function generateRandomValue() internal view returns (uint256 random) { - random = uint256(keccak256(abi.encodePacked(_msgSender(), blockhash(block.number - 1), block.difficulty))); - } - - /** - * @dev See {ERC1155-_beforeTokenTransfer}. - */ - function _beforeTokenTransfer( - address operator, - address from, - address to, - uint256[] memory ids, - uint256[] memory amounts, - bytes memory data - ) internal virtual override { - super._beforeTokenTransfer(operator, from, to, ids, amounts, data); - - // if transfer is restricted on the contract, we still want to allow burning and minting - if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { - require(hasRole(TRANSFER_ROLE, from) || hasRole(TRANSFER_ROLE, to), "restricted to TRANSFER_ROLE holders."); - } - - if (from == address(0)) { - for (uint256 i = 0; i < ids.length; ++i) { - totalSupply[ids[i]] += amounts[i]; - } - } - - if (to == address(0)) { - for (uint256 i = 0; i < ids.length; ++i) { - totalSupply[ids[i]] -= amounts[i]; - } - } - } - - /// @dev See EIP-2771 - function _msgSender() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (address sender) - { - return ERC2771ContextUpgradeable._msgSender(); - } - - /// @dev See EIP-2771 - function _msgData() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (bytes calldata) - { - return ERC2771ContextUpgradeable._msgData(); - } -} diff --git a/contracts/pack/pack.md b/contracts/pack/pack.md deleted file mode 100644 index 7533c69ae..000000000 --- a/contracts/pack/pack.md +++ /dev/null @@ -1,239 +0,0 @@ -# Pack design document. - -This is a live document that explains what the [thirdweb](https://thirdweb.com/) `Pack` smart contract is, how it works and can be used, and why it is designed the way it is. - -The document is written for technical and non-technical readers. To ask further questions about thirdweb’s `Pack` contract, please join the [thirdweb discord](https://discord.gg/thirdweb) or create a github issue. - -# Background - -The thirdweb `Pack` contract is a lootbox mechanism. An account can bundle up arbitrary ERC20, ERC721 and ERC1155 tokens into a set of packs. A pack can then be opened in return for a selection of the tokens in the pack. The selection of tokens distributed on opening a pack depends on the relative supply of all tokens in the packs. - -## Product: How packs *should* work (without web3 terminology) - -Let's say we want to create a set of packs with three kinds of rewards - 80 **circles**, 15 **squares**, and 5 **stars** — and we want exactly 1 reward to be distributed when a pack is opened. - -In this case, with thirdweb’s `Pack` contract, each pack is guaranteed to yield exactly 1 reward. To deliver this guarantee, the number of packs created is equal to the sum of the supplies of each reward. So, we now have `80 + 15 + 5` i.e. `100` packs at hand. - -![pack-diag-1.png](/assets/pack-diag-1.png) - -On opening one of these 100 packs, the opener will receive one of the pack's rewards - either a **circle**, a **square**, or a **star**. The chances of receiving a particular reward is determined by how many of that reward exists across our set of packs. - -The percentage chance of receiving a particular kind of reward (e.g. a **star**) on opening a pack is calculated as:`(number_of_stars_packed) / (total number of packs)` - -In the beginning, 80 **circles**, 15 **squares**, and 5 **stars** exist across our set of 100 packs. That means the chances of receiving a **circle** upon opening a pack is `80/100` i.e. 80%. Similarly, a pack opener stands a 15% chance of receiving a **square**, and a 5% chance of receiving a **star** upon opening a pack. - -![pack-diag-2.png](/assets/pack-diag-2.png) - -The chances of receiving each kind of reward change as packs are opened. Let's say one of our 100 packs is opened, yielding a **circle**. We then have 99 packs remaining, with *79* **circles**, 15 **squares**, and 5 **stars** packed. - -For the next pack that is opened, the opener will have a `79/99` i.e. around 79.8% chance of receiving a **circle**, around 15.2% chance of receiving a **square**, and around 5.1% chance of receiving a **star**. - -### Core parts of `Pack` as a product - -Given the above illustration of ‘how packs *should* work’, we can now note down certain core parts of the `Pack` product, that any implementation of `Pack` should maintain: - -- A creator can pack arbitrary ERC20, ERC721 and ERC1155 tokens into a set of packs. -- The % chance of receiving a particular reward on opening a pack should be a function of the relative supplies of the rewards within a pack. That is, opening a pack *should not* be like a lottery, where there’s an unchanging % chance of being distributed, assigned to rewards in a set of packs. -- A pack opener *should not* be able to tell beforehand what reward they’ll receive on opening a pack. -- Each pack in a set of packs can be opened whenever the respective pack owner chooses to open the pack. -- Packs must be capable of being transferred and sold on a marketplace. - -## Why we’re building `Pack` - -Packs are designed to work as generic packs that contain rewards in them, where a pack can be opened to retrieve the rewards in that pack. - -Packs like these already exist as e.g. regular [Pokemon card packs](https://www.pokemoncenter.com/category/booster-packs), or in other forms that use blockchain technology, like [NBA Topshot](https://nbatopshot.com/) packs. This concept is ubiquitous across various cultures, sectors and products. - -As tokens continue to get legitimized as assets / items, we’re bringing ‘packs’ — a long-standing way of gamifying distribution of items — on-chain, as a primitive with a robust implementation that can be used across all chains, and for all kinds of use cases. - -# Technical details - -We’ll now go over the technical details of the `Pack` contract, with references to the example given in the previous section — ‘How packs work (without web3 terminology)’. - -## What can be packed in packs? - -You can create a set of packs with any combination of any number of ERC20, ERC721 and ERC1155 tokens. For example, you can create a set of packs with 10,000 [USDC](https://www.circle.com/en/usdc) (ERC20), 1 [Bored Ape Yatch Club](https://opensea.io/collection/boredapeyachtclub) NFT (ERC721), and 50 of [adidas originals’ first NFT](https://opensea.io/assets/0x28472a58a490c5e09a238847f66a68a47cc76f0f/0) (ERC1155). - -With strictly non-fungible tokens i.e. ERC721 NFTs, each NFT has a supply of 1. This means if a pack is opened and an ERC721 NFT is selected by the `Pack` contract to be distributed to the opener, that 1 NFT will be distributed to the opener. - -With fungible (ERC20) and semi-fungible (ERC1155) tokens, you must specify how many of those tokens must be distributed on opening a pack, as a unit. For example, if adding 10,000 USDC to a pack, you may specify that 20 USDC, as a unit, are meant to be distributed on opening a pack. This means you’re adding 500 units of 20 USDC to the set of packs you’re creating. - -And so, what can be packed in packs are *n* number of configurations like ‘500 units of 20 USDC’. These configurations are interpreted by the `Pack` contract as `PackContent`: - -```solidity -enum TokenType { ERC20, ERC721, ERC1155 } - -struct PackContent { - address assetContract; - TokenType tokenType; - uint256 tokenId; - uint256 totalAmountPacked; - uint256 amountDistributedPerOpen; -} -``` - -| Value | Description | -| --- | --- | -| assetContract | The contract address of the token. | -| tokenType | The type of the token -- ERC20 / ERC721 / ERC1155 | -| tokenId | The tokenId of the the token. (Not applicable for ERC20 tokens. The contract will ignore this value for ERC20 tokens.) | -| totalAmountPacked | The total amount of this token packed in the pack. (Not applicable for ERC721 tokens. The contract will always consider this as 1 for ERC721 tokens.) | -| amountDistributedPerOpen | The amount of this token to distribute as a unit, on opening a pack. (Not applicable for ERC721 tokens. The contract will always consider this as 1 for ERC721 tokens.) | - -**Note:** A pack can contain different configurations for the same token. For example, the same set of packs can contain ‘500 units of 20 USDC’ and ‘10 units of 1000 USDC’ as two independent types of underlying rewards. - -## Creating packs - -You can create packs with any ERC20, ERC721 or ERC1155 tokens that you own. To create packs, you must specify the following: - -```solidity -function createPack( - PackContent[] calldata contents, - string calldata packUri, - uint128 openStartTimestamp, - uint128 amountDistributedPerOpen -) external; -``` - -| Parameter | Description | -| --- | --- | -| contents | The reward units packed in the packs. | -| packUri | The (metadata) URI assigned to the packs created. | -| openStartTimestamp | The timestamp after which packs can be opened. | -| amountDistributedPerOpen | The number of reward units distributed per open. | - -### Packs are ERC1155 tokens i.e. NFTs - -Packs themselves are ERC1155 tokens. And so, a set of packs created with your tokens is itself identified by a unique tokenId, has an associated metadata URI and a variable supply. - -In the example given in the previous section — ‘How packs work (without web3 terminology)’, there is a set of 100 packs created, where that entire set of packs is identified by a unique tokenId. - -Since packs are ERC1155 tokens, you can publish multiple sets of packs using the same `Pack` contract. - -### Supply of packs - -When creating packs, you can specify the numer of reward units to distribute to the opener on opening a pack. And so, when creating a set of packs, the total number of packs in that set is calculated as: - -`total_supply_of_packs = (total_reward_units) / (reward_units_to_distribute_per_open)` - -This guarantees that each pack can be opened to retrieve the intended *n* reward units from inside the set of packs. - -## Opening packs - -Packs can be opened by owners of packs. A pack owner can open multiple packs at once. ‘Opening a pack’ essentially means burning the pack and receiving the intended *n* number of reward units from inside the set of packs, in exchange. - -```solidity -function openPack(uint256 packId, uint256 amountToOpen) external; -``` - -| Parameter | Description | -| --- | --- | -| packId | The identifier of the pack to open. | -| amountToOpen | The number of packs to open at once. | - -### How reward units are selected to distribute on opening packs - -We build on the example in the previous section — ‘How packs work (without web3 terminology)’. - -Each single **square**, **circle** or **star** is considered as a ‘reward unit’. For example, the 5 **stars** in the packs may be “5 units of 1000 USDC”, which is represented in the `Pack` contract as a single `PackContent` as follows: - -```solidity -struct PackContent { - address assetContract; // USDC address - TokenType tokenType; // TokenType.ERC20 - uint256 tokenId; // Not applicable - uint256 totalAmountPacked; // 5000 - uint256 amountDistributedPerOpen; // 1000 -} -``` - -The percentage chance of receiving a particular kind of reward (e.g. a **star**) on opening a pack is calculated as:`(number_of_stars_packed) / (total number of packs)`. Here, `number_of_stars_packed` refers to the total number of reward units of the **star** kind inside the set of packs e.g. a total of 5 units of 1000 USDC. - -Going back to the example in the previous section — ‘How packs work (without web3 terminology)’. — the supply of the reward units in the relevant set of packs - 80 **circles**, 15 **squares**, and 5 **stars -** can be represented on a number line, from zero to the total supply of packs - in this case, 100. - -![pack-diag-2.png](/assets/pack-diag-2.png) - -Whenever a pack is opened, the `Pack` contract uses a new *random* number in the range of the total supply of packs to determine what reward unit will be distributed to the pack opener. - -In our example case, the `Pack` contract uses a random number less than 100 to determine whether the pack opener will receive a **circle**, **square** or a **star**. - -So e.g. if the random number `num` is such that `0 <= num < 5`, the pack opener will receive a **star**. Similarly, if `5 <= num < 20`, the opener will receive a **square**, and if `20 <= num < 100`, the opener will receive a **circle**. - -Note that given this design, the opener truly has a 5% chance of receiving a **star**, a 15% chance of receiving a **square**, and an 80% chance of receiving a **circle**, as long as the random number used in the selection of the reward unit(s) to distribute is truly random. - -## The problem with random numbers - -From the previous section — ‘How reward units are selected to distribute on opening packs’: - -> Note that given this design, the opener truly has a 5% chance of receiving a **star**, a 15% chance of receiving a **square**, and an 80% chance of receiving a **circle**, as long as the random number used in the selection of the reward unit(s) to distribute is truly random. -> - -In the event of a pack opening, the random number used in the process affects what unit of reward is selected by the `Pack` contract to be distributed to the pack owner. - -If a pack owner can predict, at any moment, what random number will be used in this process of the contract selecting what unit of reward to distribute on opening a pack at that moment, the pack owner can selectively open their pack at a moment where they’ll receive the reward they want from the pack. - -This is a **possible** **critical vulnerability** since a core feature of the `Pack` product offering is the guarantee that each reward unit in a pack has a % probability of being distributed on opening a pack, and that this probability has some integrity (in the common sense way). Being able to predict the random numbers, as described above, overturns this guarantee. - -### Sourcing random numbers — solution - -The `Pack` contract requires a design where a pack owner *cannot possibly* predict the random number that will be used in the process of their pack opening. - -To ensure the above, we make a simple check in the `openPack` function: - -```solidity -require(msg.sender == tx.origin, "opener cannot be smart contract"); - -require(_msgSender() == tx.origin, "opener cannot be smart contract"); -``` - -`tx.origin` returns the address of the external account that initiated the transaction, of which the `openPack` function call is a part of. - -The above check essentially means that only an external account i.e. an end user wallet, and no smart contract, can open packs. This lets us generate a pseudo random number using block variables, for the purpose of `openPack`: - -```solidity -uint256 random = uint(keccak256(abi.encodePacked(msg.sender, blockhash(block.number), block.difficulty))); -``` - -Since only end user wallets can open packs, a pack owner *cannot possibly* predict the random number that will be used in the process of their pack opening. That is because a pack opener cannot query the result of the random number calculation during a given block, and call `openPack` within that same block. - -We now list the single most important advantage, and consequent trade-off of using this solution: - -| Advantage | Trade-off | -| --- | --- | -| A pack owner cannot possibly predict the random number that will be used in the process of their pack opening. | Only external accounts / EOAs can open packs. Smart contracts cannot open packs. | - -### Sourcing random numbers — discarded solutions - - We’ll now discuss some possible solutions for this design problem along with their trade-offs / why we do not use these solutions: - -- **Using an oracle (e.g. Chainlink VRF)** - - Using an oracle like Chainlink VRF enables the original design for the `Pack` contract: a pack owner can open *n* number of packs, whenever they want, independent of when the other pack owners choose to open their own packs. All in all — opening *n* packs becomes a closed isolated event performed by a single pack owner. - - ![pack-diag-3.png](/assets/pack-diag-3.png) - - **Why we’re not using this solution:** - - - Chainlink VRF v1 is only on Ethereum and Polygon, and Chainlink VRF v2 (current version) is only on Ethereum and Binance. As a result, this solution cannot be used by itself across all the chains thirdweb supports (and wants to support). - - Each random number request costs an end user Chainlink’s LINK token — it is costly, and seems like a random requirement for using a thirdweb offering. - -- **Delayed-reveal randomness: rewards for all packs in a set of packs visible all at once** - - By ‘delayed-reveal’ randomness, we mean the following — - - - When creating a set of packs, the creator provides (1) an encrypted seed i.e. integer (see the [encryption pattern used in thirdweb’s delayed-reveal NFTs](https://blog.thirdweb.com/delayed-reveal-nfts#step-1-encryption)), and (2) a future block number. - - The created packs are *non-transferrable* by any address except the (1) pack creator, or (2) addresses manually approved by the pack creator. This is to let the creator distribute packs as they desire, *and* is essential for the next step. - - After the specified future block number passes, the creator submits the unencrypted seed to the `Pack` contract. Whenever a pack owner now opens a pack, we calculate the random number to be used in the opening process as follows: - - ```solidity - uint256 random = uint(keccak256(seed, msg.sender, blockhash(storedBlockNumber))); - ``` - - - No one can predict the block hash of the stored future block unless the pack creator is the miner of the block with that block number (highly unlikely). - - The seed is controlled by the creator, submitted at the time of pack creation, and cannot be changed after submission. - - Since packs are non-transferrable in the way described above, as long as the pack opener is not approved to transfer packs, the opener cannot manipulate the value of `random` by transferring packs to a desirable address and then opening the pack from that address. - - **Why we’re not using this solution:** - - - Active involvement from the pack creator. They’re trusted to reveal the unencrypted seed once packs are eligible to be opened. - - Packs *must* be non-transferrable in the way described above, which means they can’t be purchased on a marketplace, etc. Lack of a built-in distribution mechanism for the packs. \ No newline at end of file diff --git a/contracts/package.json b/contracts/package.json index 4bb35e90b..d2030526d 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,7 +1,7 @@ { "name": "@thirdweb-dev/contracts", "description": "Collection of smart contracts deployable via the thirdweb SDK, dashboard and CLI", - "version": "3.0.0-10", + "version": "3.15.0", "license": "Apache-2.0", "repository": { "type": "git", @@ -15,5 +15,15 @@ "files": [ "**/*.sol", "/abi" - ] + ], + "dependencies": { + "@openzeppelin/contracts": "^4.9.6", + "@openzeppelin/contracts-upgradeable": "^4.9.6", + "erc721a-upgradeable": "^3.3.0", + "@thirdweb-dev/dynamic-contracts": "^1.2.4", + "solady": "0.0.180" + }, + "engines": { + "node": ">=18.0.0" + } } diff --git a/contracts/prebuilts/account/dynamic/DynamicAccount.sol b/contracts/prebuilts/account/dynamic/DynamicAccount.sol new file mode 100644 index 000000000..0d5ac3b26 --- /dev/null +++ b/contracts/prebuilts/account/dynamic/DynamicAccount.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable no-inline-assembly */ +/* solhint-disable reason-string */ + +import "../utils/AccountCore.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/presets/BaseRouter.sol"; + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +contract DynamicAccount is AccountCore, BaseRouter { + /*/////////////////////////////////////////////////////////////// + Constructor and Initializer + //////////////////////////////////////////////////////////////*/ + + constructor( + IEntryPoint _entrypoint, + Extension[] memory _defaultExtensions + ) AccountCore(_entrypoint, msg.sender) BaseRouter(_defaultExtensions) { + _disableInitializers(); + } + + /// @notice Initializes the smart contract wallet. + function initialize(address _defaultAdmin, bytes calldata _data) public override initializer { + __BaseRouter_init(); + AccountCoreStorage.data().creationSalt = _generateSalt(_defaultAdmin, _data); + _setAdmin(_defaultAdmin, true); + } + + /*/////////////////////////////////////////////////////////////// + Internal overrides + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns whether all relevant permission and other checks are met before any upgrade. + function _isAuthorizedCallToUpgrade() internal view virtual override returns (bool) { + return isAdmin(msg.sender); + } +} diff --git a/contracts/prebuilts/account/dynamic/DynamicAccountFactory.sol b/contracts/prebuilts/account/dynamic/DynamicAccountFactory.sol new file mode 100644 index 000000000..227a32509 --- /dev/null +++ b/contracts/prebuilts/account/dynamic/DynamicAccountFactory.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +// Utils +import "../utils/BaseAccountFactory.sol"; +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +// Extensions +import "../../../extension/upgradeable//PermissionsEnumerable.sol"; +import "../../../extension/upgradeable//ContractMetadata.sol"; + +// Smart wallet implementation +import { DynamicAccount, IEntryPoint } from "./DynamicAccount.sol"; + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +contract DynamicAccountFactory is BaseAccountFactory, ContractMetadata, PermissionsEnumerable { + address public constant ENTRYPOINT_ADDRESS = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; + + /*/////////////////////////////////////////////////////////////// + Constructor + //////////////////////////////////////////////////////////////*/ + + constructor( + address _defaultAdmin, + IExtension.Extension[] memory _defaultExtensions + ) + BaseAccountFactory( + payable(address(new DynamicAccount(IEntryPoint(ENTRYPOINT_ADDRESS), _defaultExtensions))), + ENTRYPOINT_ADDRESS + ) + { + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Called in `createAccount`. Initializes the account contract created in `createAccount`. + function _initializeAccount(address _account, address _admin, bytes calldata _data) internal override { + DynamicAccount(payable(_account)).initialize(_admin, _data); + } + + /// @dev Returns whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view virtual override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, msg.sender); + } + + /// @notice Returns the sender in the given execution context. + function _msgSender() internal view override(Multicall, Permissions) returns (address) { + return msg.sender; + } +} diff --git a/contracts/prebuilts/account/interfaces/IAccount.sol b/contracts/prebuilts/account/interfaces/IAccount.sol new file mode 100644 index 000000000..5f80a930b --- /dev/null +++ b/contracts/prebuilts/account/interfaces/IAccount.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "./PackedUserOperation.sol"; + +interface IAccount { + /** + * Validate user's signature and nonce + * the entryPoint will make the call to the recipient only if this validation call returns successfully. + * signature failure should be reported by returning SIG_VALIDATION_FAILED (1). + * This allows making a "simulation call" without a valid signature + * Other failures (e.g. nonce mismatch, or invalid signature format) should still revert to signal failure. + * + * @dev Must validate caller is the entryPoint. + * Must validate the signature and nonce + * @param userOp - The operation that is about to be executed. + * @param userOpHash - Hash of the user's request data. can be used as the basis for signature. + * @param missingAccountFunds - Missing funds on the account's deposit in the entrypoint. + * This is the minimum amount to transfer to the sender(entryPoint) to be + * able to make the call. The excess is left as a deposit in the entrypoint + * for future calls. Can be withdrawn anytime using "entryPoint.withdrawTo()". + * In case there is a paymaster in the request (or the current deposit is high + * enough), this value will be zero. + * @return validationData - Packaged ValidationData structure. use `_packValidationData` and + * `_unpackValidationData` to encode and decode. + * <20-byte> sigAuthorizer - 0 for valid signature, 1 to mark signature failure, + * otherwise, an address of an "authorizer" contract. + * <6-byte> validUntil - Last timestamp this operation is valid. 0 for "indefinite" + * <6-byte> validAfter - First timestamp this operation is valid + * If an account doesn't use time-range, it is enough to + * return SIG_VALIDATION_FAILED value (1) for signature failure. + * Note that the validation code cannot use block.timestamp (or block.number) directly. + */ + function validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds + ) external returns (uint256 validationData); +} diff --git a/contracts/prebuilts/account/interfaces/IAccountCore.sol b/contracts/prebuilts/account/interfaces/IAccountCore.sol new file mode 100644 index 000000000..8ea905e81 --- /dev/null +++ b/contracts/prebuilts/account/interfaces/IAccountCore.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.12; + +import "./IAccount.sol"; +import "../../../extension/interface/IAccountPermissions.sol"; +import "../../../extension/interface/IMulticall.sol"; + +interface IAccountCore is IAccount, IAccountPermissions, IMulticall { + /// @dev Returns the address of the factory from which the account was created. + function factory() external view returns (address); +} diff --git a/contracts/prebuilts/account/interfaces/IAccountExecute.sol b/contracts/prebuilts/account/interfaces/IAccountExecute.sol new file mode 100644 index 000000000..157e4ff18 --- /dev/null +++ b/contracts/prebuilts/account/interfaces/IAccountExecute.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "./PackedUserOperation.sol"; + +interface IAccountExecute { + /** + * Account may implement this execute method. + * passing this methodSig at the beginning of callData will cause the entryPoint to pass the full UserOp (and hash) + * to the account. + * The account should skip the methodSig, and use the callData (and optionally, other UserOp fields) + * + * @param userOp - The operation that was just validated. + * @param userOpHash - Hash of the user's request data. + */ + function executeUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external; +} diff --git a/contracts/prebuilts/account/interfaces/IAccountFactory.sol b/contracts/prebuilts/account/interfaces/IAccountFactory.sol new file mode 100644 index 000000000..4452a34be --- /dev/null +++ b/contracts/prebuilts/account/interfaces/IAccountFactory.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "./IAccountFactoryCore.sol"; + +interface IAccountFactory is IAccountFactoryCore { + /*/////////////////////////////////////////////////////////////// + Callback Functions + //////////////////////////////////////////////////////////////*/ + + /// @notice Callback function for an Account to register its signers. + function onSignerAdded(address signer, bytes32 salt) external; + + /// @notice Callback function for an Account to un-register its signers. + function onSignerRemoved(address signer, bytes32 salt) external; +} diff --git a/contracts/prebuilts/account/interfaces/IAccountFactoryCore.sol b/contracts/prebuilts/account/interfaces/IAccountFactoryCore.sol new file mode 100644 index 000000000..672955a5e --- /dev/null +++ b/contracts/prebuilts/account/interfaces/IAccountFactoryCore.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +interface IAccountFactoryCore { + /*/////////////////////////////////////////////////////////////// + Events + //////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when a new Account is created. + event AccountCreated(address indexed account, address indexed accountAdmin); + + /// @notice Emitted when a new signer is added to an Account. + event SignerAdded(address indexed account, address indexed signer); + + /// @notice Emitted when a new signer is added to an Account. + event SignerRemoved(address indexed account, address indexed signer); + + /*/////////////////////////////////////////////////////////////// + Extension Functions + //////////////////////////////////////////////////////////////*/ + + /// @notice Deploys a new Account for admin. + function createAccount(address admin, bytes calldata _data) external returns (address account); + + /*/////////////////////////////////////////////////////////////// + View Functions + //////////////////////////////////////////////////////////////*/ + + /// @notice Returns the address of the Account implementation. + function accountImplementation() external view returns (address); + + /// @notice Returns all accounts created on the factory. + function getAllAccounts() external view returns (address[] memory); + + /// @notice Returns the address of an Account that would be deployed with the given admin signer. + function getAddress(address adminSigner, bytes calldata data) external view returns (address); + + /// @notice Returns all accounts on which a signer has (active or inactive) permissions. + function getAccountsOfSigner(address signer) external view returns (address[] memory accounts); +} diff --git a/contracts/prebuilts/account/interfaces/IAggregator.sol b/contracts/prebuilts/account/interfaces/IAggregator.sol new file mode 100644 index 000000000..18150b0e6 --- /dev/null +++ b/contracts/prebuilts/account/interfaces/IAggregator.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "./PackedUserOperation.sol"; + +/** + * Aggregated Signatures validator. + */ +interface IAggregator { + /** + * Validate aggregated signature. + * Revert if the aggregated signature does not match the given list of operations. + * @param userOps - Array of UserOperations to validate the signature for. + * @param signature - The aggregated signature. + */ + function validateSignatures(PackedUserOperation[] calldata userOps, bytes calldata signature) external view; + + /** + * Validate signature of a single userOp. + * This method should be called by bundler after EntryPointSimulation.simulateValidation() returns + * the aggregator this account uses. + * First it validates the signature over the userOp. Then it returns data to be used when creating the handleOps. + * @param userOp - The userOperation received from the user. + * @return sigForUserOp - The value to put into the signature field of the userOp when calling handleOps. + * (usually empty, unless account and aggregator support some kind of "multisig". + */ + function validateUserOpSignature( + PackedUserOperation calldata userOp + ) external view returns (bytes memory sigForUserOp); + + /** + * Aggregate multiple signatures into a single value. + * This method is called off-chain to calculate the signature to pass with handleOps() + * bundler MAY use optimized custom code perform this aggregation. + * @param userOps - Array of UserOperations to collect the signatures from. + * @return aggregatedSignature - The aggregated signature. + */ + function aggregateSignatures( + PackedUserOperation[] calldata userOps + ) external view returns (bytes memory aggregatedSignature); +} diff --git a/contracts/prebuilts/account/interfaces/IEntryPoint.sol b/contracts/prebuilts/account/interfaces/IEntryPoint.sol new file mode 100644 index 000000000..d938ab2ed --- /dev/null +++ b/contracts/prebuilts/account/interfaces/IEntryPoint.sol @@ -0,0 +1,204 @@ +/** + ** Account-Abstraction (EIP-4337) singleton EntryPoint implementation. + ** Only one instance required on each chain. + **/ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable no-inline-assembly */ +/* solhint-disable reason-string */ + +import "./PackedUserOperation.sol"; +import "./IStakeManager.sol"; +import "./IAggregator.sol"; +import "./INonceManager.sol"; + +interface IEntryPoint is IStakeManager, INonceManager { + /*** + * An event emitted after each successful request. + * @param userOpHash - Unique identifier for the request (hash its entire content, except signature). + * @param sender - The account that generates this request. + * @param paymaster - If non-null, the paymaster that pays for this request. + * @param nonce - The nonce value from the request. + * @param success - True if the sender transaction succeeded, false if reverted. + * @param actualGasCost - Actual amount paid (by account or paymaster) for this UserOperation. + * @param actualGasUsed - Total gas used by this UserOperation (including preVerification, creation, + * validation and execution). + */ + event UserOperationEvent( + bytes32 indexed userOpHash, + address indexed sender, + address indexed paymaster, + uint256 nonce, + bool success, + uint256 actualGasCost, + uint256 actualGasUsed + ); + + /** + * Account "sender" was deployed. + * @param userOpHash - The userOp that deployed this account. UserOperationEvent will follow. + * @param sender - The account that is deployed + * @param factory - The factory used to deploy this account (in the initCode) + * @param paymaster - The paymaster used by this UserOp + */ + event AccountDeployed(bytes32 indexed userOpHash, address indexed sender, address factory, address paymaster); + + /** + * An event emitted if the UserOperation "callData" reverted with non-zero length. + * @param userOpHash - The request unique identifier. + * @param sender - The sender of this request. + * @param nonce - The nonce used in the request. + * @param revertReason - The return bytes from the (reverted) call to "callData". + */ + event UserOperationRevertReason( + bytes32 indexed userOpHash, + address indexed sender, + uint256 nonce, + bytes revertReason + ); + + /** + * An event emitted if the UserOperation Paymaster's "postOp" call reverted with non-zero length. + * @param userOpHash - The request unique identifier. + * @param sender - The sender of this request. + * @param nonce - The nonce used in the request. + * @param revertReason - The return bytes from the (reverted) call to "callData". + */ + event PostOpRevertReason(bytes32 indexed userOpHash, address indexed sender, uint256 nonce, bytes revertReason); + + /** + * UserOp consumed more than prefund. The UserOperation is reverted, and no refund is made. + * @param userOpHash - The request unique identifier. + * @param sender - The sender of this request. + * @param nonce - The nonce used in the request. + */ + event UserOperationPrefundTooLow(bytes32 indexed userOpHash, address indexed sender, uint256 nonce); + + /** + * An event emitted by handleOps(), before starting the execution loop. + * Any event emitted before this event, is part of the validation. + */ + event BeforeExecution(); + + /** + * Signature aggregator used by the following UserOperationEvents within this bundle. + * @param aggregator - The aggregator used for the following UserOperationEvents. + */ + event SignatureAggregatorChanged(address indexed aggregator); + + /** + * A custom revert error of handleOps, to identify the offending op. + * Should be caught in off-chain handleOps simulation and not happen on-chain. + * Useful for mitigating DoS attempts against batchers or for troubleshooting of factory/account/paymaster reverts. + * NOTE: If simulateValidation passes successfully, there should be no reason for handleOps to fail on it. + * @param opIndex - Index into the array of ops to the failed one (in simulateValidation, this is always zero). + * @param reason - Revert reason. The string starts with a unique code "AAmn", + * where "m" is "1" for factory, "2" for account and "3" for paymaster issues, + * so a failure can be attributed to the correct entity. + */ + error FailedOp(uint256 opIndex, string reason); + + /** + * A custom revert error of handleOps, to report a revert by account or paymaster. + * @param opIndex - Index into the array of ops to the failed one (in simulateValidation, this is always zero). + * @param reason - Revert reason. see FailedOp(uint256,string), above + * @param inner - data from inner cought revert reason + * @dev note that inner is truncated to 2048 bytes + */ + error FailedOpWithRevert(uint256 opIndex, string reason, bytes inner); + + error PostOpReverted(bytes returnData); + + /** + * Error case when a signature aggregator fails to verify the aggregated signature it had created. + * @param aggregator The aggregator that failed to verify the signature + */ + error SignatureValidationFailed(address aggregator); + + // Return value of getSenderAddress. + error SenderAddressResult(address sender); + + // UserOps handled, per aggregator. + struct UserOpsPerAggregator { + PackedUserOperation[] userOps; + // Aggregator address + IAggregator aggregator; + // Aggregated signature + bytes signature; + } + + /** + * Execute a batch of UserOperations. + * No signature aggregator is used. + * If any account requires an aggregator (that is, it returned an aggregator when + * performing simulateValidation), then handleAggregatedOps() must be used instead. + * @param ops - The operations to execute. + * @param beneficiary - The address to receive the fees. + */ + function handleOps(PackedUserOperation[] calldata ops, address payable beneficiary) external; + + /** + * Execute a batch of UserOperation with Aggregators + * @param opsPerAggregator - The operations to execute, grouped by aggregator (or address(0) for no-aggregator accounts). + * @param beneficiary - The address to receive the fees. + */ + function handleAggregatedOps( + UserOpsPerAggregator[] calldata opsPerAggregator, + address payable beneficiary + ) external; + + /** + * Generate a request Id - unique identifier for this request. + * The request ID is a hash over the content of the userOp (except the signature), the entrypoint and the chainid. + * @param userOp - The user operation to generate the request ID for. + * @return hash the hash of this UserOperation + */ + function getUserOpHash(PackedUserOperation calldata userOp) external view returns (bytes32); + + /** + * Gas and return values during simulation. + * @param preOpGas - The gas used for validation (including preValidationGas) + * @param prefund - The required prefund for this operation + * @param accountValidationData - returned validationData from account. + * @param paymasterValidationData - return validationData from paymaster. + * @param paymasterContext - Returned by validatePaymasterUserOp (to be passed into postOp) + */ + struct ReturnInfo { + uint256 preOpGas; + uint256 prefund; + uint256 accountValidationData; + uint256 paymasterValidationData; + bytes paymasterContext; + } + + /** + * Returned aggregated signature info: + * The aggregator returned by the account, and its current stake. + */ + struct AggregatorStakeInfo { + address aggregator; + StakeInfo stakeInfo; + } + + /** + * Get counterfactual sender address. + * Calculate the sender contract address that will be generated by the initCode and salt in the UserOperation. + * This method always revert, and returns the address in SenderAddressResult error + * @param initCode - The constructor code to be passed into the UserOperation. + */ + function getSenderAddress(bytes memory initCode) external; + + error DelegateAndRevert(bool success, bytes ret); + + /** + * Helper method for dry-run testing. + * @dev calling this method, the EntryPoint will make a delegatecall to the given data, and report (via revert) the result. + * The method always revert, so is only useful off-chain for dry run calls, in cases where state-override to replace + * actual EntryPoint code is less convenient. + * @param target a target contract to make a delegatecall from entrypoint + * @param data data to pass to target in a delegatecall + */ + function delegateAndRevert(address target, bytes calldata data) external; +} diff --git a/contracts/prebuilts/account/interfaces/INonceManager.sol b/contracts/prebuilts/account/interfaces/INonceManager.sol new file mode 100644 index 000000000..08f3e6525 --- /dev/null +++ b/contracts/prebuilts/account/interfaces/INonceManager.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +interface INonceManager { + /** + * Return the next nonce for this sender. + * Within a given key, the nonce values are sequenced (starting with zero, and incremented by one on each userop) + * But UserOp with different keys can come with arbitrary order. + * + * @param sender the account address + * @param key the high 192 bit of the nonce + * @return nonce a full nonce to pass for next UserOp with this sender. + */ + function getNonce(address sender, uint192 key) external view returns (uint256 nonce); + + /** + * Manually increment the nonce of the sender. + * This method is exposed just for completeness.. + * Account does NOT need to call it, neither during validation, nor elsewhere, + * as the EntryPoint will update the nonce regardless. + * Possible use-case is call it with various keys to "initialize" their nonces to one, so that future + * UserOperations will not pay extra for the first transaction with a given key. + */ + function incrementNonce(uint192 key) external; +} diff --git a/contracts/prebuilts/account/interfaces/IOracle.sol b/contracts/prebuilts/account/interfaces/IOracle.sol new file mode 100644 index 000000000..bef4f2d3c --- /dev/null +++ b/contracts/prebuilts/account/interfaces/IOracle.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +interface IOracle { + function decimals() external view returns (uint8); + function latestRoundData() + external + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); +} diff --git a/contracts/prebuilts/account/interfaces/IPaymaster.sol b/contracts/prebuilts/account/interfaces/IPaymaster.sol new file mode 100644 index 000000000..b501c7148 --- /dev/null +++ b/contracts/prebuilts/account/interfaces/IPaymaster.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "./PackedUserOperation.sol"; + +/** + * The interface exposed by a paymaster contract, who agrees to pay the gas for user's operations. + * A paymaster must hold a stake to cover the required entrypoint stake and also the gas for the transaction. + */ +interface IPaymaster { + enum PostOpMode { + // User op succeeded. + opSucceeded, + // User op reverted. Still has to pay for gas. + opReverted, + // Only used internally in the EntryPoint (cleanup after postOp reverts). Never calling paymaster with this value + postOpReverted + } + + /** + * Payment validation: check if paymaster agrees to pay. + * Must verify sender is the entryPoint. + * Revert to reject this request. + * Note that bundlers will reject this method if it changes the state, unless the paymaster is trusted (whitelisted). + * The paymaster pre-pays using its deposit, and receive back a refund after the postOp method returns. + * @param userOp - The user operation. + * @param userOpHash - Hash of the user's request data. + * @param maxCost - The maximum cost of this transaction (based on maximum gas and gas price from userOp). + * @return context - Value to send to a postOp. Zero length to signify postOp is not required. + * @return validationData - Signature and time-range of this operation, encoded the same as the return + * value of validateUserOperation. + * <20-byte> sigAuthorizer - 0 for valid signature, 1 to mark signature failure, + * other values are invalid for paymaster. + * <6-byte> validUntil - last timestamp this operation is valid. 0 for "indefinite" + * <6-byte> validAfter - first timestamp this operation is valid + * Note that the validation code cannot use block.timestamp (or block.number) directly. + */ + function validatePaymasterUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 maxCost + ) external returns (bytes memory context, uint256 validationData); + + /** + * Post-operation handler. + * Must verify sender is the entryPoint. + * @param mode - Enum with the following options: + * opSucceeded - User operation succeeded. + * opReverted - User op reverted. The paymaster still has to pay for gas. + * postOpReverted - never passed in a call to postOp(). + * @param context - The context value returned by validatePaymasterUserOp + * @param actualGasCost - Actual gas used so far (without this postOp call). + * @param actualUserOpFeePerGas - the gas price this UserOp pays. This value is based on the UserOp's maxFeePerGas + * and maxPriorityFee (and basefee) + * It is not the same as tx.gasprice, which is what the bundler pays. + */ + function postOp( + PostOpMode mode, + bytes calldata context, + uint256 actualGasCost, + uint256 actualUserOpFeePerGas + ) external; +} diff --git a/contracts/prebuilts/account/interfaces/IStakeManager.sol b/contracts/prebuilts/account/interfaces/IStakeManager.sol new file mode 100644 index 000000000..710522cc7 --- /dev/null +++ b/contracts/prebuilts/account/interfaces/IStakeManager.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.0; + +/** + * Manage deposits and stakes. + * Deposit is just a balance used to pay for UserOperations (either by a paymaster or an account). + * Stake is value locked for at least "unstakeDelay" by the staked entity. + */ +interface IStakeManager { + event Deposited(address indexed account, uint256 totalDeposit); + + event Withdrawn(address indexed account, address withdrawAddress, uint256 amount); + + // Emitted when stake or unstake delay are modified. + event StakeLocked(address indexed account, uint256 totalStaked, uint256 unstakeDelaySec); + + // Emitted once a stake is scheduled for withdrawal. + event StakeUnlocked(address indexed account, uint256 withdrawTime); + + event StakeWithdrawn(address indexed account, address withdrawAddress, uint256 amount); + + /** + * @param deposit - The entity's deposit. + * @param staked - True if this entity is staked. + * @param stake - Actual amount of ether staked for this entity. + * @param unstakeDelaySec - Minimum delay to withdraw the stake. + * @param withdrawTime - First block timestamp where 'withdrawStake' will be callable, or zero if already locked. + * @dev Sizes were chosen so that deposit fits into one cell (used during handleOp) + * and the rest fit into a 2nd cell (used during stake/unstake) + * - 112 bit allows for 10^15 eth + * - 48 bit for full timestamp + * - 32 bit allows 150 years for unstake delay + */ + struct DepositInfo { + uint256 deposit; + bool staked; + uint112 stake; + uint32 unstakeDelaySec; + uint48 withdrawTime; + } + + // API struct used by getStakeInfo and simulateValidation. + struct StakeInfo { + uint256 stake; + uint256 unstakeDelaySec; + } + + /** + * Get deposit info. + * @param account - The account to query. + * @return info - Full deposit information of given account. + */ + function getDepositInfo(address account) external view returns (DepositInfo memory info); + + /** + * Get account balance. + * @param account - The account to query. + * @return - The deposit (for gas payment) of the account. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * Add to the deposit of the given account. + * @param account - The account to add to. + */ + function depositTo(address account) external payable; + + /** + * Add to the account's stake - amount and delay + * any pending unstake is first cancelled. + * @param _unstakeDelaySec - The new lock duration before the deposit can be withdrawn. + */ + function addStake(uint32 _unstakeDelaySec) external payable; + + /** + * Attempt to unlock the stake. + * The value can be withdrawn (using withdrawStake) after the unstake delay. + */ + function unlockStake() external; + + /** + * Withdraw from the (unlocked) stake. + * Must first call unlockStake and wait for the unstakeDelay to pass. + * @param withdrawAddress - The address to send withdrawn value. + */ + function withdrawStake(address payable withdrawAddress) external; + + /** + * Withdraw from the deposit. + * @param withdrawAddress - The address to send withdrawn value. + * @param withdrawAmount - The amount to withdraw. + */ + function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) external; +} diff --git a/contracts/prebuilts/account/interfaces/PackedUserOperation.sol b/contracts/prebuilts/account/interfaces/PackedUserOperation.sol new file mode 100644 index 000000000..d1e14a8ea --- /dev/null +++ b/contracts/prebuilts/account/interfaces/PackedUserOperation.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +/** + * User Operation struct + * @param sender - The sender account of this request. + * @param nonce - Unique value the sender uses to verify it is not a replay. + * @param initCode - If set, the account contract will be created by this constructor/ + * @param callData - The method call to execute on this account. + * @param accountGasLimits - Packed gas limits for validateUserOp and gas limit passed to the callData method call. + * @param preVerificationGas - Gas not calculated by the handleOps method, but added to the gas paid. + * Covers batch overhead. + * @param gasFees - packed gas fields maxPriorityFeePerGas and maxFeePerGas - Same as EIP-1559 gas parameters. + * @param paymasterAndData - If set, this field holds the paymaster address, verification gas limit, postOp gas limit and paymaster-specific extra data + * The paymaster will pay for the transaction instead of the sender. + * @param signature - Sender-verified signature over the entire request, the EntryPoint address and the chain ID. + */ +struct PackedUserOperation { + address sender; + uint256 nonce; + bytes initCode; + bytes callData; + bytes32 accountGasLimits; + uint256 preVerificationGas; + bytes32 gasFees; + bytes paymasterAndData; + bytes signature; +} diff --git a/contracts/prebuilts/account/managed/ManagedAccount.sol b/contracts/prebuilts/account/managed/ManagedAccount.sol new file mode 100644 index 000000000..4e70bb605 --- /dev/null +++ b/contracts/prebuilts/account/managed/ManagedAccount.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable no-inline-assembly */ +/* solhint-disable reason-string */ + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +import "../utils/AccountCore.sol"; +import "@thirdweb-dev/dynamic-contracts/src/core/Router.sol"; +import "@thirdweb-dev/dynamic-contracts/src/interface/IRouterState.sol"; + +contract ManagedAccount is AccountCore, Router, IRouterState { + constructor(IEntryPoint _entrypoint, address _factory) AccountCore(_entrypoint, _factory) {} + + /// @notice Returns the implementation contract address for a given function signature. + function getImplementationForFunction(bytes4 _functionSelector) public view virtual override returns (address) { + return Router(payable(factory)).getImplementationForFunction(_functionSelector); + } + + /// @notice Returns all extensions of the Router. + function getAllExtensions() external view returns (Extension[] memory) { + return IRouterState(payable(factory)).getAllExtensions(); + } +} diff --git a/contracts/prebuilts/account/managed/ManagedAccountFactory.sol b/contracts/prebuilts/account/managed/ManagedAccountFactory.sol new file mode 100644 index 000000000..c5f51e3ca --- /dev/null +++ b/contracts/prebuilts/account/managed/ManagedAccountFactory.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +// Utils +import "@thirdweb-dev/dynamic-contracts/src/presets/BaseRouter.sol"; +import "../utils/BaseAccountFactory.sol"; + +// Extensions +import "../../../extension/upgradeable//PermissionsEnumerable.sol"; +import "../../../extension/upgradeable//ContractMetadata.sol"; + +// Smart wallet implementation +import { ManagedAccount, IEntryPoint } from "./ManagedAccount.sol"; + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +contract ManagedAccountFactory is BaseAccountFactory, ContractMetadata, PermissionsEnumerable, BaseRouter { + /*/////////////////////////////////////////////////////////////// + Constructor + //////////////////////////////////////////////////////////////*/ + + constructor( + address _defaultAdmin, + IEntryPoint _entrypoint, + Extension[] memory _defaultExtensions + ) + BaseRouter(_defaultExtensions) + BaseAccountFactory(payable(address(new ManagedAccount(_entrypoint, address(this)))), address(_entrypoint)) + { + __BaseRouter_init(); + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + + bytes32 _extensionRole = keccak256("EXTENSION_ROLE"); + _setupRole(_extensionRole, _defaultAdmin); + _setRoleAdmin(_extensionRole, _extensionRole); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Called in `createAccount`. Initializes the account contract created in `createAccount`. + function _initializeAccount(address _account, address _admin, bytes calldata _data) internal override { + ManagedAccount(payable(_account)).initialize(_admin, _data); + } + + /// @dev Returns whether all relevant permission and other checks are met before any upgrade. + function _isAuthorizedCallToUpgrade() internal view virtual override returns (bool) { + return hasRole(keccak256("EXTENSION_ROLE"), msg.sender); + } + + /// @dev Returns whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view virtual override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, msg.sender); + } + + /// @notice Returns the sender in the given execution context. + function _msgSender() internal view override(Multicall, Permissions) returns (address) { + return msg.sender; + } +} diff --git a/contracts/prebuilts/account/non-upgradeable/Account.sol b/contracts/prebuilts/account/non-upgradeable/Account.sol new file mode 100644 index 000000000..14e2a5463 --- /dev/null +++ b/contracts/prebuilts/account/non-upgradeable/Account.sol @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable no-inline-assembly */ +/* solhint-disable reason-string */ + +// Extensions +import "../utils/AccountCore.sol"; +import "../../../extension/upgradeable/ContractMetadata.sol"; +import "../../../external-deps/openzeppelin/token/ERC721/utils/ERC721Holder.sol"; +import "../../../external-deps/openzeppelin/token/ERC1155/utils/ERC1155Holder.sol"; + +// Utils +import "../../../eip/ERC1271.sol"; +import "../utils/Helpers.sol"; +import "../../../external-deps/openzeppelin/utils/cryptography/ECDSA.sol"; +import "../utils/BaseAccountFactory.sol"; + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +contract Account is AccountCore, ContractMetadata, ERC1271, ERC721Holder, ERC1155Holder { + using ECDSA for bytes32; + using EnumerableSet for EnumerableSet.AddressSet; + + bytes32 private constant MSG_TYPEHASH = keccak256("AccountMessage(bytes message)"); + + /*/////////////////////////////////////////////////////////////// + Constructor, Initializer, Modifiers + //////////////////////////////////////////////////////////////*/ + + constructor(IEntryPoint _entrypoint, address _factory) AccountCore(_entrypoint, _factory) {} + + /// @notice Checks whether the caller is the EntryPoint contract or the admin. + modifier onlyAdminOrEntrypoint() virtual { + require(msg.sender == address(entryPoint()) || isAdmin(msg.sender), "Account: not admin or EntryPoint."); + _; + } + + /// @notice Lets the account receive native tokens. + receive() external payable {} + + /*/////////////////////////////////////////////////////////////// + View functions + //////////////////////////////////////////////////////////////*/ + + /// @notice See {IERC165-supportsInterface}. + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC1155Receiver) returns (bool) { + return + interfaceId == type(IERC1155Receiver).interfaceId || + interfaceId == type(IERC721Receiver).interfaceId || + super.supportsInterface(interfaceId); + } + + /** + * @notice See EIP-1271 + * + * @param _hash The original message hash of the data to sign (before mixing this contract's domain separator) + * @param _signature The signature produced on signing the typed data hash (result of `getMessageHash(abi.encode(rawData))`) + */ + function isValidSignature( + bytes32 _hash, + bytes memory _signature + ) public view virtual override returns (bytes4 magicValue) { + bytes32 targetDigest = getMessageHash(_hash); + address signer = targetDigest.recover(_signature); + + if (isAdmin(signer)) { + return MAGICVALUE; + } + + address caller = msg.sender; + EnumerableSet.AddressSet storage approvedTargets = _accountPermissionsStorage().approvedTargets[signer]; + + require( + approvedTargets.contains(caller) || (approvedTargets.length() == 1 && approvedTargets.at(0) == address(0)), + "Account: caller not approved target." + ); + + if (isActiveSigner(signer)) { + magicValue = MAGICVALUE; + } + } + + /** + * @notice Returns the hash of message that should be signed for EIP1271 verification. + * @param _hash The message hash to sign for the EIP-1271 origin verifying contract. + * @return messageHash The digest to sign for EIP-1271 verification. + */ + function getMessageHash(bytes32 _hash) public view returns (bytes32) { + bytes32 messageHash = keccak256(abi.encode(_hash)); + bytes32 typedDataHash = keccak256(abi.encode(MSG_TYPEHASH, messageHash)); + return keccak256(abi.encodePacked("\x19\x01", _domainSeparatorV4(), typedDataHash)); + } + + /*/////////////////////////////////////////////////////////////// + External functions + //////////////////////////////////////////////////////////////*/ + + /// @notice Executes a transaction (called directly from an admin, or by entryPoint) + function execute(address _target, uint256 _value, bytes calldata _calldata) external virtual onlyAdminOrEntrypoint { + _registerOnFactory(); + _call(_target, _value, _calldata); + } + + /// @notice Executes a sequence transaction (called directly from an admin, or by entryPoint) + function executeBatch( + address[] calldata _target, + uint256[] calldata _value, + bytes[] calldata _calldata + ) external virtual onlyAdminOrEntrypoint { + _registerOnFactory(); + + require(_target.length == _calldata.length && _target.length == _value.length, "Account: wrong array lengths."); + for (uint256 i = 0; i < _target.length; i++) { + _call(_target[i], _value[i], _calldata[i]); + } + } + + /// @notice Deposit funds for this account in Entrypoint. + function addDeposit() public payable { + entryPoint().depositTo{ value: msg.value }(address(this)); + } + + /// @notice Withdraw funds for this account from Entrypoint. + function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public { + _onlyAdmin(); + entryPoint().withdrawTo(withdrawAddress, amount); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Registers the account on the factory if it hasn't been registered yet. + function _registerOnFactory() internal virtual { + BaseAccountFactory factoryContract = BaseAccountFactory(factory); + if (!factoryContract.isRegistered(address(this))) { + factoryContract.onRegister(AccountCoreStorage.data().creationSalt); + } + } + + /// @dev Calls a target contract and reverts if it fails. + function _call( + address _target, + uint256 value, + bytes memory _calldata + ) internal virtual returns (bytes memory result) { + bool success; + (success, result) = _target.call{ value: value }(_calldata); + if (!success) { + assembly { + revert(add(result, 32), mload(result)) + } + } + } + + /// @dev Returns whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view virtual override returns (bool) { + return isAdmin(msg.sender) || msg.sender == address(this); + } +} diff --git a/contracts/prebuilts/account/non-upgradeable/AccountFactory.sol b/contracts/prebuilts/account/non-upgradeable/AccountFactory.sol new file mode 100644 index 000000000..1ab5ce47c --- /dev/null +++ b/contracts/prebuilts/account/non-upgradeable/AccountFactory.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +// Utils +import "../utils/BaseAccountFactory.sol"; +import "../../../external-deps/openzeppelin/proxy/Clones.sol"; + +// Extensions +import "../../../extension/upgradeable//PermissionsEnumerable.sol"; +import "../../../extension/upgradeable//ContractMetadata.sol"; + +// Smart wallet implementation +import { Account } from "./Account.sol"; + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +contract AccountFactory is BaseAccountFactory, ContractMetadata, PermissionsEnumerable { + /*/////////////////////////////////////////////////////////////// + Constructor + //////////////////////////////////////////////////////////////*/ + + constructor( + address _defaultAdmin, + IEntryPoint _entrypoint + ) BaseAccountFactory(address(new Account(_entrypoint, address(this))), address(_entrypoint)) { + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Called in `createAccount`. Initializes the account contract created in `createAccount`. + function _initializeAccount(address _account, address _admin, bytes calldata _data) internal override { + Account(payable(_account)).initialize(_admin, _data); + } + + /// @dev Returns whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view virtual override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, msg.sender); + } + + /// @notice Returns the sender in the given execution context. + function _msgSender() internal view override(Multicall, Permissions) returns (address) { + return msg.sender; + } +} diff --git a/contracts/prebuilts/account/token-bound-account/TokenBoundAccount.sol b/contracts/prebuilts/account/token-bound-account/TokenBoundAccount.sol new file mode 100644 index 000000000..e6ffda8ee --- /dev/null +++ b/contracts/prebuilts/account/token-bound-account/TokenBoundAccount.sol @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.0; + +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable no-inline-assembly */ +/* solhint-disable reason-string */ + +// Base +import "../utils/BaseAccount.sol"; + +// Extensions +import "../../../extension/Multicall.sol"; +import "../../../extension/upgradeable/Initializable.sol"; +import "../../../extension/upgradeable/ContractMetadata.sol"; +import "../../../external-deps/openzeppelin/token/ERC721/utils/ERC721Holder.sol"; +import "../../../external-deps/openzeppelin/token/ERC1155/utils/ERC1155Holder.sol"; +import "../../../eip/ERC1271.sol"; + +// Utils +import "../../../external-deps/openzeppelin/utils/cryptography/ECDSA.sol"; +import "../utils/BaseAccountFactory.sol"; + +import "./erc6551-utils/ERC6551AccountLib.sol"; +import "./erc6551-utils/IERC6551Account.sol"; + +import "../../../eip/interface/IERC721.sol"; +import "../non-upgradeable/Account.sol"; + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +contract TokenBoundAccount is + Initializable, + ERC1271, + Multicall, + BaseAccount, + ContractMetadata, + ERC721Holder, + ERC1155Holder, + IERC6551Account, + EIP712 +{ + using ECDSA for bytes32; + + /*/////////////////////////////////////////////////////////////// + Events + //////////////////////////////////////////////////////////////*/ + + event TokenBoundAccountCreated(address indexed account, bytes indexed data); + + /*/////////////////////////////////////////////////////////////// + State + //////////////////////////////////////////////////////////////*/ + + /// @notice EIP 4337 factory for this contract. + address public immutable factory; + + /// @notice EIP 4337 Entrypoint contract. + IEntryPoint private immutable entrypointContract; + + uint256 public state; + + /*/////////////////////////////////////////////////////////////// + Constructor, Initializer, Modifiers + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Executes once when a contract is created to initialize state variables + * + * @param _entrypoint - 0x0000000071727De22E5E9d8BAf0edAc6f37da032 + * @param _factory - The factory contract address to issue token Bound accounts + * + */ + constructor(IEntryPoint _entrypoint, address _factory) EIP712("TokenBoundAccount", "1") { + _disableInitializers(); + factory = _factory; + entrypointContract = _entrypoint; + } + + // solhint-disable-next-line no-empty-blocks + receive() external payable virtual {} + + /// @notice Initializes the smart contract wallet. + function initialize(address _defaultAdmin, bytes calldata _data) public virtual initializer { + emit TokenBoundAccountCreated(_defaultAdmin, _data); + } + + /// @notice Returns whether a signer is authorized to perform transactions using the wallet. + function isValidSigner(address _signer, PackedUserOperation calldata) public view returns (bool) { + return (owner() == _signer); + } + + function isValidSigner(address signer, bytes calldata) external view returns (bytes4) { + if (_isValidSigner(signer)) { + return IERC6551Account.isValidSigner.selector; + } + return bytes4(0); + } + + function _isValidSigner(address signer) internal view returns (bool) { + return signer == owner(); + } + + /// @notice See EIP-1271 + function isValidSignature( + bytes32 _hash, + bytes memory _signature + ) public view virtual override returns (bytes4 magicValue) { + address signer = _hash.recover(_signature); + + if (owner() == signer) { + magicValue = MAGICVALUE; + } + } + + function owner() public view returns (address) { + (uint256 chainId, address tokenContract, uint256 tokenId) = ERC6551AccountLib.token(); + + if (chainId != block.chainid) return address(0); + + return IERC721(tokenContract).ownerOf(tokenId); + } + + /// @notice Withdraw funds for this account from Entrypoint. + function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public virtual { + require(owner() == msg.sender, "Account: not NFT owner"); + entryPoint().withdrawTo(withdrawAddress, amount); + } + + function token() external view returns (uint256 chainId, address tokenContract, uint256 tokenId) { + return ERC6551AccountLib.token(); + } + + /// @notice See {IERC165-supportsInterface}. + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC1155Receiver) returns (bool) { + return + interfaceId == type(IERC1155Receiver).interfaceId || + interfaceId == type(IERC721Receiver).interfaceId || + super.supportsInterface(interfaceId); + } + + /// @notice Returns the EIP 4337 entrypoint contract. + function entryPoint() public view virtual override returns (IEntryPoint) { + return entrypointContract; + } + + /// @notice Returns the balance of the account in Entrypoint. + function getDeposit() public view virtual returns (uint256) { + return entryPoint().balanceOf(address(this)); + } + + /// @notice Executes a transaction (called directly from an admin, or by entryPoint) + function execute(address _target, uint256 _value, bytes calldata _calldata) external virtual onlyAdminOrEntrypoint { + _call(_target, _value, _calldata); + } + + /// @notice Executes a sequence transaction (called directly from an admin, or by entryPoint) + function executeBatch( + address[] calldata _target, + uint256[] calldata _value, + bytes[] calldata _calldata + ) external virtual onlyAdminOrEntrypoint { + require(_target.length == _calldata.length && _target.length == _value.length, "Account: wrong array lengths."); + for (uint256 i = 0; i < _target.length; i++) { + _call(_target[i], _value[i], _calldata[i]); + } + } + + /// @notice Deposit funds for this account in Entrypoint. + function addDeposit() public payable virtual { + entryPoint().depositTo{ value: msg.value }(address(this)); + } + + /*/////////////////////////////////////////////////////////////// + Internal Functions + //////////////////////////////////////////////////////////////*/ + + function _call( + address _target, + uint256 value, + bytes memory _calldata + ) internal virtual returns (bytes memory result) { + ++state; + bool success; + (success, result) = _target.call{ value: value }(_calldata); + if (!success) { + assembly { + revert(add(result, 32), mload(result)) + } + } + } + + /// @notice Validates the signature of a user operation. + function _validateSignature( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal virtual override returns (uint256 validationData) { + bytes32 hash = userOpHash.toEthSignedMessageHash(); + address signer = hash.recover(userOp.signature); + + if (!isValidSigner(signer, userOp)) return SIG_VALIDATION_FAILED; + return 0; + } + + function getFunctionSignature(bytes calldata data) internal pure returns (bytes4 functionSelector) { + require(data.length >= 4, "Data too short"); + return bytes4(data[:4]); + } + + function decodeExecuteCalldata(bytes calldata data) internal pure returns (address _target, uint256 _value) { + require(data.length >= 4 + 32 + 32, "Data too short"); + + // Decode the address, which is bytes 4 to 35 + _target = abi.decode(data[4:36], (address)); + + // Decode the value, which is bytes 36 to 68 + _value = abi.decode(data[36:68], (uint256)); + } + + function decodeExecuteBatchCalldata( + bytes calldata data + ) internal pure returns (address[] memory _targets, uint256[] memory _values, bytes[] memory _callData) { + require(data.length >= 4 + 32 + 32 + 32, "Data too short"); + + (_targets, _values, _callData) = abi.decode(data[4:], (address[], uint256[], bytes[])); + } + + /// @dev Returns whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /*/////////////////////////////////////////////////////////////// + Modifiers + //////////////////////////////////////////////////////////////*/ + + modifier onlyAdminOrEntrypoint() { + require(msg.sender == address(entryPoint()) || msg.sender == owner(), "Account: not admin or EntryPoint."); + _; + } + + modifier onlyAdmin() { + require(msg.sender == owner(), "Account: not admin."); + _; + } +} diff --git a/contracts/prebuilts/account/token-bound-account/erc6551-utils/ERC6551AccountLib.sol b/contracts/prebuilts/account/token-bound-account/erc6551-utils/ERC6551AccountLib.sol new file mode 100644 index 000000000..ff99e843c --- /dev/null +++ b/contracts/prebuilts/account/token-bound-account/erc6551-utils/ERC6551AccountLib.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../../../../external-deps/openzeppelin/utils/Create2.sol"; +import "./ERC6551BytecodeLib.sol"; + +library ERC6551AccountLib { + function computeAddress( + address registry, + address implementation, + uint256 chainId, + address tokenContract, + uint256 tokenId, + uint256 _salt + ) internal pure returns (address) { + bytes32 bytecodeHash = keccak256( + ERC6551BytecodeLib.getCreationCode(implementation, chainId, tokenContract, tokenId, _salt) + ); + + return Create2.computeAddress(bytes32(_salt), bytecodeHash, registry); + } + + function token() internal view returns (uint256, address, uint256) { + bytes memory footer = new bytes(0x60); + + assembly { + // copy 0x60 bytes from end of footer + extcodecopy(address(), add(footer, 0x20), 0x4d, 0xad) + } + + return abi.decode(footer, (uint256, address, uint256)); + } + + function salt() internal view returns (uint256) { + bytes memory footer = new bytes(0x20); + + assembly { + // copy 0x20 bytes from beginning of footer + extcodecopy(address(), add(footer, 0x20), 0x2d, 0x4d) + } + + return abi.decode(footer, (uint256)); + } +} diff --git a/contracts/prebuilts/account/token-bound-account/erc6551-utils/ERC6551BytecodeLib.sol b/contracts/prebuilts/account/token-bound-account/erc6551-utils/ERC6551BytecodeLib.sol new file mode 100644 index 000000000..604ba74d2 --- /dev/null +++ b/contracts/prebuilts/account/token-bound-account/erc6551-utils/ERC6551BytecodeLib.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +library ERC6551BytecodeLib { + function getCreationCode( + address implementation_, + uint256 chainId_, + address tokenContract_, + uint256 tokenId_, + uint256 salt_ + ) internal pure returns (bytes memory) { + return + abi.encodePacked( + hex"3d60ad80600a3d3981f3363d3d373d3d3d363d73", + implementation_, + hex"5af43d82803e903d91602b57fd5bf3", + abi.encode(salt_, chainId_, tokenContract_, tokenId_) + ); + } +} diff --git a/contracts/prebuilts/account/token-bound-account/erc6551-utils/IERC6551Account.sol b/contracts/prebuilts/account/token-bound-account/erc6551-utils/IERC6551Account.sol new file mode 100644 index 000000000..160e27e7b --- /dev/null +++ b/contracts/prebuilts/account/token-bound-account/erc6551-utils/IERC6551Account.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @dev the ERC-165 identifier for this interface is `0x6faff5f1` +interface IERC6551Account { + /** + * @dev Allows the account to receive Ether + * + * Accounts MUST implement a `receive` function + * + * Accounts MAY perform arbitrary logic to restrict conditions + * under which Ether can be received + */ + receive() external payable; + + /** + * @dev Returns the identifier of the non-fungible token which owns the account + * + * The return value of this function MUST be constant - it MUST NOT change over time + * + * @return chainId The EIP-155 ID of the chain the token exists on + * @return tokenContract The contract address of the token + * @return tokenId The ID of the token + */ + function token() external view returns (uint256 chainId, address tokenContract, uint256 tokenId); + + /** + * @dev Returns a value that SHOULD be modified each time the account changes state + * + * @return The current account state + */ + function state() external view returns (uint256); + + /** + * @dev Returns a magic value indicating whether a given signer is authorized to act on behalf + * of the account + * + * MUST return the bytes4 magic value 0x523e3260 if the given signer is valid + * + * By default, the holder of the non-fungible token the account is bound to MUST be considered + * a valid signer + * + * Accounts MAY implement additional authorization logic which invalidates the holder as a + * signer or grants signing permissions to other non-holder accounts + * + * @param signer The address to check signing authorization for + * @param context Additional data used to determine whether the signer is valid + * @return magicValue Magic value indicating whether the signer is valid + */ + function isValidSigner(address signer, bytes calldata context) external view returns (bytes4 magicValue); +} diff --git a/contracts/prebuilts/account/token-paymaster/BasePaymaster.sol b/contracts/prebuilts/account/token-paymaster/BasePaymaster.sol new file mode 100644 index 000000000..a1b337ab3 --- /dev/null +++ b/contracts/prebuilts/account/token-paymaster/BasePaymaster.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +/* solhint-disable reason-string */ + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import "../interfaces/IPaymaster.sol"; +import "../interfaces/IEntryPoint.sol"; +import "../utils/UserOperationLib.sol"; +/** + * Helper class for creating a paymaster. + * provides helper methods for staking. + * Validates that the postOp is called only by the entryPoint. + */ +abstract contract BasePaymaster is IPaymaster, Ownable { + IEntryPoint public immutable entryPoint; + + uint256 internal constant PAYMASTER_VALIDATION_GAS_OFFSET = UserOperationLib.PAYMASTER_VALIDATION_GAS_OFFSET; + uint256 internal constant PAYMASTER_POSTOP_GAS_OFFSET = UserOperationLib.PAYMASTER_POSTOP_GAS_OFFSET; + uint256 internal constant PAYMASTER_DATA_OFFSET = UserOperationLib.PAYMASTER_DATA_OFFSET; + + constructor(IEntryPoint _entryPoint) Ownable() { + _validateEntryPointInterface(_entryPoint); + entryPoint = _entryPoint; + } + + //sanity check: make sure this EntryPoint was compiled against the same + // IEntryPoint of this paymaster + function _validateEntryPointInterface(IEntryPoint _entryPoint) internal virtual { + require( + IERC165(address(_entryPoint)).supportsInterface(type(IEntryPoint).interfaceId), + "IEntryPoint interface mismatch" + ); + } + + /// @inheritdoc IPaymaster + function validatePaymasterUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 maxCost + ) external override returns (bytes memory context, uint256 validationData) { + _requireFromEntryPoint(); + return _validatePaymasterUserOp(userOp, userOpHash, maxCost); + } + + /** + * Validate a user operation. + * @param userOp - The user operation. + * @param userOpHash - The hash of the user operation. + * @param maxCost - The maximum cost of the user operation. + */ + function _validatePaymasterUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 maxCost + ) internal virtual returns (bytes memory context, uint256 validationData); + + /// @inheritdoc IPaymaster + function postOp( + PostOpMode mode, + bytes calldata context, + uint256 actualGasCost, + uint256 actualUserOpFeePerGas + ) external override { + _requireFromEntryPoint(); + _postOp(mode, context, actualGasCost, actualUserOpFeePerGas); + } + + /** + * Post-operation handler. + * (verified to be called only through the entryPoint) + * @dev If subclass returns a non-empty context from validatePaymasterUserOp, + * it must also implement this method. + * @param mode - Enum with the following options: + * opSucceeded - User operation succeeded. + * opReverted - User op reverted. The paymaster still has to pay for gas. + * postOpReverted - never passed in a call to postOp(). + * @param context - The context value returned by validatePaymasterUserOp + * @param actualGasCost - Actual gas used so far (without this postOp call). + * @param actualUserOpFeePerGas - the gas price this UserOp pays. This value is based on the UserOp's maxFeePerGas + * and maxPriorityFee (and basefee) + * It is not the same as tx.gasprice, which is what the bundler pays. + */ + function _postOp( + PostOpMode mode, + bytes calldata context, + uint256 actualGasCost, + uint256 actualUserOpFeePerGas + ) internal virtual { + (mode, context, actualGasCost, actualUserOpFeePerGas); // unused params + // subclass must override this method if validatePaymasterUserOp returns a context + revert("must override"); + } + + /** + * Add a deposit for this paymaster, used for paying for transaction fees. + */ + function deposit() public payable { + entryPoint.depositTo{ value: msg.value }(address(this)); + } + + /** + * Withdraw value from the deposit. + * @param withdrawAddress - Target to send to. + * @param amount - Amount to withdraw. + */ + function withdrawTo(address payable withdrawAddress, uint256 amount) public onlyOwner { + entryPoint.withdrawTo(withdrawAddress, amount); + } + + /** + * Add stake for this paymaster. + * This method can also carry eth value to add to the current stake. + * @param unstakeDelaySec - The unstake delay for this paymaster. Can only be increased. + */ + function addStake(uint32 unstakeDelaySec) external payable onlyOwner { + entryPoint.addStake{ value: msg.value }(unstakeDelaySec); + } + + /** + * Return current paymaster's deposit on the entryPoint. + */ + function getDeposit() public view returns (uint256) { + return entryPoint.balanceOf(address(this)); + } + + /** + * Unlock the stake, in order to withdraw it. + * The paymaster can't serve requests once unlocked, until it calls addStake again + */ + function unlockStake() external onlyOwner { + entryPoint.unlockStake(); + } + + /** + * Withdraw the entire paymaster's stake. + * stake must be unlocked first (and then wait for the unstakeDelay to be over) + * @param withdrawAddress - The address to send withdrawn value. + */ + function withdrawStake(address payable withdrawAddress) external onlyOwner { + entryPoint.withdrawStake(withdrawAddress); + } + + /** + * Validate the call is made from a valid entrypoint + */ + function _requireFromEntryPoint() internal virtual { + require(msg.sender == address(entryPoint), "Sender not EntryPoint"); + } +} diff --git a/contracts/prebuilts/account/token-paymaster/TokenPaymaster.sol b/contracts/prebuilts/account/token-paymaster/TokenPaymaster.sol new file mode 100644 index 000000000..c7104c763 --- /dev/null +++ b/contracts/prebuilts/account/token-paymaster/TokenPaymaster.sol @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +// Import the required libraries and contracts +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; + +import "../interfaces/IEntryPoint.sol"; +import "./BasePaymaster.sol"; +import "../utils/Helpers.sol"; +import "../utils/UniswapHelper.sol"; +import "../utils/OracleHelper.sol"; + +/// @title Sample ERC-20 Token Paymaster for ERC-4337 +/// This Paymaster covers gas fees in exchange for ERC20 tokens charged using allowance pre-issued by ERC-4337 accounts. +/// The contract refunds excess tokens if the actual gas cost is lower than the initially provided amount. +/// The token price cannot be queried in the validation code due to storage access restrictions of ERC-4337. +/// The price is cached inside the contract and is updated in the 'postOp' stage if the change is >10%. +/// It is theoretically possible the token has depreciated so much since the last 'postOp' the refund becomes negative. +/// The contract reverts the inner user transaction in that case but keeps the charge. +/// The contract also allows honest clients to prepay tokens at a higher price to avoid getting reverted. +/// It also allows updating price configuration and withdrawing tokens by the contract owner. +/// The contract uses an Oracle to fetch the latest token prices. +/// @dev Inherits from BasePaymaster. +contract TokenPaymaster is BasePaymaster, UniswapHelper, OracleHelper { + using UserOperationLib for PackedUserOperation; + + struct TokenPaymasterConfig { + /// @notice The price markup percentage applied to the token price (1e26 = 100%). Ranges from 1e26 to 2e26 + uint256 priceMarkup; + /// @notice Exchange tokens to native currency if the EntryPoint balance of this Paymaster falls below this value + uint128 minEntryPointBalance; + /// @notice Estimated gas cost for refunding tokens after the transaction is completed + uint48 refundPostopCost; + /// @notice Transactions are only valid as long as the cached price is not older than this value + uint48 priceMaxAge; + } + + event ConfigUpdated(TokenPaymasterConfig tokenPaymasterConfig); + + event UserOperationSponsored( + address indexed user, + uint256 actualTokenCharge, + uint256 actualGasCost, + uint256 actualTokenPriceWithMarkup + ); + + event Received(address indexed sender, uint256 value); + + /// @notice All 'price' variables are multiplied by this value to avoid rounding up + uint256 private constant PRICE_DENOMINATOR = 1e26; + + TokenPaymasterConfig public tokenPaymasterConfig; + + uint256 private immutable _tokenDecimals; + + /// @notice Initializes the TokenPaymaster contract with the given parameters. + /// @param _token The ERC20 token used for transaction fee payments. + /// @param _entryPoint The EntryPoint contract used in the Account Abstraction infrastructure. + /// @param _wrappedNative The ERC-20 token that wraps the native asset for current chain. + /// @param _uniswap The Uniswap V3 SwapRouter contract. + /// @param _tokenPaymasterConfig The configuration for the Token Paymaster. + /// @param _oracleHelperConfig The configuration for the Oracle Helper. + /// @param _uniswapHelperConfig The configuration for the Uniswap Helper. + /// @param _owner The address that will be set as the owner of the contract. + constructor( + IERC20Metadata _token, + IEntryPoint _entryPoint, + IERC20 _wrappedNative, + IV3SwapRouter _uniswap, + TokenPaymasterConfig memory _tokenPaymasterConfig, + OracleHelperConfig memory _oracleHelperConfig, + UniswapHelperConfig memory _uniswapHelperConfig, + address _owner + ) + BasePaymaster(_entryPoint) + OracleHelper(_oracleHelperConfig) + UniswapHelper(_token, _wrappedNative, _uniswap, _uniswapHelperConfig) + { + _tokenDecimals = _token.decimals(); + require(_tokenDecimals <= 18, "TPM: token not supported"); + + setTokenPaymasterConfig(_tokenPaymasterConfig); + transferOwnership(_owner); + } + + /// @notice Updates the configuration for the Token Paymaster. + /// @param _tokenPaymasterConfig The new configuration struct. + function setTokenPaymasterConfig(TokenPaymasterConfig memory _tokenPaymasterConfig) public onlyOwner { + require(_tokenPaymasterConfig.priceMarkup <= 2 * PRICE_DENOMINATOR, "TPM: price markup too high"); + require(_tokenPaymasterConfig.priceMarkup >= PRICE_DENOMINATOR, "TPM: price markup too low"); + tokenPaymasterConfig = _tokenPaymasterConfig; + emit ConfigUpdated(_tokenPaymasterConfig); + } + + function setUniswapConfiguration(UniswapHelperConfig memory _uniswapHelperConfig) external onlyOwner { + _setUniswapHelperConfiguration(_uniswapHelperConfig); + } + + function setOracleConfiguration(OracleHelperConfig memory _oracleHelperConfig) external onlyOwner { + _setOracleConfiguration(_oracleHelperConfig); + } + + /// @notice Allows the contract owner to withdraw a specified amount of tokens from the contract. + /// @param to The address to transfer the tokens to. + /// @param amount The amount of tokens to transfer. + function withdrawToken(address to, uint256 amount) external onlyOwner { + SafeERC20.safeTransfer(token, to, amount); + } + + /// @notice Validates a paymaster user operation and calculates the required token amount for the transaction. + /// @param userOp The user operation data. + /// @param requiredPreFund The maximum cost (in native token) the paymaster has to prefund. + /// @return context The context containing the token amount and user sender address (if applicable). + /// @return validationResult A uint256 value indicating the result of the validation (always 0 in this implementation). + function _validatePaymasterUserOp( + PackedUserOperation calldata userOp, + bytes32, + uint256 requiredPreFund + ) internal override returns (bytes memory context, uint256 validationResult) { + unchecked { + uint256 priceMarkup = tokenPaymasterConfig.priceMarkup; + uint256 dataLength = userOp.paymasterAndData.length - PAYMASTER_DATA_OFFSET; + require(dataLength == 0 || dataLength == 32, "TPM: invalid data length"); + uint256 maxFeePerGas = userOp.unpackMaxFeePerGas(); + uint256 refundPostopCost = tokenPaymasterConfig.refundPostopCost; + require(refundPostopCost < userOp.unpackPostOpGasLimit(), "TPM: postOpGasLimit too low"); + uint256 preChargeNative = requiredPreFund + (refundPostopCost * maxFeePerGas); + + bool forceUpdate = (block.timestamp - cachedPriceTimestamp) > tokenPaymasterConfig.priceMaxAge; + updateCachedPrice(forceUpdate); + + // note: as price is in native-asset-per-token and we want more tokens increasing it means dividing it by markup + uint256 cachedPriceWithMarkup = (cachedPrice * PRICE_DENOMINATOR) / priceMarkup; + if (dataLength == 32) { + uint256 clientSuppliedPrice = uint256( + bytes32(userOp.paymasterAndData[PAYMASTER_DATA_OFFSET:PAYMASTER_DATA_OFFSET + 32]) + ); + if (clientSuppliedPrice < cachedPriceWithMarkup) { + // note: smaller number means 'more native asset per token' + cachedPriceWithMarkup = clientSuppliedPrice; + } + } + uint256 tokenAmount = weiToToken(preChargeNative, _tokenDecimals, cachedPriceWithMarkup); + SafeERC20.safeTransferFrom(token, userOp.sender, address(this), tokenAmount); + context = abi.encode(tokenAmount, userOp.sender); + validationResult = _packValidationData( + false, + uint48(cachedPriceTimestamp + tokenPaymasterConfig.priceMaxAge), + 0 + ); + } + } + + /// @notice Performs post-operation tasks, such as updating the token price and refunding excess tokens. + /// @dev This function is called after a user operation has been executed or reverted. + /// @param context The context containing the token amount and user sender address. + /// @param actualGasCost The actual gas cost of the transaction. + /// @param actualUserOpFeePerGas - the gas price this UserOp pays. This value is based on the UserOp's maxFeePerGas + // and maxPriorityFee (and basefee) + // It is not the same as tx.gasprice, which is what the bundler pays. + function _postOp( + PostOpMode, + bytes calldata context, + uint256 actualGasCost, + uint256 actualUserOpFeePerGas + ) internal override { + unchecked { + uint256 priceMarkup = tokenPaymasterConfig.priceMarkup; + (uint256 preCharge, address userOpSender) = abi.decode(context, (uint256, address)); + uint256 _cachedPrice = cachedPrice; + // note: as price is in native-asset-per-token and we want more tokens increasing it means dividing it by markup + uint256 cachedPriceWithMarkup = (_cachedPrice * PRICE_DENOMINATOR) / priceMarkup; + // Refund tokens based on actual gas cost + uint256 actualChargeNative = actualGasCost + tokenPaymasterConfig.refundPostopCost * actualUserOpFeePerGas; + uint256 actualTokenNeeded = weiToToken(actualChargeNative, _tokenDecimals, cachedPriceWithMarkup); + + if (preCharge > actualTokenNeeded) { + // If the initially provided token amount is greater than the actual amount needed, refund the difference + SafeERC20.safeTransfer(token, userOpSender, preCharge - actualTokenNeeded); + } else if (preCharge < actualTokenNeeded) { + // Attempt to cover Paymaster's gas expenses by withdrawing the 'overdraft' from the client + // If the transfer reverts also revert the 'postOp' to remove the incentive to cheat + SafeERC20.safeTransferFrom(token, userOpSender, address(this), actualTokenNeeded - preCharge); + } + + emit UserOperationSponsored(userOpSender, actualTokenNeeded, actualGasCost, cachedPriceWithMarkup); + refillEntryPointDeposit(_cachedPrice); + } + } + + /// @notice If necessary this function uses this Paymaster's token balance to refill the deposit on EntryPoint + /// @param _cachedPrice the token price that will be used to calculate the swap amount. + function refillEntryPointDeposit(uint256 _cachedPrice) private { + uint256 currentEntryPointBalance = entryPoint.balanceOf(address(this)); + if (currentEntryPointBalance < tokenPaymasterConfig.minEntryPointBalance) { + uint256 swappedWeth = _maybeSwapTokenToWeth(token, _cachedPrice); + unwrapWeth(swappedWeth); + entryPoint.depositTo{ value: address(this).balance }(address(this)); + } + } + + receive() external payable { + emit Received(msg.sender, msg.value); + } + + function withdrawEth(address payable recipient, uint256 amount) external onlyOwner { + (bool success, ) = recipient.call{ value: amount }(""); + require(success, "withdraw failed"); + } +} diff --git a/contracts/prebuilts/account/utils/AccountCore.sol b/contracts/prebuilts/account/utils/AccountCore.sol new file mode 100644 index 000000000..32c480ef9 --- /dev/null +++ b/contracts/prebuilts/account/utils/AccountCore.sol @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable no-inline-assembly */ +/* solhint-disable reason-string */ + +// Base +import "./BaseAccount.sol"; + +// Fixed Extensions +import "../../../extension/Multicall.sol"; +import "../../../extension/upgradeable/Initializable.sol"; +import "../../../extension/upgradeable/AccountPermissions.sol"; + +// Utils +import "./Helpers.sol"; +import "./AccountCoreStorage.sol"; +import "./BaseAccountFactory.sol"; +import { AccountExtension } from "./AccountExtension.sol"; +import "../../../external-deps/openzeppelin/utils/cryptography/ECDSA.sol"; + +import "../interfaces/IAccountCore.sol"; + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +contract AccountCore is IAccountCore, Initializable, Multicall, BaseAccount, AccountPermissions { + using ECDSA for bytes32; + using EnumerableSet for EnumerableSet.AddressSet; + + /*/////////////////////////////////////////////////////////////// + State + //////////////////////////////////////////////////////////////*/ + + /// @notice EIP 4337 factory for this contract. + address public immutable factory; + + /// @notice EIP 4337 Entrypoint contract. + IEntryPoint private immutable entrypointContract; + + /*/////////////////////////////////////////////////////////////// + Constructor, Initializer, Modifiers + //////////////////////////////////////////////////////////////*/ + + constructor(IEntryPoint _entrypoint, address _factory) EIP712("Account", "1") { + _disableInitializers(); + factory = _factory; + entrypointContract = _entrypoint; + } + + /// @notice Initializes the smart contract wallet. + function initialize(address _defaultAdmin, bytes calldata _data) public virtual initializer { + // This is passed as data in the `_registerOnFactory()` call in `AccountExtension` / `Account`. + AccountCoreStorage.data().creationSalt = _generateSalt(_defaultAdmin, _data); + _setAdmin(_defaultAdmin, true); + } + + /*/////////////////////////////////////////////////////////////// + View functions + //////////////////////////////////////////////////////////////*/ + + /// @notice Returns the EIP 4337 entrypoint contract. + function entryPoint() public view virtual override returns (IEntryPoint) { + address entrypointOverride = AccountCoreStorage.data().entrypointOverride; + if (address(entrypointOverride) != address(0)) { + return IEntryPoint(entrypointOverride); + } + return entrypointContract; + } + + /** + @notice Returns whether a signer is authorized to perform transactions using the account. + Validity of the signature is based upon signer permission start/end timestamps, txn target, and txn value. + Account admins will always return true, and signers with address(0) as the only approved target will skip target checks. + + @param _signer The signer to check. + @param _userOp The user operation to check. + + @return Whether the signer is authorized to perform the transaction. + */ + + /* solhint-disable*/ + function isValidSigner(address _signer, PackedUserOperation calldata _userOp) public view virtual returns (bool) { + // First, check if the signer is an admin. + if (_accountPermissionsStorage().isAdmin[_signer]) { + return true; + } + + SignerPermissionsStatic memory permissions = _accountPermissionsStorage().signerPermissions[_signer]; + EnumerableSet.AddressSet storage approvedTargets = _accountPermissionsStorage().approvedTargets[_signer]; + + // If not an admin, check if the signer is active. + if ( + permissions.startTimestamp > block.timestamp || + block.timestamp >= permissions.endTimestamp || + approvedTargets.length() == 0 + ) { + // Account: no active permissions. + return false; + } + + // Extract the function signature from the userOp calldata and check whether the signer is attempting to call `execute` or `executeBatch`. + bytes4 sig = getFunctionSignature(_userOp.callData); + + // if address(0) is the only approved target, set isWildCard to true (wildcard approved). + bool isWildCard = approvedTargets.length() == 1 && approvedTargets.at(0) == address(0); + + // checking target and value for `execute` + if (sig == AccountExtension.execute.selector) { + // Extract the `target` and `value` arguments from the calldata for `execute`. + (address target, uint256 value) = decodeExecuteCalldata(_userOp.callData); + + // if wildcard target is not approved, check that the target is in the approvedTargets set. + if (!isWildCard) { + // Check if the target is approved. + if (!approvedTargets.contains(target)) { + // Account: target not approved. + return false; + } + } + + // Check if the value is within the allowed range. + if (permissions.nativeTokenLimitPerTransaction < value) { + // Account: value too high OR Account: target not approved. + return false; + } + } + // checking target and value for `executeBatch` + else if (sig == AccountExtension.executeBatch.selector) { + // Extract the `target` and `value` array arguments from the calldata for `executeBatch`. + (address[] memory targets, uint256[] memory values, ) = decodeExecuteBatchCalldata(_userOp.callData); + + // if wildcard target is not approved, check that the targets are in the approvedTargets set. + if (!isWildCard) { + for (uint256 i = 0; i < targets.length; i++) { + if (!approvedTargets.contains(targets[i])) { + // If any target is not approved, break the loop. + return false; + } + } + } + + // For each target+value pair, check if the value is within the allowed range. + for (uint256 i = 0; i < targets.length; i++) { + if (permissions.nativeTokenLimitPerTransaction < values[i]) { + // Account: value too high OR Account: target not approved. + return false; + } + } + } else { + // Account: calling invalid fn. + return false; + } + + return true; + } + + /* solhint-enable */ + + /*/////////////////////////////////////////////////////////////// + External functions + //////////////////////////////////////////////////////////////*/ + + /// @notice Overrides the Entrypoint contract being used. + function setEntrypointOverride(IEntryPoint _entrypointOverride) public virtual { + _onlyAdmin(); + AccountCoreStorage.data().entrypointOverride = address(_entrypointOverride); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the salt used when deploying an Account. + function _generateSalt(address _admin, bytes memory _data) internal view virtual returns (bytes32) { + return keccak256(abi.encode(_admin, _data)); + } + + function getFunctionSignature(bytes calldata data) internal pure returns (bytes4 functionSelector) { + require(data.length >= 4, "!Data"); + return bytes4(data[:4]); + } + + function decodeExecuteCalldata(bytes calldata data) internal pure returns (address _target, uint256 _value) { + require(data.length >= 4 + 32 + 32, "!Data"); + + // Decode the address, which is bytes 4 to 35 + _target = abi.decode(data[4:36], (address)); + + // Decode the value, which is bytes 36 to 68 + _value = abi.decode(data[36:68], (uint256)); + } + + function decodeExecuteBatchCalldata( + bytes calldata data + ) internal pure returns (address[] memory _targets, uint256[] memory _values, bytes[] memory _callData) { + require(data.length >= 4 + 32 + 32 + 32, "!Data"); + + (_targets, _values, _callData) = abi.decode(data[4:], (address[], uint256[], bytes[])); + } + + /// @notice Validates the signature of a user operation. + function _validateSignature( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal virtual override returns (uint256 validationData) { + bytes32 hash = userOpHash.toEthSignedMessageHash(); + address signer = hash.recover(userOp.signature); + + if (!isValidSigner(signer, userOp)) return SIG_VALIDATION_FAILED; + + SignerPermissionsStatic memory permissions = _accountPermissionsStorage().signerPermissions[signer]; + + uint48 validAfter = uint48(permissions.startTimestamp); + uint48 validUntil = uint48(permissions.endTimestamp); + + return _packValidationData(ValidationData(address(0), validAfter, validUntil)); + } + + /// @notice Makes the given account an admin. + function _setAdmin(address _account, bool _isAdmin) internal virtual override { + super._setAdmin(_account, _isAdmin); + if (factory.code.length > 0) { + if (_isAdmin) { + BaseAccountFactory(factory).onSignerAdded(_account, AccountCoreStorage.data().creationSalt); + } else { + BaseAccountFactory(factory).onSignerRemoved(_account, AccountCoreStorage.data().creationSalt); + } + } + } + + /// @notice Runs after every `changeRole` run. + function _afterSignerPermissionsUpdate(SignerPermissionRequest calldata _req) internal virtual override { + if (factory.code.length > 0) { + BaseAccountFactory(factory).onSignerAdded(_req.signer, AccountCoreStorage.data().creationSalt); + } + } +} diff --git a/contracts/prebuilts/account/utils/AccountCoreStorage.sol b/contracts/prebuilts/account/utils/AccountCoreStorage.sol new file mode 100644 index 000000000..4356ef94a --- /dev/null +++ b/contracts/prebuilts/account/utils/AccountCoreStorage.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +library AccountCoreStorage { + /// @custom:storage-location erc7201:account.core.storage + /// @dev keccak256(abi.encode(uint256(keccak256("account.core.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant ACCOUNT_CORE_STORAGE_POSITION = + 0x036f52c1827dab135f7fd44ca0bddde297e2f659c710e0ec53e975f22b548300; + + struct Data { + address entrypointOverride; + bytes32 creationSalt; + } + + function data() internal pure returns (Data storage acountCoreData) { + bytes32 position = ACCOUNT_CORE_STORAGE_POSITION; + assembly { + acountCoreData.slot := position + } + } +} diff --git a/contracts/prebuilts/account/utils/AccountExtension.sol b/contracts/prebuilts/account/utils/AccountExtension.sol new file mode 100644 index 000000000..b2ceca2b7 --- /dev/null +++ b/contracts/prebuilts/account/utils/AccountExtension.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable no-inline-assembly */ +/* solhint-disable reason-string */ + +// Extensions +import "../../../extension/upgradeable/AccountPermissions.sol"; +import "../../../extension/upgradeable/ContractMetadata.sol"; +import "../../../external-deps/openzeppelin/token/ERC721/utils/ERC721Holder.sol"; +import "../../../external-deps/openzeppelin/token/ERC1155/utils/ERC1155Holder.sol"; + +// Utils +import "../../../eip/ERC1271.sol"; +import "../../../external-deps/openzeppelin/utils/cryptography/ECDSA.sol"; +import "../../../external-deps/openzeppelin/utils/structs/EnumerableSet.sol"; +import "./BaseAccountFactory.sol"; +import "./AccountCore.sol"; +import "./AccountCoreStorage.sol"; + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +contract AccountExtension is ContractMetadata, ERC1271, AccountPermissions, ERC721Holder, ERC1155Holder { + using ECDSA for bytes32; + using EnumerableSet for EnumerableSet.AddressSet; + + bytes32 private constant MSG_TYPEHASH = keccak256("AccountMessage(bytes message)"); + + /*/////////////////////////////////////////////////////////////// + Constructor, Initializer, Modifiers + //////////////////////////////////////////////////////////////*/ + + /// @notice Checks whether the caller is the EntryPoint contract or the admin. + modifier onlyAdminOrEntrypoint() virtual { + require( + msg.sender == address(AccountCore(payable(address(this))).entryPoint()) || isAdmin(msg.sender), + "Account: not admin or EntryPoint." + ); + _; + } + + // solhint-disable-next-line no-empty-blocks + receive() external payable virtual {} + + constructor() EIP712("Account", "1") {} + + /*/////////////////////////////////////////////////////////////// + View functions + //////////////////////////////////////////////////////////////*/ + + /// @notice See {IERC165-supportsInterface}. + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC1155Receiver) returns (bool) { + return + interfaceId == type(IERC1155Receiver).interfaceId || + interfaceId == type(IERC721Receiver).interfaceId || + super.supportsInterface(interfaceId); + } + + /** + * @notice See EIP-1271 + * + * @param _hash The original message hash of the data to sign (before mixing this contract's domain separator) + * @param _signature The signature produced on signing the typed data hash (result of `getMessageHash(abi.encode(rawData))`) + */ + function isValidSignature( + bytes32 _hash, + bytes memory _signature + ) public view virtual override returns (bytes4 magicValue) { + bytes32 targetDigest = getMessageHash(_hash); + address signer = targetDigest.recover(_signature); + + if (isAdmin(signer)) { + return MAGICVALUE; + } + + address caller = msg.sender; + EnumerableSet.AddressSet storage approvedTargets = _accountPermissionsStorage().approvedTargets[signer]; + + require( + approvedTargets.contains(caller) || (approvedTargets.length() == 1 && approvedTargets.at(0) == address(0)), + "Account: caller not approved target." + ); + + if (isActiveSigner(signer)) { + magicValue = MAGICVALUE; + } + } + + /** + * @notice Returns the hash of message that should be signed for EIP1271 verification. + * @param _hash The message hash to sign for the EIP-1271 origin verifying contract. + * @return messageHash The digest to sign for EIP-1271 verification. + */ + function getMessageHash(bytes32 _hash) public view returns (bytes32) { + bytes32 messageHash = keccak256(abi.encode(_hash)); + bytes32 typedDataHash = keccak256(abi.encode(MSG_TYPEHASH, messageHash)); + return keccak256(abi.encodePacked("\x19\x01", _domainSeparatorV4(), typedDataHash)); + } + + /*/////////////////////////////////////////////////////////////// + External functions + //////////////////////////////////////////////////////////////*/ + + /// @notice Executes a transaction (called directly from an admin, or by entryPoint) + function execute(address _target, uint256 _value, bytes calldata _calldata) external virtual onlyAdminOrEntrypoint { + _registerOnFactory(); + _call(_target, _value, _calldata); + } + + /// @notice Executes a sequence transaction (called directly from an admin, or by entryPoint) + function executeBatch( + address[] calldata _target, + uint256[] calldata _value, + bytes[] calldata _calldata + ) external virtual onlyAdminOrEntrypoint { + _registerOnFactory(); + require(_target.length == _calldata.length && _target.length == _value.length, "Account: wrong array lengths."); + for (uint256 i = 0; i < _target.length; i++) { + _call(_target[i], _value[i], _calldata[i]); + } + } + + /// @notice Deposit funds for this account in Entrypoint. + function addDeposit() public payable { + AccountCore(payable(address(this))).entryPoint().depositTo{ value: msg.value }(address(this)); + } + + /// @notice Withdraw funds for this account from Entrypoint. + function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public { + _onlyAdmin(); + AccountCore(payable(address(this))).entryPoint().withdrawTo(withdrawAddress, amount); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Registers the account on the factory if it hasn't been registered yet. + function _registerOnFactory() internal virtual { + address factory = AccountCore(payable(address(this))).factory(); + BaseAccountFactory factoryContract = BaseAccountFactory(factory); + if (!factoryContract.isRegistered(address(this))) { + factoryContract.onRegister(AccountCoreStorage.data().creationSalt); + } + } + + /// @dev Calls a target contract and reverts if it fails. + function _call(address _target, uint256 value, bytes memory _calldata) internal returns (bytes memory result) { + bool success; + (success, result) = _target.call{ value: value }(_calldata); + if (!success) { + assembly { + revert(add(result, 32), mload(result)) + } + } + } + + /// @dev Returns whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view virtual override returns (bool) { + return isAdmin(msg.sender) || msg.sender == address(this); + } + + function _afterSignerPermissionsUpdate(SignerPermissionRequest calldata _req) internal virtual override {} +} diff --git a/contracts/prebuilts/account/utils/AccountSeaportBulkSigSupport.sol b/contracts/prebuilts/account/utils/AccountSeaportBulkSigSupport.sol new file mode 100644 index 000000000..e5a1dcb7a --- /dev/null +++ b/contracts/prebuilts/account/utils/AccountSeaportBulkSigSupport.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import { SeaportEIP1271 } from "../../../extension/SeaportEIP1271.sol"; +import { AccountPermissions, AccountPermissionsStorage } from "../../../extension/upgradeable/AccountPermissions.sol"; +import { EnumerableSet } from "../../../external-deps/openzeppelin/utils/structs/EnumerableSet.sol"; + +contract AccountSeaportBulkSigSupport is SeaportEIP1271 { + using EnumerableSet for EnumerableSet.AddressSet; + + constructor() SeaportEIP1271("Account", "1") {} + + /// @notice Returns whether a given signer is an authorized signer for the contract. + function _isAuthorizedSigner(address _signer) internal view virtual override returns (bool authorized) { + // is signer an admin? + if (AccountPermissionsStorage.data().isAdmin[_signer]) { + authorized = true; + } + + address caller = msg.sender; + EnumerableSet.AddressSet storage approvedTargets = AccountPermissionsStorage.data().approvedTargets[_signer]; + + require( + approvedTargets.contains(caller) || (approvedTargets.length() == 1 && approvedTargets.at(0) == address(0)), + "Account: caller not approved target." + ); + + // is signer an active signer of account? + AccountPermissions.SignerPermissionsStatic memory permissions = AccountPermissionsStorage + .data() + .signerPermissions[_signer]; + if ( + permissions.startTimestamp <= block.timestamp && + block.timestamp < permissions.endTimestamp && + AccountPermissionsStorage.data().approvedTargets[_signer].length() > 0 + ) { + authorized = true; + } + } +} diff --git a/contracts/prebuilts/account/utils/BaseAccount.sol b/contracts/prebuilts/account/utils/BaseAccount.sol new file mode 100644 index 000000000..d1b7a48dd --- /dev/null +++ b/contracts/prebuilts/account/utils/BaseAccount.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable no-empty-blocks */ + +import "../interfaces/IAccount.sol"; +import "../interfaces/IEntryPoint.sol"; +import "./UserOperationLib.sol"; + +/** + * Basic account implementation. + * This contract provides the basic logic for implementing the IAccount interface - validateUserOp + * Specific account implementation should inherit it and provide the account-specific logic. + */ +abstract contract BaseAccount is IAccount { + using UserOperationLib for PackedUserOperation; + + /** + * Return the account nonce. + * This method returns the next sequential nonce. + * For a nonce of a specific key, use `entrypoint.getNonce(account, key)` + */ + function getNonce() public view virtual returns (uint256) { + return entryPoint().getNonce(address(this), 0); + } + + /** + * Return the entryPoint used by this account. + * Subclass should return the current entryPoint used by this account. + */ + function entryPoint() public view virtual returns (IEntryPoint); + + /// @inheritdoc IAccount + function validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds + ) external virtual override returns (uint256 validationData) { + _requireFromEntryPoint(); + validationData = _validateSignature(userOp, userOpHash); + _validateNonce(userOp.nonce); + _payPrefund(missingAccountFunds); + } + + /** + * Ensure the request comes from the known entrypoint. + */ + function _requireFromEntryPoint() internal view virtual { + require(msg.sender == address(entryPoint()), "account: not from EntryPoint"); + } + + /** + * Validate the signature is valid for this message. + * @param userOp - Validate the userOp.signature field. + * @param userOpHash - Convenient field: the hash of the request, to check the signature against. + * (also hashes the entrypoint and chain id) + * @return validationData - Signature and time-range of this operation. + * <20-byte> aggregatorOrSigFail - 0 for valid signature, 1 to mark signature failure, + * otherwise, an address of an aggregator contract. + * <6-byte> validUntil - last timestamp this operation is valid. 0 for "indefinite" + * <6-byte> validAfter - first timestamp this operation is valid + * If the account doesn't use time-range, it is enough to return + * SIG_VALIDATION_FAILED value (1) for signature failure. + * Note that the validation code cannot use block.timestamp (or block.number) directly. + */ + function _validateSignature( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal virtual returns (uint256 validationData); + + /** + * Validate the nonce of the UserOperation. + * This method may validate the nonce requirement of this account. + * e.g. + * To limit the nonce to use sequenced UserOps only (no "out of order" UserOps): + * `require(nonce < type(uint64).max)` + * For a hypothetical account that *requires* the nonce to be out-of-order: + * `require(nonce & type(uint64).max == 0)` + * + * The actual nonce uniqueness is managed by the EntryPoint, and thus no other + * action is needed by the account itself. + * + * @param nonce to validate + * + * solhint-disable-next-line no-empty-blocks + */ + function _validateNonce(uint256 nonce) internal view virtual {} + + /** + * Sends to the entrypoint (msg.sender) the missing funds for this transaction. + * SubClass MAY override this method for better funds management + * (e.g. send to the entryPoint more than the minimum required, so that in future transactions + * it will not be required to send again). + * @param missingAccountFunds - The minimum value this method should send the entrypoint. + * This value MAY be zero, in case there is enough deposit, + * or the userOp has a paymaster. + */ + function _payPrefund(uint256 missingAccountFunds) internal virtual { + if (missingAccountFunds != 0) { + (bool success, ) = payable(msg.sender).call{ value: missingAccountFunds, gas: type(uint256).max }(""); + (success); + //ignore failure (its EntryPoint's job to verify, not account.) + } + } +} diff --git a/contracts/prebuilts/account/utils/BaseAccountFactory.sol b/contracts/prebuilts/account/utils/BaseAccountFactory.sol new file mode 100644 index 000000000..5e30cb5c1 --- /dev/null +++ b/contracts/prebuilts/account/utils/BaseAccountFactory.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +// Utils +import "../../../extension/Multicall.sol"; +import "../../../external-deps/openzeppelin/proxy/Clones.sol"; +import "../../../external-deps/openzeppelin/utils/structs/EnumerableSet.sol"; +import "./BaseAccount.sol"; +import "../../../extension/interface/IAccountPermissions.sol"; +import "../../../lib/BytesLib.sol"; + +// Interface +import "../interfaces/IAccountFactory.sol"; + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +abstract contract BaseAccountFactory is IAccountFactory, Multicall { + using EnumerableSet for EnumerableSet.AddressSet; + + /*/////////////////////////////////////////////////////////////// + State + //////////////////////////////////////////////////////////////*/ + + address public immutable accountImplementation; + address public immutable entrypoint; + + EnumerableSet.AddressSet private allAccounts; + mapping(address => EnumerableSet.AddressSet) internal accountsOfSigner; + + /*/////////////////////////////////////////////////////////////// + Constructor + //////////////////////////////////////////////////////////////*/ + + constructor(address _accountImpl, address _entrypoint) { + accountImplementation = _accountImpl; + entrypoint = _entrypoint; + } + + /*/////////////////////////////////////////////////////////////// + External functions + //////////////////////////////////////////////////////////////*/ + + /// @notice Deploys a new Account for admin. + function createAccount(address _admin, bytes calldata _data) external virtual override returns (address) { + address impl = accountImplementation; + bytes32 salt = _generateSalt(_admin, _data); + address account = Clones.predictDeterministicAddress(impl, salt); + + if (account.code.length > 0) { + return account; + } + + account = Clones.cloneDeterministic(impl, salt); + + if (msg.sender != entrypoint) { + require(allAccounts.add(account), "AccountFactory: account already registered"); + } + + _initializeAccount(account, _admin, _data); + + emit AccountCreated(account, _admin); + + return account; + } + + /// @notice Callback function for an Account to register itself on the factory. + function onRegister(bytes32 _salt) external { + address account = msg.sender; + require(_isAccountOfFactory(account, _salt), "AccountFactory: not an account."); + + require(allAccounts.add(account), "AccountFactory: account already registered"); + } + + function onSignerAdded(address _signer, bytes32 _salt) external { + address account = msg.sender; + require(_isAccountOfFactory(account, _salt), "AccountFactory: not an account."); + + bool isNewSigner = accountsOfSigner[_signer].add(account); + + if (isNewSigner) { + emit SignerAdded(account, _signer); + } + } + + /// @notice Callback function for an Account to un-register its signers. + function onSignerRemoved(address _signer, bytes32 _salt) external { + address account = msg.sender; + require(_isAccountOfFactory(account, _salt), "AccountFactory: not an account."); + + bool isAccount = accountsOfSigner[_signer].remove(account); + + if (isAccount) { + emit SignerRemoved(account, _signer); + } + } + + /*/////////////////////////////////////////////////////////////// + View functions + //////////////////////////////////////////////////////////////*/ + + /// @notice Returns whether an account is registered on this factory. + function isRegistered(address _account) external view returns (bool) { + return allAccounts.contains(_account); + } + + /// @notice Returns the total number of accounts. + function totalAccounts() external view returns (uint256) { + return allAccounts.length(); + } + + /// @notice Returns all accounts between the given indices. + function getAccounts(uint256 _start, uint256 _end) external view returns (address[] memory accounts) { + require(_start < _end && _end <= allAccounts.length(), "BaseAccountFactory: invalid indices"); + + uint256 len = _end - _start; + accounts = new address[](_end - _start); + + for (uint256 i = 0; i < len; i += 1) { + accounts[i] = allAccounts.at(i + _start); + } + } + + /// @notice Returns all accounts created on the factory. + function getAllAccounts() external view returns (address[] memory) { + return allAccounts.values(); + } + + /// @notice Returns the address of an Account that would be deployed with the given admin signer. + function getAddress(address _adminSigner, bytes calldata _data) public view returns (address) { + bytes32 salt = _generateSalt(_adminSigner, _data); + return Clones.predictDeterministicAddress(accountImplementation, salt); + } + + /// @notice Returns all accounts that the given address is a signer of. + function getAccountsOfSigner(address signer) external view returns (address[] memory accounts) { + return accountsOfSigner[signer].values(); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns whether the caller is an account deployed by this factory. + function _isAccountOfFactory(address _account, bytes32 _salt) internal view virtual returns (bool) { + address predicted = Clones.predictDeterministicAddress(accountImplementation, _salt); + return _account == predicted; + } + + function _getImplementation(address cloneAddress) internal view returns (address) { + bytes memory code = cloneAddress.code; + return BytesLib.toAddress(code, 10); + } + + /// @dev Returns the salt used when deploying an Account. + function _generateSalt(address _admin, bytes memory _data) internal view virtual returns (bytes32) { + return keccak256(abi.encode(_admin, _data)); + } + + /// @dev Called in `createAccount`. Initializes the account contract created in `createAccount`. + function _initializeAccount(address _account, address _admin, bytes calldata _data) internal virtual; +} diff --git a/contracts/prebuilts/account/utils/EntryPoint.sol b/contracts/prebuilts/account/utils/EntryPoint.sol new file mode 100644 index 000000000..253890734 --- /dev/null +++ b/contracts/prebuilts/account/utils/EntryPoint.sol @@ -0,0 +1,725 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable no-inline-assembly */ + +import "../interfaces/IAccount.sol"; +import "../interfaces/IAccountExecute.sol"; +import "../interfaces/IPaymaster.sol"; +import "../interfaces/IEntryPoint.sol"; + +import "../utils/Exec.sol"; +import "./StakeManager.sol"; +import "./SenderCreator.sol"; +import "./Helpers.sol"; +import "./NonceManager.sol"; +import "./UserOperationLib.sol"; + +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; + +/* + * Account-Abstraction (EIP-4337) singleton EntryPoint implementation. + * Only one instance required on each chain. + */ + +/// @custom:security-contact https://bounty.ethereum.org +contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, ERC165 { + using UserOperationLib for PackedUserOperation; + + SenderCreator private immutable _senderCreator = new SenderCreator(); + + function senderCreator() internal view virtual returns (SenderCreator) { + return _senderCreator; + } + + //compensate for innerHandleOps' emit message and deposit refund. + // allow some slack for future gas price changes. + uint256 private constant INNER_GAS_OVERHEAD = 10000; + + // Marker for inner call revert on out of gas + bytes32 private constant INNER_OUT_OF_GAS = hex"deaddead"; + bytes32 private constant INNER_REVERT_LOW_PREFUND = hex"deadaa51"; + + uint256 private constant REVERT_REASON_MAX_LEN = 2048; + uint256 private constant PENALTY_PERCENT = 10; + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + // note: solidity "type(IEntryPoint).interfaceId" is without inherited methods but we want to check everything + return + interfaceId == + (type(IEntryPoint).interfaceId ^ type(IStakeManager).interfaceId ^ type(INonceManager).interfaceId) || + interfaceId == type(IEntryPoint).interfaceId || + interfaceId == type(IStakeManager).interfaceId || + interfaceId == type(INonceManager).interfaceId || + super.supportsInterface(interfaceId); + } + + /** + * Compensate the caller's beneficiary address with the collected fees of all UserOperations. + * @param beneficiary - The address to receive the fees. + * @param amount - Amount to transfer. + */ + function _compensate(address payable beneficiary, uint256 amount) internal { + require(beneficiary != address(0), "AA90 invalid beneficiary"); + (bool success, ) = beneficiary.call{ value: amount }(""); + require(success, "AA91 failed send to beneficiary"); + } + + /** + * Execute a user operation. + * @param opIndex - Index into the opInfo array. + * @param userOp - The userOp to execute. + * @param opInfo - The opInfo filled by validatePrepayment for this userOp. + * @return collected - The total amount this userOp paid. + */ + function _executeUserOp( + uint256 opIndex, + PackedUserOperation calldata userOp, + UserOpInfo memory opInfo + ) internal returns (uint256 collected) { + uint256 preGas = gasleft(); + bytes memory context = getMemoryBytesFromOffset(opInfo.contextOffset); + bool success; + { + uint256 saveFreePtr; + assembly ("memory-safe") { + saveFreePtr := mload(0x40) + } + bytes calldata callData = userOp.callData; + bytes memory innerCall; + bytes4 methodSig; + assembly { + let len := callData.length + if gt(len, 3) { + methodSig := calldataload(callData.offset) + } + } + if (methodSig == IAccountExecute.executeUserOp.selector) { + bytes memory executeUserOp = abi.encodeCall(IAccountExecute.executeUserOp, (userOp, opInfo.userOpHash)); + innerCall = abi.encodeCall(this.innerHandleOp, (executeUserOp, opInfo, context)); + } else { + innerCall = abi.encodeCall(this.innerHandleOp, (callData, opInfo, context)); + } + assembly ("memory-safe") { + success := call(gas(), address(), 0, add(innerCall, 0x20), mload(innerCall), 0, 32) + collected := mload(0) + mstore(0x40, saveFreePtr) + } + } + if (!success) { + bytes32 innerRevertCode; + assembly ("memory-safe") { + let len := returndatasize() + if eq(32, len) { + returndatacopy(0, 0, 32) + innerRevertCode := mload(0) + } + } + if (innerRevertCode == INNER_OUT_OF_GAS) { + // handleOps was called with gas limit too low. abort entire bundle. + //can only be caused by bundler (leaving not enough gas for inner call) + revert FailedOp(opIndex, "AA95 out of gas"); + } else if (innerRevertCode == INNER_REVERT_LOW_PREFUND) { + // innerCall reverted on prefund too low. treat entire prefund as "gas cost" + uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; + uint256 actualGasCost = opInfo.prefund; + emitPrefundTooLow(opInfo); + emitUserOperationEvent(opInfo, false, actualGasCost, actualGas); + collected = actualGasCost; + } else { + emit PostOpRevertReason( + opInfo.userOpHash, + opInfo.mUserOp.sender, + opInfo.mUserOp.nonce, + Exec.getReturnData(REVERT_REASON_MAX_LEN) + ); + + uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; + collected = _postExecution(IPaymaster.PostOpMode.postOpReverted, opInfo, context, actualGas); + } + } + } + + function emitUserOperationEvent( + UserOpInfo memory opInfo, + bool success, + uint256 actualGasCost, + uint256 actualGas + ) internal virtual { + emit UserOperationEvent( + opInfo.userOpHash, + opInfo.mUserOp.sender, + opInfo.mUserOp.paymaster, + opInfo.mUserOp.nonce, + success, + actualGasCost, + actualGas + ); + } + + function emitPrefundTooLow(UserOpInfo memory opInfo) internal virtual { + emit UserOperationPrefundTooLow(opInfo.userOpHash, opInfo.mUserOp.sender, opInfo.mUserOp.nonce); + } + + /// @inheritdoc IEntryPoint + function handleOps(PackedUserOperation[] calldata ops, address payable beneficiary) public nonReentrant { + uint256 opslen = ops.length; + UserOpInfo[] memory opInfos = new UserOpInfo[](opslen); + + unchecked { + for (uint256 i = 0; i < opslen; i++) { + UserOpInfo memory opInfo = opInfos[i]; + (uint256 validationData, uint256 pmValidationData) = _validatePrepayment(i, ops[i], opInfo); + _validateAccountAndPaymasterValidationData(i, validationData, pmValidationData, address(0)); + } + + uint256 collected = 0; + emit BeforeExecution(); + + for (uint256 i = 0; i < opslen; i++) { + collected += _executeUserOp(i, ops[i], opInfos[i]); + } + + _compensate(beneficiary, collected); + } + } + + /// @inheritdoc IEntryPoint + function handleAggregatedOps( + UserOpsPerAggregator[] calldata opsPerAggregator, + address payable beneficiary + ) public nonReentrant { + uint256 opasLen = opsPerAggregator.length; + uint256 totalOps = 0; + for (uint256 i = 0; i < opasLen; i++) { + UserOpsPerAggregator calldata opa = opsPerAggregator[i]; + PackedUserOperation[] calldata ops = opa.userOps; + IAggregator aggregator = opa.aggregator; + + //address(1) is special marker of "signature error" + require(address(aggregator) != address(1), "AA96 invalid aggregator"); + + if (address(aggregator) != address(0)) { + // solhint-disable-next-line no-empty-blocks + try aggregator.validateSignatures(ops, opa.signature) {} catch { + revert SignatureValidationFailed(address(aggregator)); + } + } + + totalOps += ops.length; + } + + UserOpInfo[] memory opInfos = new UserOpInfo[](totalOps); + + uint256 opIndex = 0; + for (uint256 a = 0; a < opasLen; a++) { + UserOpsPerAggregator calldata opa = opsPerAggregator[a]; + PackedUserOperation[] calldata ops = opa.userOps; + IAggregator aggregator = opa.aggregator; + + uint256 opslen = ops.length; + for (uint256 i = 0; i < opslen; i++) { + UserOpInfo memory opInfo = opInfos[opIndex]; + (uint256 validationData, uint256 paymasterValidationData) = _validatePrepayment( + opIndex, + ops[i], + opInfo + ); + _validateAccountAndPaymasterValidationData( + i, + validationData, + paymasterValidationData, + address(aggregator) + ); + opIndex++; + } + } + + emit BeforeExecution(); + + uint256 collected = 0; + opIndex = 0; + for (uint256 a = 0; a < opasLen; a++) { + UserOpsPerAggregator calldata opa = opsPerAggregator[a]; + emit SignatureAggregatorChanged(address(opa.aggregator)); + PackedUserOperation[] calldata ops = opa.userOps; + uint256 opslen = ops.length; + + for (uint256 i = 0; i < opslen; i++) { + collected += _executeUserOp(opIndex, ops[i], opInfos[opIndex]); + opIndex++; + } + } + emit SignatureAggregatorChanged(address(0)); + + _compensate(beneficiary, collected); + } + + /** + * A memory copy of UserOp static fields only. + * Excluding: callData, initCode and signature. Replacing paymasterAndData with paymaster. + */ + struct MemoryUserOp { + address sender; + uint256 nonce; + uint256 verificationGasLimit; + uint256 callGasLimit; + uint256 paymasterVerificationGasLimit; + uint256 paymasterPostOpGasLimit; + uint256 preVerificationGas; + address paymaster; + uint256 maxFeePerGas; + uint256 maxPriorityFeePerGas; + } + + struct UserOpInfo { + MemoryUserOp mUserOp; + bytes32 userOpHash; + uint256 prefund; + uint256 contextOffset; + uint256 preOpGas; + } + + /** + * Inner function to handle a UserOperation. + * Must be declared "external" to open a call context, but it can only be called by handleOps. + * @param callData - The callData to execute. + * @param opInfo - The UserOpInfo struct. + * @param context - The context bytes. + * @return actualGasCost - the actual cost in eth this UserOperation paid for gas + */ + function innerHandleOp( + bytes memory callData, + UserOpInfo memory opInfo, + bytes calldata context + ) external returns (uint256 actualGasCost) { + uint256 preGas = gasleft(); + require(msg.sender == address(this), "AA92 internal call only"); + MemoryUserOp memory mUserOp = opInfo.mUserOp; + + uint256 callGasLimit = mUserOp.callGasLimit; + unchecked { + // handleOps was called with gas limit too low. abort entire bundle. + if ((gasleft() * 63) / 64 < callGasLimit + mUserOp.paymasterPostOpGasLimit + INNER_GAS_OVERHEAD) { + assembly ("memory-safe") { + mstore(0, INNER_OUT_OF_GAS) + revert(0, 32) + } + } + } + + IPaymaster.PostOpMode mode = IPaymaster.PostOpMode.opSucceeded; + if (callData.length > 0) { + bool success = Exec.call(mUserOp.sender, 0, callData, callGasLimit); + if (!success) { + bytes memory result = Exec.getReturnData(REVERT_REASON_MAX_LEN); + if (result.length > 0) { + emit UserOperationRevertReason(opInfo.userOpHash, mUserOp.sender, mUserOp.nonce, result); + } + mode = IPaymaster.PostOpMode.opReverted; + } + } + + unchecked { + uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; + return _postExecution(mode, opInfo, context, actualGas); + } + } + + /// @inheritdoc IEntryPoint + function getUserOpHash(PackedUserOperation calldata userOp) public view returns (bytes32) { + return keccak256(abi.encode(userOp.hash(), address(this), block.chainid)); + } + + /** + * Copy general fields from userOp into the memory opInfo structure. + * @param userOp - The user operation. + * @param mUserOp - The memory user operation. + */ + function _copyUserOpToMemory(PackedUserOperation calldata userOp, MemoryUserOp memory mUserOp) internal pure { + mUserOp.sender = userOp.sender; + mUserOp.nonce = userOp.nonce; + (mUserOp.verificationGasLimit, mUserOp.callGasLimit) = UserOperationLib.unpackUints(userOp.accountGasLimits); + mUserOp.preVerificationGas = userOp.preVerificationGas; + (mUserOp.maxPriorityFeePerGas, mUserOp.maxFeePerGas) = UserOperationLib.unpackUints(userOp.gasFees); + bytes calldata paymasterAndData = userOp.paymasterAndData; + if (paymasterAndData.length > 0) { + require(paymasterAndData.length >= UserOperationLib.PAYMASTER_DATA_OFFSET, "AA93 invalid paymasterAndData"); + ( + mUserOp.paymaster, + mUserOp.paymasterVerificationGasLimit, + mUserOp.paymasterPostOpGasLimit + ) = UserOperationLib.unpackPaymasterStaticFields(paymasterAndData); + } else { + mUserOp.paymaster = address(0); + mUserOp.paymasterVerificationGasLimit = 0; + mUserOp.paymasterPostOpGasLimit = 0; + } + } + + /** + * Get the required prefunded gas fee amount for an operation. + * @param mUserOp - The user operation in memory. + */ + function _getRequiredPrefund(MemoryUserOp memory mUserOp) internal pure returns (uint256 requiredPrefund) { + unchecked { + uint256 requiredGas = mUserOp.verificationGasLimit + + mUserOp.callGasLimit + + mUserOp.paymasterVerificationGasLimit + + mUserOp.paymasterPostOpGasLimit + + mUserOp.preVerificationGas; + + requiredPrefund = requiredGas * mUserOp.maxFeePerGas; + } + } + + /** + * Create sender smart contract account if init code is provided. + * @param opIndex - The operation index. + * @param opInfo - The operation info. + * @param initCode - The init code for the smart contract account. + */ + function _createSenderIfNeeded(uint256 opIndex, UserOpInfo memory opInfo, bytes calldata initCode) internal { + if (initCode.length != 0) { + address sender = opInfo.mUserOp.sender; + if (sender.code.length != 0) revert FailedOp(opIndex, "AA10 sender already constructed"); + address sender1 = senderCreator().createSender{ gas: opInfo.mUserOp.verificationGasLimit }(initCode); + if (sender1 == address(0)) revert FailedOp(opIndex, "AA13 initCode failed or OOG"); + if (sender1 != sender) revert FailedOp(opIndex, "AA14 initCode must return sender"); + if (sender1.code.length == 0) revert FailedOp(opIndex, "AA15 initCode must create sender"); + address factory = address(bytes20(initCode[0:20])); + emit AccountDeployed(opInfo.userOpHash, sender, factory, opInfo.mUserOp.paymaster); + } + } + + /// @inheritdoc IEntryPoint + function getSenderAddress(bytes calldata initCode) public { + address sender = senderCreator().createSender(initCode); + revert SenderAddressResult(sender); + } + + /** + * Call account.validateUserOp. + * Revert (with FailedOp) in case validateUserOp reverts, or account didn't send required prefund. + * Decrement account's deposit if needed. + * @param opIndex - The operation index. + * @param op - The user operation. + * @param opInfo - The operation info. + * @param requiredPrefund - The required prefund amount. + */ + function _validateAccountPrepayment( + uint256 opIndex, + PackedUserOperation calldata op, + UserOpInfo memory opInfo, + uint256 requiredPrefund, + uint256 verificationGasLimit + ) internal returns (uint256 validationData) { + unchecked { + MemoryUserOp memory mUserOp = opInfo.mUserOp; + address sender = mUserOp.sender; + _createSenderIfNeeded(opIndex, opInfo, op.initCode); + address paymaster = mUserOp.paymaster; + uint256 missingAccountFunds = 0; + if (paymaster == address(0)) { + uint256 bal = balanceOf(sender); + missingAccountFunds = bal > requiredPrefund ? 0 : requiredPrefund - bal; + } + try + IAccount(sender).validateUserOp{ gas: verificationGasLimit }(op, opInfo.userOpHash, missingAccountFunds) + returns (uint256 _validationData) { + validationData = _validationData; + } catch { + revert FailedOpWithRevert(opIndex, "AA23 reverted", Exec.getReturnData(REVERT_REASON_MAX_LEN)); + } + if (paymaster == address(0)) { + DepositInfo storage senderInfo = deposits[sender]; + uint256 deposit = senderInfo.deposit; + if (requiredPrefund > deposit) { + revert FailedOp(opIndex, "AA21 didn't pay prefund"); + } + senderInfo.deposit = deposit - requiredPrefund; + } + } + } + + /** + * In case the request has a paymaster: + * - Validate paymaster has enough deposit. + * - Call paymaster.validatePaymasterUserOp. + * - Revert with proper FailedOp in case paymaster reverts. + * - Decrement paymaster's deposit. + * @param opIndex - The operation index. + * @param op - The user operation. + * @param opInfo - The operation info. + * @param requiredPreFund - The required prefund amount. + */ + function _validatePaymasterPrepayment( + uint256 opIndex, + PackedUserOperation calldata op, + UserOpInfo memory opInfo, + uint256 requiredPreFund + ) internal returns (bytes memory context, uint256 validationData) { + unchecked { + uint256 preGas = gasleft(); + MemoryUserOp memory mUserOp = opInfo.mUserOp; + address paymaster = mUserOp.paymaster; + DepositInfo storage paymasterInfo = deposits[paymaster]; + uint256 deposit = paymasterInfo.deposit; + if (deposit < requiredPreFund) { + revert FailedOp(opIndex, "AA31 paymaster deposit too low"); + } + paymasterInfo.deposit = deposit - requiredPreFund; + uint256 pmVerificationGasLimit = mUserOp.paymasterVerificationGasLimit; + try + IPaymaster(paymaster).validatePaymasterUserOp{ gas: pmVerificationGasLimit }( + op, + opInfo.userOpHash, + requiredPreFund + ) + returns (bytes memory _context, uint256 _validationData) { + context = _context; + validationData = _validationData; + } catch { + revert FailedOpWithRevert(opIndex, "AA33 reverted", Exec.getReturnData(REVERT_REASON_MAX_LEN)); + } + if (preGas - gasleft() > pmVerificationGasLimit) { + revert FailedOp(opIndex, "AA36 over paymasterVerificationGasLimit"); + } + } + } + + /** + * Revert if either account validationData or paymaster validationData is expired. + * @param opIndex - The operation index. + * @param validationData - The account validationData. + * @param paymasterValidationData - The paymaster validationData. + * @param expectedAggregator - The expected aggregator. + */ + function _validateAccountAndPaymasterValidationData( + uint256 opIndex, + uint256 validationData, + uint256 paymasterValidationData, + address expectedAggregator + ) internal view { + (address aggregator, bool outOfTimeRange) = _getValidationData(validationData); + if (expectedAggregator != aggregator) { + revert FailedOp(opIndex, "AA24 signature error"); + } + if (outOfTimeRange) { + revert FailedOp(opIndex, "AA22 expired or not due"); + } + // pmAggregator is not a real signature aggregator: we don't have logic to handle it as address. + // Non-zero address means that the paymaster fails due to some signature check (which is ok only during estimation). + address pmAggregator; + (pmAggregator, outOfTimeRange) = _getValidationData(paymasterValidationData); + if (pmAggregator != address(0)) { + revert FailedOp(opIndex, "AA34 signature error"); + } + if (outOfTimeRange) { + revert FailedOp(opIndex, "AA32 paymaster expired or not due"); + } + } + + /** + * Parse validationData into its components. + * @param validationData - The packed validation data (sigFailed, validAfter, validUntil). + * @return aggregator the aggregator of the validationData + * @return outOfTimeRange true if current time is outside the time range of this validationData. + */ + function _getValidationData( + uint256 validationData + ) internal view returns (address aggregator, bool outOfTimeRange) { + if (validationData == 0) { + return (address(0), false); + } + ValidationData memory data = _parseValidationData(validationData); + // solhint-disable-next-line not-rely-on-time + outOfTimeRange = block.timestamp > data.validUntil || block.timestamp < data.validAfter; + aggregator = data.aggregator; + } + + /** + * Validate account and paymaster (if defined) and + * also make sure total validation doesn't exceed verificationGasLimit. + * This method is called off-chain (simulateValidation()) and on-chain (from handleOps) + * @param opIndex - The index of this userOp into the "opInfos" array. + * @param userOp - The userOp to validate. + */ + function _validatePrepayment( + uint256 opIndex, + PackedUserOperation calldata userOp, + UserOpInfo memory outOpInfo + ) internal returns (uint256 validationData, uint256 paymasterValidationData) { + uint256 preGas = gasleft(); + MemoryUserOp memory mUserOp = outOpInfo.mUserOp; + _copyUserOpToMemory(userOp, mUserOp); + outOpInfo.userOpHash = getUserOpHash(userOp); + + // Validate all numeric values in userOp are well below 128 bit, so they can safely be added + // and multiplied without causing overflow. + uint256 verificationGasLimit = mUserOp.verificationGasLimit; + uint256 maxGasValues = mUserOp.preVerificationGas | + verificationGasLimit | + mUserOp.callGasLimit | + mUserOp.paymasterVerificationGasLimit | + mUserOp.paymasterPostOpGasLimit | + mUserOp.maxFeePerGas | + mUserOp.maxPriorityFeePerGas; + require(maxGasValues <= type(uint120).max, "AA94 gas values overflow"); + + uint256 requiredPreFund = _getRequiredPrefund(mUserOp); + validationData = _validateAccountPrepayment(opIndex, userOp, outOpInfo, requiredPreFund, verificationGasLimit); + + if (!_validateAndUpdateNonce(mUserOp.sender, mUserOp.nonce)) { + revert FailedOp(opIndex, "AA25 invalid account nonce"); + } + + unchecked { + if (preGas - gasleft() > verificationGasLimit) { + revert FailedOp(opIndex, "AA26 over verificationGasLimit"); + } + } + + bytes memory context; + if (mUserOp.paymaster != address(0)) { + (context, paymasterValidationData) = _validatePaymasterPrepayment( + opIndex, + userOp, + outOpInfo, + requiredPreFund + ); + } + unchecked { + outOpInfo.prefund = requiredPreFund; + outOpInfo.contextOffset = getOffsetOfMemoryBytes(context); + outOpInfo.preOpGas = preGas - gasleft() + userOp.preVerificationGas; + } + } + + /** + * Process post-operation, called just after the callData is executed. + * If a paymaster is defined and its validation returned a non-empty context, its postOp is called. + * The excess amount is refunded to the account (or paymaster - if it was used in the request). + * @param mode - Whether is called from innerHandleOp, or outside (postOpReverted). + * @param opInfo - UserOp fields and info collected during validation. + * @param context - The context returned in validatePaymasterUserOp. + * @param actualGas - The gas used so far by this user operation. + */ + function _postExecution( + IPaymaster.PostOpMode mode, + UserOpInfo memory opInfo, + bytes memory context, + uint256 actualGas + ) private returns (uint256 actualGasCost) { + uint256 preGas = gasleft(); + unchecked { + address refundAddress; + MemoryUserOp memory mUserOp = opInfo.mUserOp; + uint256 gasPrice = getUserOpGasPrice(mUserOp); + + address paymaster = mUserOp.paymaster; + if (paymaster == address(0)) { + refundAddress = mUserOp.sender; + } else { + refundAddress = paymaster; + if (context.length > 0) { + actualGasCost = actualGas * gasPrice; + if (mode != IPaymaster.PostOpMode.postOpReverted) { + try + IPaymaster(paymaster).postOp{ gas: mUserOp.paymasterPostOpGasLimit }( + mode, + context, + actualGasCost, + gasPrice + ) + // solhint-disable-next-line no-empty-blocks + { + + } catch { + bytes memory reason = Exec.getReturnData(REVERT_REASON_MAX_LEN); + revert PostOpReverted(reason); + } + } + } + } + actualGas += preGas - gasleft(); + + // Calculating a penalty for unused execution gas + { + uint256 executionGasLimit = mUserOp.callGasLimit + mUserOp.paymasterPostOpGasLimit; + uint256 executionGasUsed = actualGas - opInfo.preOpGas; + // this check is required for the gas used within EntryPoint and not covered by explicit gas limits + if (executionGasLimit > executionGasUsed) { + uint256 unusedGas = executionGasLimit - executionGasUsed; + uint256 unusedGasPenalty = (unusedGas * PENALTY_PERCENT) / 100; + actualGas += unusedGasPenalty; + } + } + + actualGasCost = actualGas * gasPrice; + uint256 prefund = opInfo.prefund; + if (prefund < actualGasCost) { + if (mode == IPaymaster.PostOpMode.postOpReverted) { + actualGasCost = prefund; + emitPrefundTooLow(opInfo); + emitUserOperationEvent(opInfo, false, actualGasCost, actualGas); + } else { + assembly ("memory-safe") { + mstore(0, INNER_REVERT_LOW_PREFUND) + revert(0, 32) + } + } + } else { + uint256 refund = prefund - actualGasCost; + _incrementDeposit(refundAddress, refund); + bool success = mode == IPaymaster.PostOpMode.opSucceeded; + emitUserOperationEvent(opInfo, success, actualGasCost, actualGas); + } + } // unchecked + } + + /** + * The gas price this UserOp agrees to pay. + * Relayer/block builder might submit the TX with higher priorityFee, but the user should not. + * @param mUserOp - The userOp to get the gas price from. + */ + function getUserOpGasPrice(MemoryUserOp memory mUserOp) internal view returns (uint256) { + unchecked { + uint256 maxFeePerGas = mUserOp.maxFeePerGas; + uint256 maxPriorityFeePerGas = mUserOp.maxPriorityFeePerGas; + if (maxFeePerGas == maxPriorityFeePerGas) { + //legacy mode (for networks that don't support basefee opcode) + return maxFeePerGas; + } + return min(maxFeePerGas, maxPriorityFeePerGas + block.basefee); + } + } + + /** + * The offset of the given bytes in memory. + * @param data - The bytes to get the offset of. + */ + function getOffsetOfMemoryBytes(bytes memory data) internal pure returns (uint256 offset) { + assembly { + offset := data + } + } + + /** + * The bytes in memory at the given offset. + * @param offset - The offset to get the bytes from. + */ + function getMemoryBytesFromOffset(uint256 offset) internal pure returns (bytes memory data) { + assembly ("memory-safe") { + data := offset + } + } + + /// @inheritdoc IEntryPoint + function delegateAndRevert(address target, bytes calldata data) external { + (bool success, bytes memory ret) = target.delegatecall(data); + revert DelegateAndRevert(success, ret); + } +} diff --git a/contracts/prebuilts/account/utils/Exec.sol b/contracts/prebuilts/account/utils/Exec.sol new file mode 100644 index 000000000..a245deddd --- /dev/null +++ b/contracts/prebuilts/account/utils/Exec.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.23; + +// solhint-disable no-inline-assembly + +/** + * Utility functions helpful when making different kinds of contract calls in Solidity. + */ +library Exec { + function call(address to, uint256 value, bytes memory data, uint256 txGas) internal returns (bool success) { + assembly ("memory-safe") { + success := call(txGas, to, value, add(data, 0x20), mload(data), 0, 0) + } + } + + function staticcall(address to, bytes memory data, uint256 txGas) internal view returns (bool success) { + assembly ("memory-safe") { + success := staticcall(txGas, to, add(data, 0x20), mload(data), 0, 0) + } + } + + function delegateCall(address to, bytes memory data, uint256 txGas) internal returns (bool success) { + assembly ("memory-safe") { + success := delegatecall(txGas, to, add(data, 0x20), mload(data), 0, 0) + } + } + + // get returned data from last call or calldelegate + function getReturnData(uint256 maxLen) internal pure returns (bytes memory returnData) { + assembly ("memory-safe") { + let len := returndatasize() + if gt(len, maxLen) { + len := maxLen + } + let ptr := mload(0x40) + mstore(0x40, add(ptr, add(len, 0x20))) + mstore(ptr, len) + returndatacopy(add(ptr, 0x20), 0, len) + returnData := ptr + } + } + + // revert with explicit byte array (probably reverted info from call) + function revertWithData(bytes memory returnData) internal pure { + assembly ("memory-safe") { + revert(add(returnData, 32), mload(returnData)) + } + } + + function callAndRevert(address to, bytes memory data, uint256 maxLen) internal { + bool success = call(to, 0, data, gasleft()); + if (!success) { + revertWithData(getReturnData(maxLen)); + } + } +} diff --git a/contracts/prebuilts/account/utils/Helpers.sol b/contracts/prebuilts/account/utils/Helpers.sol new file mode 100644 index 000000000..cab842e29 --- /dev/null +++ b/contracts/prebuilts/account/utils/Helpers.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +/* solhint-disable no-inline-assembly */ + +/* + * For simulation purposes, validateUserOp (and validatePaymasterUserOp) + * must return this value in case of signature failure, instead of revert. + */ +uint256 constant SIG_VALIDATION_FAILED = 1; + +/* + * For simulation purposes, validateUserOp (and validatePaymasterUserOp) + * return this value on success. + */ +uint256 constant SIG_VALIDATION_SUCCESS = 0; + +/** + * Returned data from validateUserOp. + * validateUserOp returns a uint256, which is created by `_packedValidationData` and + * parsed by `_parseValidationData`. + * @param aggregator - address(0) - The account validated the signature by itself. + * address(1) - The account failed to validate the signature. + * otherwise - This is an address of a signature aggregator that must + * be used to validate the signature. + * @param validAfter - This UserOp is valid only after this timestamp. + * @param validaUntil - This UserOp is valid only up to this timestamp. + */ +struct ValidationData { + address aggregator; + uint48 validAfter; + uint48 validUntil; +} + +/** + * Extract sigFailed, validAfter, validUntil. + * Also convert zero validUntil to type(uint48).max. + * @param validationData - The packed validation data. + */ +function _parseValidationData(uint256 validationData) pure returns (ValidationData memory data) { + address aggregator = address(uint160(validationData)); + uint48 validUntil = uint48(validationData >> 160); + if (validUntil == 0) { + validUntil = type(uint48).max; + } + uint48 validAfter = uint48(validationData >> (48 + 160)); + return ValidationData(aggregator, validAfter, validUntil); +} + +/** + * Helper to pack the return value for validateUserOp. + * @param data - The ValidationData to pack. + */ +function _packValidationData(ValidationData memory data) pure returns (uint256) { + return uint160(data.aggregator) | (uint256(data.validUntil) << 160) | (uint256(data.validAfter) << (160 + 48)); +} + +/** + * Helper to pack the return value for validateUserOp, when not using an aggregator. + * @param sigFailed - True for signature failure, false for success. + * @param validUntil - Last timestamp this UserOperation is valid (or zero for infinite). + * @param validAfter - First timestamp this UserOperation is valid. + */ +function _packValidationData(bool sigFailed, uint48 validUntil, uint48 validAfter) pure returns (uint256) { + return (sigFailed ? 1 : 0) | (uint256(validUntil) << 160) | (uint256(validAfter) << (160 + 48)); +} + +/** + * keccak function over calldata. + * @dev copy calldata into memory, do keccak and drop allocated memory. Strangely, this is more efficient than letting solidity do it. + */ +function calldataKeccak(bytes calldata data) pure returns (bytes32 ret) { + assembly ("memory-safe") { + let mem := mload(0x40) + let len := data.length + calldatacopy(mem, data.offset, len) + ret := keccak256(mem, len) + } +} + +/** + * The minimum of two numbers. + * @param a - First number. + * @param b - Second number. + */ +function min(uint256 a, uint256 b) pure returns (uint256) { + return a < b ? a : b; +} diff --git a/contracts/prebuilts/account/utils/NonceManager.sol b/contracts/prebuilts/account/utils/NonceManager.sol new file mode 100644 index 000000000..ec16026ea --- /dev/null +++ b/contracts/prebuilts/account/utils/NonceManager.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +import "../interfaces/INonceManager.sol"; + +/** + * nonce management functionality + */ +abstract contract NonceManager is INonceManager { + /** + * The next valid sequence number for a given nonce key. + */ + mapping(address => mapping(uint192 => uint256)) public nonceSequenceNumber; + + /// @inheritdoc INonceManager + function getNonce(address sender, uint192 key) public view override returns (uint256 nonce) { + return nonceSequenceNumber[sender][key] | (uint256(key) << 64); + } + + // allow an account to manually increment its own nonce. + // (mainly so that during construction nonce can be made non-zero, + // to "absorb" the gas cost of first nonce increment to 1st transaction (construction), + // not to 2nd transaction) + function incrementNonce(uint192 key) public override { + nonceSequenceNumber[msg.sender][key]++; + } + + /** + * validate nonce uniqueness for this account. + * called just after validateUserOp() + * @return true if the nonce was incremented successfully. + * false if the current nonce doesn't match the given one. + */ + function _validateAndUpdateNonce(address sender, uint256 nonce) internal returns (bool) { + uint192 key = uint192(nonce >> 64); + uint64 seq = uint64(nonce); + return nonceSequenceNumber[sender][key]++ == seq; + } +} diff --git a/contracts/prebuilts/account/utils/OracleHelper.sol b/contracts/prebuilts/account/utils/OracleHelper.sol new file mode 100644 index 000000000..89ce08843 --- /dev/null +++ b/contracts/prebuilts/account/utils/OracleHelper.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +/* solhint-disable not-rely-on-time */ + +import "../interfaces/IOracle.sol"; + +/// @title Helper functions for dealing with various forms of price feed oracles. +/// @notice Maintains a price cache and updates the current price if needed. +/// In the best case scenario we have a direct oracle from the token to the native asset. +/// Also support tokens that have no direct price oracle to the native asset. +/// Sometimes oracles provide the price in the opposite direction of what we need in the moment. +abstract contract OracleHelper { + event TokenPriceUpdated(uint256 currentPrice, uint256 previousPrice, uint256 cachedPriceTimestamp); + + uint256 private constant PRICE_DENOMINATOR = 1e26; + + struct OracleHelperConfig { + /// @notice The price cache will be returned without even fetching the oracles for this number of seconds + uint48 cacheTimeToLive; + /// @notice The maximum acceptable age of the price oracle round + uint48 maxOracleRoundAge; + /// @notice The Oracle contract used to fetch the latest token prices + IOracle tokenOracle; + /// @notice The Oracle contract used to fetch the latest native asset prices. Only needed if tokenToNativeOracle flag is not set. + IOracle nativeOracle; + /// @notice If 'true' we will fetch price directly from tokenOracle + /// @notice If 'false' we will use nativeOracle to establish a token price through a shared third currency + bool tokenToNativeOracle; + /// @notice 'false' if price is bridging-asset-per-token (or native-asset-per-token), 'true' if price is tokens-per-bridging-asset + bool tokenOracleReverse; + /// @notice 'false' if price is bridging-asset-per-native-asset, 'true' if price is native-asset-per-bridging-asset + bool nativeOracleReverse; + /// @notice The price update threshold percentage from PRICE_DENOMINATOR that triggers a price update (1e26 = 100%) + uint256 priceUpdateThreshold; + } + + /// @notice The cached token price from the Oracle, always in (native-asset-per-token) * PRICE_DENOMINATOR format + uint256 public cachedPrice; + + /// @notice The timestamp of a block when the cached price was updated + uint48 public cachedPriceTimestamp; + + OracleHelperConfig public oracleHelperConfig; + + /// @notice The "10^(tokenOracle.decimals)" value used for the price calculation + uint128 private tokenOracleDecimalPower; + + /// @notice The "10^(nativeOracle.decimals)" value used for the price calculation + uint128 private nativeOracleDecimalPower; + + constructor(OracleHelperConfig memory _oracleHelperConfig) { + cachedPrice = type(uint256).max; // initialize the storage slot to invalid value + _setOracleConfiguration(_oracleHelperConfig); + } + + function _setOracleConfiguration(OracleHelperConfig memory _oracleHelperConfig) internal { + oracleHelperConfig = _oracleHelperConfig; + require(_oracleHelperConfig.priceUpdateThreshold <= PRICE_DENOMINATOR, "TPM: update threshold too high"); + tokenOracleDecimalPower = uint128(10 ** oracleHelperConfig.tokenOracle.decimals()); + if (oracleHelperConfig.tokenToNativeOracle) { + require(address(oracleHelperConfig.nativeOracle) == address(0), "TPM: native oracle must be zero"); + nativeOracleDecimalPower = 1; + } else { + nativeOracleDecimalPower = uint128(10 ** oracleHelperConfig.nativeOracle.decimals()); + } + } + + /// @notice Updates the token price by fetching the latest price from the Oracle. + /// @param force true to force cache update, even if called after short time or the change is lower than the update threshold. + /// @return newPrice the new cached token price + function updateCachedPrice(bool force) public returns (uint256) { + uint256 cacheTimeToLive = oracleHelperConfig.cacheTimeToLive; + uint256 cacheAge = block.timestamp - cachedPriceTimestamp; + if (!force && cacheAge <= cacheTimeToLive) { + return cachedPrice; + } + uint256 priceUpdateThreshold = oracleHelperConfig.priceUpdateThreshold; + IOracle tokenOracle = oracleHelperConfig.tokenOracle; + IOracle nativeOracle = oracleHelperConfig.nativeOracle; + + uint256 _cachedPrice = cachedPrice; + uint256 tokenPrice = fetchPrice(tokenOracle); + uint256 nativeAssetPrice = 1; + // If the 'TokenOracle' returns the price in the native asset units there is no need to fetch native asset price + if (!oracleHelperConfig.tokenToNativeOracle) { + nativeAssetPrice = fetchPrice(nativeOracle); + } + uint256 newPrice = calculatePrice( + tokenPrice, + nativeAssetPrice, + oracleHelperConfig.tokenOracleReverse, + oracleHelperConfig.nativeOracleReverse + ); + uint256 priceRatio = (PRICE_DENOMINATOR * newPrice) / _cachedPrice; + bool updateRequired = force || + priceRatio > PRICE_DENOMINATOR + priceUpdateThreshold || + priceRatio < PRICE_DENOMINATOR - priceUpdateThreshold; + if (!updateRequired) { + return _cachedPrice; + } + cachedPrice = newPrice; + cachedPriceTimestamp = uint48(block.timestamp); + emit TokenPriceUpdated(newPrice, _cachedPrice, cachedPriceTimestamp); + return newPrice; + } + + /** + * Calculate the effective price of the selected token denominated in native asset. + * + * @param tokenPrice - the price of the token relative to a native asset or a bridging asset like the U.S. dollar. + * @param nativeAssetPrice - the price of the native asset relative to a bridging asset or 1 if no bridging needed. + * @param tokenOracleReverse - flag indicating direction of the "tokenPrice". + * @param nativeOracleReverse - flag indicating direction of the "nativeAssetPrice". + * @return the native-asset-per-token price multiplied by the PRICE_DENOMINATOR constant. + */ + function calculatePrice( + uint256 tokenPrice, + uint256 nativeAssetPrice, + bool tokenOracleReverse, + bool nativeOracleReverse + ) private view returns (uint256) { + // tokenPrice is normalized as bridging-asset-per-token + if (tokenOracleReverse) { + // inverting tokenPrice that was tokens-per-bridging-asset (or tokens-per-native-asset) + tokenPrice = (PRICE_DENOMINATOR * tokenOracleDecimalPower) / tokenPrice; + } else { + // tokenPrice already bridging-asset-per-token (or native-asset-per-token) + tokenPrice = (PRICE_DENOMINATOR * tokenPrice) / tokenOracleDecimalPower; + } + + if (nativeOracleReverse) { + // multiplying by nativeAssetPrice that is native-asset-per-bridging-asset + // => result = (bridging-asset / token) * (native-asset / bridging-asset) = native-asset / token + return (nativeAssetPrice * tokenPrice) / nativeOracleDecimalPower; + } else { + // dividing by nativeAssetPrice that is bridging-asset-per-native-asset + // => result = (bridging-asset / token) / (bridging-asset / native-asset) = native-asset / token + return (tokenPrice * nativeOracleDecimalPower) / nativeAssetPrice; + } + } + + /// @notice Fetches the latest price from the given Oracle. + /// @dev This function is used to get the latest price from the tokenOracle or nativeOracle. + /// @param _oracle The Oracle contract to fetch the price from. + /// @return price The latest price fetched from the Oracle. + function fetchPrice(IOracle _oracle) internal view returns (uint256 price) { + (uint80 roundId, int256 answer, , uint256 updatedAt, uint80 answeredInRound) = _oracle.latestRoundData(); + require(answer > 0, "TPM: Chainlink price <= 0"); + require(updatedAt >= block.timestamp - oracleHelperConfig.maxOracleRoundAge, "TPM: Incomplete round"); + require(answeredInRound >= roundId, "TPM: Stale price"); + price = uint256(answer); + } +} diff --git a/contracts/prebuilts/account/utils/SenderCreator.sol b/contracts/prebuilts/account/utils/SenderCreator.sol new file mode 100644 index 000000000..91ac6c882 --- /dev/null +++ b/contracts/prebuilts/account/utils/SenderCreator.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +/** + * Helper contract for EntryPoint, to call userOp.initCode from a "neutral" address, + * which is explicitly not the entryPoint itself. + */ +contract SenderCreator { + /** + * Call the "initCode" factory to create and return the sender account address. + * @param initCode - The initCode value from a UserOp. contains 20 bytes of factory address, + * followed by calldata. + * @return sender - The returned address of the created account, or zero address on failure. + */ + function createSender(bytes calldata initCode) external returns (address sender) { + address factory = address(bytes20(initCode[0:20])); + bytes memory initCallData = initCode[20:]; + bool success; + /* solhint-disable no-inline-assembly */ + assembly ("memory-safe") { + success := call(gas(), factory, 0, add(initCallData, 0x20), mload(initCallData), 0, 32) + sender := mload(0) + } + if (!success) { + sender = address(0); + } + } +} diff --git a/contracts/prebuilts/account/utils/StakeManager.sol b/contracts/prebuilts/account/utils/StakeManager.sol new file mode 100644 index 000000000..f53ecb8b5 --- /dev/null +++ b/contracts/prebuilts/account/utils/StakeManager.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.23; + +import "../interfaces/IStakeManager.sol"; + +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable not-rely-on-time */ + +/** + * Manage deposits and stakes. + * Deposit is just a balance used to pay for UserOperations (either by a paymaster or an account). + * Stake is value locked for at least "unstakeDelay" by a paymaster. + */ +abstract contract StakeManager is IStakeManager { + /// maps paymaster to their deposits and stakes + mapping(address => DepositInfo) public deposits; + + /// @inheritdoc IStakeManager + function getDepositInfo(address account) public view returns (DepositInfo memory info) { + return deposits[account]; + } + + /** + * Internal method to return just the stake info. + * @param addr - The account to query. + */ + function _getStakeInfo(address addr) internal view returns (StakeInfo memory info) { + DepositInfo storage depositInfo = deposits[addr]; + info.stake = depositInfo.stake; + info.unstakeDelaySec = depositInfo.unstakeDelaySec; + } + + /// @inheritdoc IStakeManager + function balanceOf(address account) public view returns (uint256) { + return deposits[account].deposit; + } + + receive() external payable { + depositTo(msg.sender); + } + + /** + * Increments an account's deposit. + * @param account - The account to increment. + * @param amount - The amount to increment by. + * @return the updated deposit of this account + */ + function _incrementDeposit(address account, uint256 amount) internal returns (uint256) { + DepositInfo storage info = deposits[account]; + uint256 newAmount = info.deposit + amount; + info.deposit = newAmount; + return newAmount; + } + + /** + * Add to the deposit of the given account. + * @param account - The account to add to. + */ + function depositTo(address account) public payable virtual { + uint256 newDeposit = _incrementDeposit(account, msg.value); + emit Deposited(account, newDeposit); + } + + /** + * Add to the account's stake - amount and delay + * any pending unstake is first cancelled. + * @param unstakeDelaySec The new lock duration before the deposit can be withdrawn. + */ + function addStake(uint32 unstakeDelaySec) public payable { + DepositInfo storage info = deposits[msg.sender]; + require(unstakeDelaySec > 0, "must specify unstake delay"); + require(unstakeDelaySec >= info.unstakeDelaySec, "cannot decrease unstake time"); + uint256 stake = info.stake + msg.value; + require(stake > 0, "no stake specified"); + require(stake <= type(uint112).max, "stake overflow"); + deposits[msg.sender] = DepositInfo(info.deposit, true, uint112(stake), unstakeDelaySec, 0); + emit StakeLocked(msg.sender, stake, unstakeDelaySec); + } + + /** + * Attempt to unlock the stake. + * The value can be withdrawn (using withdrawStake) after the unstake delay. + */ + function unlockStake() external { + DepositInfo storage info = deposits[msg.sender]; + require(info.unstakeDelaySec != 0, "not staked"); + require(info.staked, "already unstaking"); + uint48 withdrawTime = uint48(block.timestamp) + info.unstakeDelaySec; + info.withdrawTime = withdrawTime; + info.staked = false; + emit StakeUnlocked(msg.sender, withdrawTime); + } + + /** + * Withdraw from the (unlocked) stake. + * Must first call unlockStake and wait for the unstakeDelay to pass. + * @param withdrawAddress - The address to send withdrawn value. + */ + function withdrawStake(address payable withdrawAddress) external { + DepositInfo storage info = deposits[msg.sender]; + uint256 stake = info.stake; + require(stake > 0, "No stake to withdraw"); + require(info.withdrawTime > 0, "must call unlockStake() first"); + require(info.withdrawTime <= block.timestamp, "Stake withdrawal is not due"); + info.unstakeDelaySec = 0; + info.withdrawTime = 0; + info.stake = 0; + emit StakeWithdrawn(msg.sender, withdrawAddress, stake); + (bool success, ) = withdrawAddress.call{ value: stake }(""); + require(success, "failed to withdraw stake"); + } + + /** + * Withdraw from the deposit. + * @param withdrawAddress - The address to send withdrawn value. + * @param withdrawAmount - The amount to withdraw. + */ + function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) external { + DepositInfo storage info = deposits[msg.sender]; + require(withdrawAmount <= info.deposit, "Withdraw amount too large"); + info.deposit = info.deposit - withdrawAmount; + emit Withdrawn(msg.sender, withdrawAddress, withdrawAmount); + (bool success, ) = withdrawAddress.call{ value: withdrawAmount }(""); + require(success, "failed to withdraw"); + } +} diff --git a/contracts/prebuilts/account/utils/TokenCallbackHandler.sol b/contracts/prebuilts/account/utils/TokenCallbackHandler.sol new file mode 100644 index 000000000..1900af613 --- /dev/null +++ b/contracts/prebuilts/account/utils/TokenCallbackHandler.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +/* solhint-disable no-empty-blocks */ + +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import "@openzeppelin/contracts/token/ERC777/IERC777Recipient.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; +import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; + +/** + * Token callback handler. + * Handles supported tokens' callbacks, allowing account receiving these tokens. + */ +contract TokenCallbackHandler is IERC777Recipient, IERC721Receiver, IERC1155Receiver { + function tokensReceived( + address, + address, + address, + uint256, + bytes calldata, + bytes calldata + ) external pure override {} + + function onERC721Received(address, address, uint256, bytes calldata) external pure override returns (bytes4) { + return IERC721Receiver.onERC721Received.selector; + } + + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes calldata + ) external pure override returns (bytes4) { + return IERC1155Receiver.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address, + address, + uint256[] calldata, + uint256[] calldata, + bytes calldata + ) external pure override returns (bytes4) { + return IERC1155Receiver.onERC1155BatchReceived.selector; + } + + function supportsInterface(bytes4 interfaceId) external view virtual override returns (bool) { + return + interfaceId == type(IERC721Receiver).interfaceId || + interfaceId == type(IERC1155Receiver).interfaceId || + interfaceId == type(IERC165).interfaceId; + } +} diff --git a/contracts/prebuilts/account/utils/UniswapHelper.sol b/contracts/prebuilts/account/utils/UniswapHelper.sol new file mode 100644 index 000000000..30f5031ae --- /dev/null +++ b/contracts/prebuilts/account/utils/UniswapHelper.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +/* solhint-disable not-rely-on-time */ + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +import "@uniswap/swap-router-contracts/contracts/interfaces/IV3SwapRouter.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/IPeripheryPayments.sol"; + +abstract contract UniswapHelper { + event UniswapReverted(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMin); + + uint256 private constant PRICE_DENOMINATOR = 1e26; + + struct UniswapHelperConfig { + /// @notice Minimum native asset amount to receive from a single swap + uint256 minSwapAmount; + uint24 uniswapPoolFee; + uint8 slippage; + bool wethIsNativeAsset; + } + + /// @notice The Uniswap V3 SwapRouter contract + IV3SwapRouter public immutable uniswap; + + /// @notice The ERC20 token used for transaction fee payments + IERC20Metadata public immutable token; + + /// @notice The ERC-20 token that wraps the native asset for current chain + IERC20 public immutable wrappedNative; + + UniswapHelperConfig public uniswapHelperConfig; + + constructor( + IERC20Metadata _token, + IERC20 _wrappedNative, + IV3SwapRouter _uniswap, + UniswapHelperConfig memory _uniswapHelperConfig + ) { + _token.approve(address(_uniswap), type(uint256).max); + token = _token; + wrappedNative = _wrappedNative; + uniswap = _uniswap; + _setUniswapHelperConfiguration(_uniswapHelperConfig); + } + + function _setUniswapHelperConfiguration(UniswapHelperConfig memory _uniswapHelperConfig) internal { + uniswapHelperConfig = _uniswapHelperConfig; + } + + function _maybeSwapTokenToWeth(IERC20Metadata tokenIn, uint256 quote) internal returns (uint256) { + uint256 tokenBalance = tokenIn.balanceOf(address(this)); + uint256 tokenDecimals = tokenIn.decimals(); + + uint256 amountOutMin = addSlippage( + tokenToWei(tokenBalance, tokenDecimals, quote), + uniswapHelperConfig.slippage + ); + + if (amountOutMin < uniswapHelperConfig.minSwapAmount) { + return 0; + } + // note: calling 'swapToToken' but destination token is Wrapped Ether + return + swapToToken( + address(tokenIn), + address(wrappedNative), + tokenBalance, + amountOutMin, + uniswapHelperConfig.uniswapPoolFee + ); + } + + function addSlippage(uint256 amount, uint8 slippage) private pure returns (uint256) { + return (amount * (1000 - slippage)) / 1000; + } + + function tokenToWei(uint256 amount, uint256 decimals, uint256 price) public pure returns (uint256) { + return (amount * price * (10 ** (18 - decimals))) / PRICE_DENOMINATOR; + } + + function weiToToken(uint256 amount, uint256 decimals, uint256 price) public pure returns (uint256) { + return (amount * PRICE_DENOMINATOR) / (price * (10 ** (18 - decimals))); + } + + function unwrapWeth(uint256 amount) internal { + if (uniswapHelperConfig.wethIsNativeAsset) { + return; + } + IPeripheryPayments(address(uniswap)).unwrapWETH9(amount, address(this)); + } + + // swap ERC-20 tokens at market price + function swapToToken( + address tokenIn, + address tokenOut, + uint256 amountIn, + uint256 amountOutMin, + uint24 fee + ) internal returns (uint256 amountOut) { + IV3SwapRouter.ExactInputSingleParams memory params = IV3SwapRouter.ExactInputSingleParams( + tokenIn, //tokenIn + tokenOut, //tokenOut + fee, + address(uniswap), + amountIn, + amountOutMin, + 0 + ); + try uniswap.exactInputSingle(params) returns (uint256 _amountOut) { + amountOut = _amountOut; + } catch { + emit UniswapReverted(tokenIn, tokenOut, amountIn, amountOutMin); + amountOut = 0; + } + } +} diff --git a/contracts/prebuilts/account/utils/UserOperationLib.sol b/contracts/prebuilts/account/utils/UserOperationLib.sol new file mode 100644 index 000000000..0b5cbf719 --- /dev/null +++ b/contracts/prebuilts/account/utils/UserOperationLib.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +/* solhint-disable no-inline-assembly */ + +import "../interfaces/PackedUserOperation.sol"; +import { calldataKeccak, min } from "./Helpers.sol"; + +/** + * Utility functions helpful when working with UserOperation structs. + */ +library UserOperationLib { + uint256 public constant PAYMASTER_VALIDATION_GAS_OFFSET = 20; + uint256 public constant PAYMASTER_POSTOP_GAS_OFFSET = 36; + uint256 public constant PAYMASTER_DATA_OFFSET = 52; + /** + * Get sender from user operation data. + * @param userOp - The user operation data. + */ + function getSender(PackedUserOperation calldata userOp) internal pure returns (address) { + address data; + //read sender from userOp, which is first userOp member (saves 800 gas...) + assembly { + data := calldataload(userOp) + } + return address(uint160(data)); + } + + /** + * Relayer/block builder might submit the TX with higher priorityFee, + * but the user should not pay above what he signed for. + * @param userOp - The user operation data. + */ + function gasPrice(PackedUserOperation calldata userOp) internal view returns (uint256) { + unchecked { + (uint256 maxPriorityFeePerGas, uint256 maxFeePerGas) = unpackUints(userOp.gasFees); + if (maxFeePerGas == maxPriorityFeePerGas) { + //legacy mode (for networks that don't support basefee opcode) + return maxFeePerGas; + } + return min(maxFeePerGas, maxPriorityFeePerGas + block.basefee); + } + } + + /** + * Pack the user operation data into bytes for hashing. + * @param userOp - The user operation data. + */ + function encode(PackedUserOperation calldata userOp) internal pure returns (bytes memory ret) { + address sender = getSender(userOp); + uint256 nonce = userOp.nonce; + bytes32 hashInitCode = calldataKeccak(userOp.initCode); + bytes32 hashCallData = calldataKeccak(userOp.callData); + bytes32 accountGasLimits = userOp.accountGasLimits; + uint256 preVerificationGas = userOp.preVerificationGas; + bytes32 gasFees = userOp.gasFees; + bytes32 hashPaymasterAndData = calldataKeccak(userOp.paymasterAndData); + + return + abi.encode( + sender, + nonce, + hashInitCode, + hashCallData, + accountGasLimits, + preVerificationGas, + gasFees, + hashPaymasterAndData + ); + } + + function unpackUints(bytes32 packed) internal pure returns (uint256 high128, uint256 low128) { + return (uint128(bytes16(packed)), uint128(uint256(packed))); + } + + //unpack just the high 128-bits from a packed value + function unpackHigh128(bytes32 packed) internal pure returns (uint256) { + return uint256(packed) >> 128; + } + + // unpack just the low 128-bits from a packed value + function unpackLow128(bytes32 packed) internal pure returns (uint256) { + return uint128(uint256(packed)); + } + + function unpackMaxPriorityFeePerGas(PackedUserOperation calldata userOp) internal pure returns (uint256) { + return unpackHigh128(userOp.gasFees); + } + + function unpackMaxFeePerGas(PackedUserOperation calldata userOp) internal pure returns (uint256) { + return unpackLow128(userOp.gasFees); + } + + function unpackVerificationGasLimit(PackedUserOperation calldata userOp) internal pure returns (uint256) { + return unpackHigh128(userOp.accountGasLimits); + } + + function unpackCallGasLimit(PackedUserOperation calldata userOp) internal pure returns (uint256) { + return unpackLow128(userOp.accountGasLimits); + } + + function unpackPaymasterVerificationGasLimit(PackedUserOperation calldata userOp) internal pure returns (uint256) { + return uint128(bytes16(userOp.paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET:PAYMASTER_POSTOP_GAS_OFFSET])); + } + + function unpackPostOpGasLimit(PackedUserOperation calldata userOp) internal pure returns (uint256) { + return uint128(bytes16(userOp.paymasterAndData[PAYMASTER_POSTOP_GAS_OFFSET:PAYMASTER_DATA_OFFSET])); + } + + function unpackPaymasterStaticFields( + bytes calldata paymasterAndData + ) internal pure returns (address paymaster, uint256 validationGasLimit, uint256 postOpGasLimit) { + return ( + address(bytes20(paymasterAndData[:PAYMASTER_VALIDATION_GAS_OFFSET])), + uint128(bytes16(paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET:PAYMASTER_POSTOP_GAS_OFFSET])), + uint128(bytes16(paymasterAndData[PAYMASTER_POSTOP_GAS_OFFSET:PAYMASTER_DATA_OFFSET])) + ); + } + + /** + * Hash the user operation data. + * @param userOp - The user operation data. + */ + function hash(PackedUserOperation calldata userOp) internal pure returns (bytes32) { + return keccak256(encode(userOp)); + } +} diff --git a/contracts/prebuilts/airdrop/Airdrop.sol b/contracts/prebuilts/airdrop/Airdrop.sol new file mode 100644 index 000000000..b2865c9b2 --- /dev/null +++ b/contracts/prebuilts/airdrop/Airdrop.sol @@ -0,0 +1,616 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +import "solady/src/utils/MerkleProofLib.sol"; +import "solady/src/utils/ECDSA.sol"; +import "solady/src/utils/EIP712.sol"; +import "solady/src/utils/SafeTransferLib.sol"; +import "solady/src/utils/SignatureCheckerLib.sol"; + +import { Initializable } from "../../extension/Initializable.sol"; +import { Ownable } from "../../extension/Ownable.sol"; +import { ContractMetadata } from "../../extension/ContractMetadata.sol"; + +import "../../eip/interface/IERC20.sol"; +import "../../eip/interface/IERC721.sol"; +import "../../eip/interface/IERC1155.sol"; + +contract Airdrop is EIP712, Initializable, Ownable, ContractMetadata { + /*/////////////////////////////////////////////////////////////// + State, constants & structs + //////////////////////////////////////////////////////////////*/ + + /// @dev token contract address => conditionId + mapping(address => uint256) public tokenConditionId; + /// @dev token contract address => merkle root + mapping(address => bytes32) public tokenMerkleRoot; + /// @dev conditionId => hash(claimer address, token address, token id [1155]) => has claimed + mapping(uint256 => mapping(bytes32 => bool)) private claimed; + /// @dev Mapping from request UID => whether the request is processed. + mapping(bytes32 => bool) public processed; + + struct AirdropContentERC20 { + address recipient; + uint256 amount; + } + + struct AirdropContentERC721 { + address recipient; + uint256 tokenId; + } + + struct AirdropContentERC1155 { + address recipient; + uint256 tokenId; + uint256 amount; + } + + struct AirdropRequestERC20 { + bytes32 uid; + address tokenAddress; + uint256 expirationTimestamp; + AirdropContentERC20[] contents; + } + + struct AirdropRequestERC721 { + bytes32 uid; + address tokenAddress; + uint256 expirationTimestamp; + AirdropContentERC721[] contents; + } + + struct AirdropRequestERC1155 { + bytes32 uid; + address tokenAddress; + uint256 expirationTimestamp; + AirdropContentERC1155[] contents; + } + + bytes32 private constant CONTENT_TYPEHASH_ERC20 = + keccak256("AirdropContentERC20(address recipient,uint256 amount)"); + bytes32 private constant REQUEST_TYPEHASH_ERC20 = + keccak256( + "AirdropRequestERC20(bytes32 uid,address tokenAddress,uint256 expirationTimestamp,AirdropContentERC20[] contents)AirdropContentERC20(address recipient,uint256 amount)" + ); + + bytes32 private constant CONTENT_TYPEHASH_ERC721 = + keccak256("AirdropContentERC721(address recipient,uint256 tokenId)"); + bytes32 private constant REQUEST_TYPEHASH_ERC721 = + keccak256( + "AirdropRequestERC721(bytes32 uid,address tokenAddress,uint256 expirationTimestamp,AirdropContentERC721[] contents)AirdropContentERC721(address recipient,uint256 tokenId)" + ); + + bytes32 private constant CONTENT_TYPEHASH_ERC1155 = + keccak256("AirdropContentERC1155(address recipient,uint256 tokenId,uint256 amount)"); + bytes32 private constant REQUEST_TYPEHASH_ERC1155 = + keccak256( + "AirdropRequestERC1155(bytes32 uid,address tokenAddress,uint256 expirationTimestamp,AirdropContentERC1155[] contents)AirdropContentERC1155(address recipient,uint256 tokenId,uint256 amount)" + ); + + address private constant NATIVE_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + /*/////////////////////////////////////////////////////////////// + Errors + //////////////////////////////////////////////////////////////*/ + + error AirdropInvalidProof(); + error AirdropAlreadyClaimed(); + error AirdropNoMerkleRoot(); + error AirdropValueMismatch(); + error AirdropRequestExpired(uint256 expirationTimestamp); + error AirdropRequestAlreadyProcessed(); + error AirdropRequestInvalidSigner(); + + /*/////////////////////////////////////////////////////////////// + Events + //////////////////////////////////////////////////////////////*/ + + event Airdrop(address token); + event AirdropWithSignature(address token); + event AirdropClaimed(address token, address receiver); + + /*/////////////////////////////////////////////////////////////// + Constructor + //////////////////////////////////////////////////////////////*/ + + constructor() { + _disableInitializers(); + } + + function initialize(address _defaultAdmin, string memory _contractURI) external initializer { + _setupOwner(_defaultAdmin); + _setupContractURI(_contractURI); + } + + /*/////////////////////////////////////////////////////////////// + Airdrop Push + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Lets contract-owner send native token (eth) to a list of addresses. + * @dev Owner should send total airdrop amount as msg.value. + * Can only be called by contract owner. + * + * @param _contents List containing recipients and amounts to airdrop + */ + function airdropNativeToken(AirdropContentERC20[] calldata _contents) external payable onlyOwner { + uint256 len = _contents.length; + uint256 nativeTokenAmount; + + for (uint256 i = 0; i < len; i++) { + nativeTokenAmount += _contents[i].amount; + + SafeTransferLib.safeTransferETH(_contents[i].recipient, _contents[i].amount); + } + + if (nativeTokenAmount != msg.value) { + revert AirdropValueMismatch(); + } + + emit Airdrop(NATIVE_TOKEN_ADDRESS); + } + + /** + * @notice Lets contract owner send ERC20 tokens to a list of addresses. + * @dev The token-owner should approve total airdrop amount to this contract. + * Can only be called by contract owner. + * + * @param _tokenAddress Address of the ERC20 token being airdropped + * @param _contents List containing recipients and amounts to airdrop + */ + function airdropERC20(address _tokenAddress, AirdropContentERC20[] calldata _contents) external onlyOwner { + uint256 len = _contents.length; + + for (uint256 i = 0; i < len; i++) { + SafeTransferLib.safeTransferFrom(_tokenAddress, msg.sender, _contents[i].recipient, _contents[i].amount); + } + + emit Airdrop(_tokenAddress); + } + + /** + * @notice Lets contract owner send ERC721 tokens to a list of addresses. + * @dev The token-owner should approve airdrop tokenIds to this contract. + * Can only be called by contract owner. + * + * @param _tokenAddress Address of the ERC721 token being airdropped + * @param _contents List containing recipients and tokenIds to airdrop + */ + function airdropERC721(address _tokenAddress, AirdropContentERC721[] calldata _contents) external onlyOwner { + uint256 len = _contents.length; + + for (uint256 i = 0; i < len; i++) { + IERC721(_tokenAddress).safeTransferFrom(msg.sender, _contents[i].recipient, _contents[i].tokenId); + } + + emit Airdrop(_tokenAddress); + } + + /** + * @notice Lets contract owner send ERC1155 tokens to a list of addresses. + * @dev The token-owner should approve airdrop tokenIds and amounts to this contract. + * Can only be called by contract owner. + * + * @param _tokenAddress Address of the ERC1155 token being airdropped + * @param _contents List containing recipients, tokenIds, and amounts to airdrop + */ + function airdropERC1155(address _tokenAddress, AirdropContentERC1155[] calldata _contents) external onlyOwner { + uint256 len = _contents.length; + + for (uint256 i = 0; i < len; i++) { + IERC1155(_tokenAddress).safeTransferFrom( + msg.sender, + _contents[i].recipient, + _contents[i].tokenId, + _contents[i].amount, + "" + ); + } + + emit Airdrop(_tokenAddress); + } + + /*/////////////////////////////////////////////////////////////// + Airdrop With Signature + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Lets contract owner send ERC20 tokens to a list of addresses with EIP-712 signature. + * @dev The token-owner should approve airdrop amounts to this contract. + * Signer should be the contract owner. + * + * @param req Struct containing airdrop contents, uid, and expiration timestamp + * @param signature EIP-712 signature to perform the airdrop + */ + function airdropERC20WithSignature(AirdropRequestERC20 calldata req, bytes calldata signature) external { + // verify expiration timestamp + if (req.expirationTimestamp < block.timestamp) { + revert AirdropRequestExpired(req.expirationTimestamp); + } + + if (processed[req.uid]) { + revert AirdropRequestAlreadyProcessed(); + } + + // verify data + if (!_verifyRequestSignerERC20(req, signature)) { + revert AirdropRequestInvalidSigner(); + } + + processed[req.uid] = true; + + uint256 len = req.contents.length; + address _from = owner(); + + for (uint256 i = 0; i < len; i++) { + SafeTransferLib.safeTransferFrom( + req.tokenAddress, + _from, + req.contents[i].recipient, + req.contents[i].amount + ); + } + + emit AirdropWithSignature(req.tokenAddress); + } + + /** + * @notice Lets contract owner send ERC721 tokens to a list of addresses with EIP-712 signature. + * @dev The token-owner should approve airdrop tokenIds to this contract. + * Signer should be the contract owner. + * + * @param req Struct containing airdrop contents, uid, and expiration timestamp + * @param signature EIP-712 signature to perform the airdrop + */ + function airdropERC721WithSignature(AirdropRequestERC721 calldata req, bytes calldata signature) external { + // verify expiration timestamp + if (req.expirationTimestamp < block.timestamp) { + revert AirdropRequestExpired(req.expirationTimestamp); + } + + if (processed[req.uid]) { + revert AirdropRequestAlreadyProcessed(); + } + + // verify data + if (!_verifyRequestSignerERC721(req, signature)) { + revert AirdropRequestInvalidSigner(); + } + + processed[req.uid] = true; + + address _from = owner(); + uint256 len = req.contents.length; + + for (uint256 i = 0; i < len; i++) { + IERC721(req.tokenAddress).safeTransferFrom(_from, req.contents[i].recipient, req.contents[i].tokenId); + } + + emit AirdropWithSignature(req.tokenAddress); + } + + /** + * @notice Lets contract owner send ERC1155 tokens to a list of addresses with EIP-712 signature. + * @dev The token-owner should approve airdrop tokenIds and amounts to this contract. + * Signer should be the contract owner. + * + * @param req Struct containing airdrop contents, uid, and expiration timestamp + * @param signature EIP-712 signature to perform the airdrop + */ + function airdropERC1155WithSignature(AirdropRequestERC1155 calldata req, bytes calldata signature) external { + // verify expiration timestamp + if (req.expirationTimestamp < block.timestamp) { + revert AirdropRequestExpired(req.expirationTimestamp); + } + + if (processed[req.uid]) { + revert AirdropRequestAlreadyProcessed(); + } + + // verify data + if (!_verifyRequestSignerERC1155(req, signature)) { + revert AirdropRequestInvalidSigner(); + } + + processed[req.uid] = true; + + address _from = owner(); + uint256 len = req.contents.length; + + for (uint256 i = 0; i < len; i++) { + IERC1155(req.tokenAddress).safeTransferFrom( + _from, + req.contents[i].recipient, + req.contents[i].tokenId, + req.contents[i].amount, + "" + ); + } + + emit AirdropWithSignature(req.tokenAddress); + } + + /*/////////////////////////////////////////////////////////////// + Airdrop Claimable + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Lets allowlisted addresses claim ERC20 airdrop tokens. + * @dev The token-owner should approve total airdrop amount to this contract, + * and set merkle root of allowlisted address for this airdrop. + * + * @param _token Address of ERC20 airdrop token + * @param _receiver Allowlisted address for which the token is being claimed + * @param _quantity Allowlisted quantity of tokens to claim + * @param _proofs Merkle proofs for allowlist verification + */ + function claimERC20(address _token, address _receiver, uint256 _quantity, bytes32[] calldata _proofs) external { + bytes32 claimHash = _getClaimHashERC20(_receiver, _token); + uint256 conditionId = tokenConditionId[_token]; + + if (claimed[conditionId][claimHash]) { + revert AirdropAlreadyClaimed(); + } + + bytes32 _tokenMerkleRoot = tokenMerkleRoot[_token]; + if (_tokenMerkleRoot == bytes32(0)) { + revert AirdropNoMerkleRoot(); + } + + bool valid = MerkleProofLib.verifyCalldata( + _proofs, + _tokenMerkleRoot, + keccak256(abi.encodePacked(_receiver, _quantity)) + ); + if (!valid) { + revert AirdropInvalidProof(); + } + + claimed[conditionId][claimHash] = true; + + SafeTransferLib.safeTransferFrom(_token, owner(), _receiver, _quantity); + + emit AirdropClaimed(_token, _receiver); + } + + /** + * @notice Lets allowlisted addresses claim ERC721 airdrop tokens. + * @dev The token-owner should approve airdrop tokenIds to this contract, + * and set merkle root of allowlisted address for this airdrop. + * + * @param _token Address of ERC721 airdrop token + * @param _receiver Allowlisted address for which the token is being claimed + * @param _tokenId Allowlisted tokenId to claim + * @param _proofs Merkle proofs for allowlist verification + */ + function claimERC721(address _token, address _receiver, uint256 _tokenId, bytes32[] calldata _proofs) external { + bytes32 claimHash = _getClaimHashERC721(_receiver, _token, _tokenId); + uint256 conditionId = tokenConditionId[_token]; + + if (claimed[conditionId][claimHash]) { + revert AirdropAlreadyClaimed(); + } + + bytes32 _tokenMerkleRoot = tokenMerkleRoot[_token]; + if (_tokenMerkleRoot == bytes32(0)) { + revert AirdropNoMerkleRoot(); + } + + bool valid = MerkleProofLib.verifyCalldata( + _proofs, + _tokenMerkleRoot, + keccak256(abi.encodePacked(_receiver, _tokenId)) + ); + if (!valid) { + revert AirdropInvalidProof(); + } + + claimed[conditionId][claimHash] = true; + + IERC721(_token).safeTransferFrom(owner(), _receiver, _tokenId); + + emit AirdropClaimed(_token, _receiver); + } + + /** + * @notice Lets allowlisted addresses claim ERC1155 airdrop tokens. + * @dev The token-owner should approve tokenIds and total airdrop amounts to this contract, + * and set merkle root of allowlisted address for this airdrop. + * + * @param _token Address of ERC1155 airdrop token + * @param _receiver Allowlisted address for which the token is being claimed + * @param _tokenId Allowlisted tokenId to claim + * @param _quantity Allowlisted quantity of tokens to claim + * @param _proofs Merkle proofs for allowlist verification + */ + function claimERC1155( + address _token, + address _receiver, + uint256 _tokenId, + uint256 _quantity, + bytes32[] calldata _proofs + ) external { + bytes32 claimHash = _getClaimHashERC1155(_receiver, _token, _tokenId); + uint256 conditionId = tokenConditionId[_token]; + + if (claimed[conditionId][claimHash]) { + revert AirdropAlreadyClaimed(); + } + + bytes32 _tokenMerkleRoot = tokenMerkleRoot[_token]; + if (_tokenMerkleRoot == bytes32(0)) { + revert AirdropNoMerkleRoot(); + } + + bool valid = MerkleProofLib.verifyCalldata( + _proofs, + _tokenMerkleRoot, + keccak256(abi.encodePacked(_receiver, _tokenId, _quantity)) + ); + if (!valid) { + revert AirdropInvalidProof(); + } + + claimed[conditionId][claimHash] = true; + + IERC1155(_token).safeTransferFrom(owner(), _receiver, _tokenId, _quantity, ""); + + emit AirdropClaimed(_token, _receiver); + } + + /*/////////////////////////////////////////////////////////////// + Setter functions + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Lets contract owner set merkle root (allowlist) for claim based airdrops. + * + * @param _token Address of airdrop token + * @param _tokenMerkleRoot Merkle root of allowlist + * @param _resetClaimStatus Reset claim status / amount claimed so far to zero for all recipients + */ + function setMerkleRoot(address _token, bytes32 _tokenMerkleRoot, bool _resetClaimStatus) external onlyOwner { + if (_resetClaimStatus || tokenConditionId[_token] == 0) { + tokenConditionId[_token] += 1; + } + tokenMerkleRoot[_token] = _tokenMerkleRoot; + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + /// @notice Returns claim status of a receiver for a claim based airdrop + function isClaimed(address _receiver, address _token, uint256 _tokenId) external view returns (bool) { + uint256 _conditionId = tokenConditionId[_token]; + + bytes32 claimHash = keccak256(abi.encodePacked(_receiver, _token, _tokenId)); + if (claimed[_conditionId][claimHash]) { + return true; + } + + claimHash = keccak256(abi.encodePacked(_receiver, _token)); + if (claimed[_conditionId][claimHash]) { + return true; + } + + return false; + } + /// @dev Checks whether contract owner can be set in the given execution context. + function _canSetOwner() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Checks whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Domain name and version for EIP-712 + function _domainNameAndVersion() internal pure override returns (string memory name, string memory version) { + name = "Airdrop"; + version = "1"; + } + + /// @dev Keccak256 hash of receiver and token addresses, for claim based airdrop status tracking + function _getClaimHashERC20(address _receiver, address _token) private view returns (bytes32) { + return keccak256(abi.encodePacked(_receiver, _token)); + } + + /// @dev Keccak256 hash of receiver, token address and tokenId, for claim based airdrop status tracking + function _getClaimHashERC721(address _receiver, address _token, uint256 _tokenId) private view returns (bytes32) { + return keccak256(abi.encodePacked(_receiver, _token, _tokenId)); + } + + /// @dev Keccak256 hash of receiver, token address and tokenId, for claim based airdrop status tracking + function _getClaimHashERC1155(address _receiver, address _token, uint256 _tokenId) private view returns (bytes32) { + return keccak256(abi.encodePacked(_receiver, _token, _tokenId)); + } + + /// @dev Hash nested struct within AirdropRequest___ + function _hashContentInfoERC20(AirdropContentERC20[] calldata contents) private pure returns (bytes32) { + bytes32[] memory contentHashes = new bytes32[](contents.length); + for (uint256 i = 0; i < contents.length; i++) { + contentHashes[i] = keccak256(abi.encode(CONTENT_TYPEHASH_ERC20, contents[i].recipient, contents[i].amount)); + } + return keccak256(abi.encodePacked(contentHashes)); + } + + /// @dev Hash nested struct within AirdropRequest___ + function _hashContentInfoERC721(AirdropContentERC721[] calldata contents) private pure returns (bytes32) { + bytes32[] memory contentHashes = new bytes32[](contents.length); + for (uint256 i = 0; i < contents.length; i++) { + contentHashes[i] = keccak256( + abi.encode(CONTENT_TYPEHASH_ERC721, contents[i].recipient, contents[i].tokenId) + ); + } + return keccak256(abi.encodePacked(contentHashes)); + } + + /// @dev Hash nested struct within AirdropRequest___ + function _hashContentInfoERC1155(AirdropContentERC1155[] calldata contents) private pure returns (bytes32) { + bytes32[] memory contentHashes = new bytes32[](contents.length); + for (uint256 i = 0; i < contents.length; i++) { + contentHashes[i] = keccak256( + abi.encode(CONTENT_TYPEHASH_ERC1155, contents[i].recipient, contents[i].tokenId, contents[i].amount) + ); + } + return keccak256(abi.encodePacked(contentHashes)); + } + + /// @dev Verify EIP-712 signature + function _verifyRequestSignerERC20( + AirdropRequestERC20 calldata req, + bytes calldata signature + ) private view returns (bool) { + bytes32 contentHash = _hashContentInfoERC20(req.contents); + bytes32 structHash = keccak256( + abi.encode(REQUEST_TYPEHASH_ERC20, req.uid, req.tokenAddress, req.expirationTimestamp, contentHash) + ); + + bytes32 digest = _hashTypedData(structHash); + + return SignatureCheckerLib.isValidSignatureNowCalldata(owner(), digest, signature); + } + + /// @dev Verify EIP-712 signature + function _verifyRequestSignerERC721( + AirdropRequestERC721 calldata req, + bytes calldata signature + ) private view returns (bool) { + bytes32 contentHash = _hashContentInfoERC721(req.contents); + bytes32 structHash = keccak256( + abi.encode(REQUEST_TYPEHASH_ERC721, req.uid, req.tokenAddress, req.expirationTimestamp, contentHash) + ); + + bytes32 digest = _hashTypedData(structHash); + + return SignatureCheckerLib.isValidSignatureNowCalldata(owner(), digest, signature); + } + + /// @dev Verify EIP-712 signature + function _verifyRequestSignerERC1155( + AirdropRequestERC1155 calldata req, + bytes calldata signature + ) private view returns (bool) { + bytes32 contentHash = _hashContentInfoERC1155(req.contents); + bytes32 structHash = keccak256( + abi.encode(REQUEST_TYPEHASH_ERC1155, req.uid, req.tokenAddress, req.expirationTimestamp, contentHash) + ); + + bytes32 digest = _hashTypedData(structHash); + + return SignatureCheckerLib.isValidSignatureNowCalldata(owner(), digest, signature); + } +} diff --git a/contracts/prebuilts/drop/DropERC1155.sol b/contracts/prebuilts/drop/DropERC1155.sol new file mode 100644 index 000000000..b6673c632 --- /dev/null +++ b/contracts/prebuilts/drop/DropERC1155.sol @@ -0,0 +1,390 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// ========== External imports ========== + +import "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol"; + +import "../../extension/Multicall.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; + +// ========== Internal imports ========== + +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; +import "../../lib/CurrencyTransferLib.sol"; + +// ========== Features ========== + +import "../../extension/ContractMetadata.sol"; +import "../../extension/PlatformFee.sol"; +import "../../extension/Royalty.sol"; +import "../../extension/PrimarySale.sol"; +import "../../extension/Ownable.sol"; +import "../../extension/LazyMint.sol"; +import "../../extension/PermissionsEnumerable.sol"; +import "../../extension/Drop1155.sol"; + +contract DropERC1155 is + Initializable, + ContractMetadata, + PlatformFee, + Royalty, + PrimarySale, + Ownable, + LazyMint, + PermissionsEnumerable, + Drop1155, + ERC2771ContextUpgradeable, + Multicall, + ERC1155Upgradeable +{ + using StringsUpgradeable for uint256; + + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + // Token name + string public name; + + // Token symbol + string public symbol; + + /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. + bytes32 private transferRole; + /// @dev Only MINTER_ROLE holders can sign off on `MintRequest`s and lazy mint tokens. + bytes32 private minterRole; + /// @dev Only METADATA_ROLE holders can reveal the URI for a batch of delayed reveal NFTs, and update or freeze batch metadata. + bytes32 private metadataRole; + + /// @dev Max bps in the thirdweb system. + uint256 private constant MAX_BPS = 10_000; + + address public constant DEFAULT_FEE_RECIPIENT = 0x1Af20C6B23373350aD464700B5965CE4B0D2aD94; + uint16 private constant DEFAULT_FEE_BPS = 100; + + /*/////////////////////////////////////////////////////////////// + Mappings + //////////////////////////////////////////////////////////////*/ + + /// @dev Mapping from token ID => total circulating supply of tokens with that ID. + mapping(uint256 => uint256) public totalSupply; + + /// @dev Mapping from token ID => maximum possible total circulating supply of tokens with that ID. + mapping(uint256 => uint256) public maxTotalSupply; + + /// @dev Mapping from token ID => the address of the recipient of primary sales. + mapping(uint256 => address) public saleRecipient; + + /*/////////////////////////////////////////////////////////////// + Events + //////////////////////////////////////////////////////////////*/ + + /// @dev Emitted when the global max supply of a token is updated. + event MaxTotalSupplyUpdated(uint256 tokenId, uint256 maxTotalSupply); + + /// @dev Emitted when the sale recipient for a particular tokenId is updated. + event SaleRecipientForTokenUpdated(uint256 indexed tokenId, address saleRecipient); + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + constructor() initializer {} + + /// @dev Initializes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _royaltyRecipient, + uint128 _royaltyBps, + uint128 _platformFeeBps, + address _platformFeeRecipient + ) external initializer { + bytes32 _transferRole = keccak256("TRANSFER_ROLE"); + bytes32 _minterRole = keccak256("MINTER_ROLE"); + bytes32 _metadataRole = keccak256("METADATA_ROLE"); + + // Initialize inherited contracts, most base-like -> most derived. + __ERC2771Context_init(_trustedForwarders); + __ERC1155_init_unchained(""); + + // Initialize this contract's state. + _setupContractURI(_contractURI); + _setupOwner(_defaultAdmin); + + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + _setupRole(_minterRole, _defaultAdmin); + _setupRole(_transferRole, _defaultAdmin); + _setupRole(_transferRole, address(0)); + _setupRole(_metadataRole, _defaultAdmin); + _setRoleAdmin(_metadataRole, _metadataRole); + + _setupPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + _setupPrimarySaleRecipient(_saleRecipient); + + transferRole = _transferRole; + minterRole = _minterRole; + metadataRole = _metadataRole; + name = _name; + symbol = _symbol; + } + + /*/////////////////////////////////////////////////////////////// + ERC 165 / 1155 / 2981 logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the uri for a given tokenId. + function uri(uint256 _tokenId) public view override returns (string memory) { + string memory batchUri = _getBaseURI(_tokenId); + return string(abi.encodePacked(batchUri, _tokenId.toString())); + } + + /// @dev See ERC 165 + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC1155Upgradeable, IERC165) returns (bool) { + return super.supportsInterface(interfaceId) || type(IERC2981Upgradeable).interfaceId == interfaceId; + } + + /*/////////////////////////////////////////////////////////////// + Contract identifiers + //////////////////////////////////////////////////////////////*/ + + function contractType() external pure returns (bytes32) { + return bytes32("DropERC1155"); + } + + function contractVersion() external pure returns (uint8) { + return uint8(4); + } + + /*/////////////////////////////////////////////////////////////// + Setter functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Lets a module admin set a max total supply for token. + function setMaxTotalSupply(uint256 _tokenId, uint256 _maxTotalSupply) external onlyRole(DEFAULT_ADMIN_ROLE) { + maxTotalSupply[_tokenId] = _maxTotalSupply; + emit MaxTotalSupplyUpdated(_tokenId, _maxTotalSupply); + } + + /// @dev Lets a contract admin set the recipient for all primary sales. + function setSaleRecipientForToken(uint256 _tokenId, address _saleRecipient) external onlyRole(DEFAULT_ADMIN_ROLE) { + saleRecipient[_tokenId] = _saleRecipient; + emit SaleRecipientForTokenUpdated(_tokenId, _saleRecipient); + } + + /** + * @notice Updates the base URI for a batch of tokens. + * + * @param _index Index of the desired batch in batchIds array. + * @param _uri the new base URI for the batch. + */ + function updateBatchBaseURI(uint256 _index, string calldata _uri) external onlyRole(metadataRole) { + uint256 batchId = getBatchIdAtIndex(_index); + _setBaseURI(batchId, _uri); + } + + /** + * @notice Freezes the base URI for a batch of tokens. + * + * @param _index Index of the desired batch in batchIds array. + */ + function freezeBatchBaseURI(uint256 _index) external onlyRole(metadataRole) { + uint256 batchId = getBatchIdAtIndex(_index); + _freezeBaseURI(batchId); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Runs before every `claim` function call. + function _beforeClaim( + uint256 _tokenId, + address, + uint256 _quantity, + address, + uint256, + AllowlistProof calldata, + bytes memory + ) internal view override { + require( + maxTotalSupply[_tokenId] == 0 || totalSupply[_tokenId] + _quantity <= maxTotalSupply[_tokenId], + "exceed max total supply" + ); + } + + /// @dev Collects and distributes the primary sale value of NFTs being claimed. + function collectPriceOnClaim( + uint256 _tokenId, + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override { + if (_pricePerToken == 0) { + require(msg.value == 0, "!V"); + return; + } + + (address platformFeeRecipient, uint16 platformFeeBps) = getPlatformFeeInfo(); + + address _saleRecipient = _primarySaleRecipient == address(0) + ? (saleRecipient[_tokenId] == address(0) ? primarySaleRecipient() : saleRecipient[_tokenId]) + : _primarySaleRecipient; + + uint256 totalPrice = _quantityToClaim * _pricePerToken; + uint256 platformFeesTw = (totalPrice * DEFAULT_FEE_BPS) / MAX_BPS; + uint256 platformFees = (totalPrice * platformFeeBps) / MAX_BPS; + + bool validMsgValue; + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { + validMsgValue = msg.value == totalPrice; + } else { + validMsgValue = msg.value == 0; + } + require(validMsgValue, "!V"); + + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), DEFAULT_FEE_RECIPIENT, platformFeesTw); + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), platformFeeRecipient, platformFees); + CurrencyTransferLib.transferCurrency( + _currency, + _msgSender(), + _saleRecipient, + totalPrice - platformFees - platformFeesTw + ); + } + + /// @dev Transfers the NFTs being claimed. + function transferTokensOnClaim(address _to, uint256 _tokenId, uint256 _quantityBeingClaimed) internal override { + _mint(_to, _tokenId, _quantityBeingClaimed, ""); + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function _canSetPlatformFeeInfo() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether primary sale recipient can be set in the given execution context. + function _canSetPrimarySaleRecipient() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether owner can be set in the given execution context. + function _canSetOwner() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether royalty info can be set in the given execution context. + function _canSetRoyaltyInfo() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function _canSetClaimConditions() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Returns whether lazy minting can be done in the given execution context. + function _canLazyMint() internal view virtual override returns (bool) { + return hasRole(minterRole, _msgSender()); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + /// @dev The tokenId of the next NFT that will be minted / lazy minted. + function nextTokenIdToMint() external view returns (uint256) { + return nextTokenIdToLazyMint; + } + + /// @dev Lets a token owner burn multiple tokens they own at once (i.e. destroy for good) + function burnBatch(address account, uint256[] memory ids, uint256[] memory values) public virtual { + require( + account == _msgSender() || isApprovedForAll(account, _msgSender()), + "ERC1155: caller is not owner nor approved." + ); + + _burnBatch(account, ids, values); + } + + /** + * @dev See {ERC1155-_beforeTokenTransfer}. + */ + function _beforeTokenTransfer( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal virtual override { + super._beforeTokenTransfer(operator, from, to, ids, amounts, data); + + // if transfer is restricted on the contract, we still want to allow burning and minting + if (!hasRole(transferRole, address(0)) && from != address(0) && to != address(0)) { + require(hasRole(transferRole, from) || hasRole(transferRole, to), "restricted to TRANSFER_ROLE holders."); + } + + if (from == address(0)) { + for (uint256 i = 0; i < ids.length; ++i) { + totalSupply[ids[i]] += amounts[i]; + } + } + + if (to == address(0)) { + for (uint256 i = 0; i < ids.length; ++i) { + totalSupply[ids[i]] -= amounts[i]; + } + } + } + + function _dropMsgSender() internal view virtual override returns (address) { + return _msgSender(); + } + + function _msgSender() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } + + function _msgData() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable) + returns (bytes calldata) + { + return ERC2771ContextUpgradeable._msgData(); + } +} diff --git a/contracts/prebuilts/drop/DropERC20.sol b/contracts/prebuilts/drop/DropERC20.sol new file mode 100644 index 000000000..8cd1bc555 --- /dev/null +++ b/contracts/prebuilts/drop/DropERC20.sol @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// ========== External imports ========== +import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20VotesUpgradeable.sol"; + +import "../../extension/Multicall.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; + +// ========== Internal imports ========== + +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; +import "../../lib/CurrencyTransferLib.sol"; + +// ========== Features ========== + +import "../../extension/ContractMetadata.sol"; +import "../../extension/PlatformFee.sol"; +import "../../extension/PrimarySale.sol"; +import "../../extension/PermissionsEnumerable.sol"; +import "../../extension/Drop.sol"; + +contract DropERC20 is + Initializable, + ContractMetadata, + PlatformFee, + PrimarySale, + PermissionsEnumerable, + Drop, + ERC2771ContextUpgradeable, + Multicall, + ERC20BurnableUpgradeable, + ERC20VotesUpgradeable +{ + using StringsUpgradeable for uint256; + + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. + bytes32 private transferRole; + + /// @dev Max bps in the thirdweb system. + uint256 private constant MAX_BPS = 10_000; + + address public constant DEFAULT_FEE_RECIPIENT = 0x1Af20C6B23373350aD464700B5965CE4B0D2aD94; + uint16 private constant DEFAULT_FEE_BPS = 100; + + /// @dev Global max total supply of tokens. + uint256 public maxTotalSupply; + + /// @dev Emitted when the global max supply of tokens is updated. + event MaxTotalSupplyUpdated(uint256 maxTotalSupply); + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + constructor() initializer {} + + /// @dev Initializes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _platformFeeRecipient, + uint128 _platformFeeBps + ) external initializer { + bytes32 _transferRole = keccak256("TRANSFER_ROLE"); + + // Initialize inherited contracts, most base-like -> most derived. + __ERC2771Context_init(_trustedForwarders); + __ERC20Permit_init(_name); + __ERC20_init_unchained(_name, _symbol); + + _setupContractURI(_contractURI); + + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + _setupRole(_transferRole, _defaultAdmin); + _setupRole(_transferRole, address(0)); + + _setupPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + _setupPrimarySaleRecipient(_saleRecipient); + + transferRole = _transferRole; + } + + /*/////////////////////////////////////////////////////////////// + Contract identifiers + //////////////////////////////////////////////////////////////*/ + + function contractType() external pure returns (bytes32) { + return bytes32("DropERC20"); + } + + function contractVersion() external pure returns (uint8) { + return uint8(4); + } + + /*/////////////////////////////////////////////////////////////// + Setter functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Lets a contract admin set the global maximum supply for collection's NFTs. + function setMaxTotalSupply(uint256 _maxTotalSupply) external onlyRole(DEFAULT_ADMIN_ROLE) { + maxTotalSupply = _maxTotalSupply; + emit MaxTotalSupplyUpdated(_maxTotalSupply); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Runs before every `claim` function call. + function _beforeClaim( + address, + uint256 _quantity, + address, + uint256, + AllowlistProof calldata, + bytes memory + ) internal view override { + uint256 _maxTotalSupply = maxTotalSupply; + require(_maxTotalSupply == 0 || totalSupply() + _quantity <= _maxTotalSupply, "exceed max total supply."); + } + + /// @dev Collects and distributes the primary sale value of tokens being claimed. + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override { + if (_pricePerToken == 0) { + require(msg.value == 0, "!Value"); + return; + } + + (address platformFeeRecipient, uint16 platformFeeBps) = getPlatformFeeInfo(); + + address saleRecipient = _primarySaleRecipient == address(0) ? primarySaleRecipient() : _primarySaleRecipient; + + // `_pricePerToken` is interpreted as price per 1 ether unit of the ERC20 tokens. + uint256 totalPrice = (_quantityToClaim * _pricePerToken) / 1 ether; + require(totalPrice > 0, "quantity too low"); + + uint256 platformFeesTw = (totalPrice * DEFAULT_FEE_BPS) / MAX_BPS; + uint256 platformFees = (totalPrice * platformFeeBps) / MAX_BPS; + + bool validMsgValue; + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { + validMsgValue = msg.value == totalPrice; + } else { + validMsgValue = msg.value == 0; + } + require(validMsgValue, "Invalid msg value"); + + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), DEFAULT_FEE_RECIPIENT, platformFeesTw); + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), platformFeeRecipient, platformFees); + CurrencyTransferLib.transferCurrency( + _currency, + _msgSender(), + saleRecipient, + totalPrice - platformFees - platformFeesTw + ); + } + + /// @dev Transfers the tokens being claimed. + function _transferTokensOnClaim(address _to, uint256 _quantityBeingClaimed) internal override returns (uint256) { + _mint(_to, _quantityBeingClaimed); + return 0; + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function _canSetPlatformFeeInfo() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether primary sale recipient can be set in the given execution context. + function _canSetPrimarySaleRecipient() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function _canSetClaimConditions() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function _mint(address account, uint256 amount) internal virtual override(ERC20Upgradeable, ERC20VotesUpgradeable) { + super._mint(account, amount); + } + + function _burn(address account, uint256 amount) internal virtual override(ERC20Upgradeable, ERC20VotesUpgradeable) { + super._burn(account, amount); + } + + function _afterTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual override(ERC20Upgradeable, ERC20VotesUpgradeable) { + super._afterTokenTransfer(from, to, amount); + } + + /// @dev Runs on every transfer. + function _beforeTokenTransfer(address from, address to, uint256 amount) internal override(ERC20Upgradeable) { + super._beforeTokenTransfer(from, to, amount); + + if (!hasRole(transferRole, address(0)) && from != address(0) && to != address(0)) { + require(hasRole(transferRole, from) || hasRole(transferRole, to), "transfers restricted."); + } + } + + function _dropMsgSender() internal view virtual override returns (address) { + return _msgSender(); + } + + function _msgSender() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } + + function _msgData() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable) + returns (bytes calldata) + { + return ERC2771ContextUpgradeable._msgData(); + } +} diff --git a/contracts/prebuilts/drop/DropERC721.sol b/contracts/prebuilts/drop/DropERC721.sol new file mode 100644 index 000000000..f7bbb7e79 --- /dev/null +++ b/contracts/prebuilts/drop/DropERC721.sol @@ -0,0 +1,399 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// ========== External imports ========== + +import "../../extension/Multicall.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; + +import "../../eip/ERC721AVirtualApproveUpgradeable.sol"; + +// ========== Internal imports ========== + +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; +import "../../lib/CurrencyTransferLib.sol"; + +// ========== Features ========== + +import "../../extension/ContractMetadata.sol"; +import "../../extension/PlatformFee.sol"; +import "../../extension/Royalty.sol"; +import "../../extension/PrimarySale.sol"; +import "../../extension/Ownable.sol"; +import "../../extension/DelayedReveal.sol"; +import "../../extension/LazyMint.sol"; +import "../../extension/PermissionsEnumerable.sol"; +import "../../extension/Drop.sol"; + +contract DropERC721 is + Initializable, + ContractMetadata, + PlatformFee, + Royalty, + PrimarySale, + Ownable, + DelayedReveal, + LazyMint, + PermissionsEnumerable, + Drop, + ERC2771ContextUpgradeable, + Multicall, + ERC721AUpgradeable +{ + using StringsUpgradeable for uint256; + + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. + bytes32 private transferRole; + /// @dev Only MINTER_ROLE holders can sign off on `MintRequest`s and lazy mint tokens. + bytes32 private minterRole; + /// @dev Only METADATA_ROLE holders can reveal the URI for a batch of delayed reveal NFTs, and update or freeze batch metadata. + bytes32 private metadataRole; + + /// @dev Max bps in the thirdweb system. + uint256 private constant MAX_BPS = 10_000; + + address public constant DEFAULT_FEE_RECIPIENT = 0x1Af20C6B23373350aD464700B5965CE4B0D2aD94; + uint16 private constant DEFAULT_FEE_BPS = 100; + + /// @dev Global max total supply of NFTs. + uint256 public maxTotalSupply; + + /// @dev Emitted when the global max supply of tokens is updated. + event MaxTotalSupplyUpdated(uint256 maxTotalSupply); + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + constructor() initializer {} + + /// @dev Initializes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _royaltyRecipient, + uint128 _royaltyBps, + uint128 _platformFeeBps, + address _platformFeeRecipient + ) external initializer { + bytes32 _transferRole = keccak256("TRANSFER_ROLE"); + bytes32 _minterRole = keccak256("MINTER_ROLE"); + bytes32 _metadataRole = keccak256("METADATA_ROLE"); + + // Initialize inherited contracts, most base-like -> most derived. + __ERC2771Context_init(_trustedForwarders); + __ERC721A_init(_name, _symbol); + + _setupContractURI(_contractURI); + _setupOwner(_defaultAdmin); + + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + _setupRole(_minterRole, _defaultAdmin); + _setupRole(_transferRole, _defaultAdmin); + _setupRole(_transferRole, address(0)); + _setupRole(_metadataRole, _defaultAdmin); + _setRoleAdmin(_metadataRole, _metadataRole); + + _setupPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + _setupPrimarySaleRecipient(_saleRecipient); + + transferRole = _transferRole; + minterRole = _minterRole; + metadataRole = _metadataRole; + } + + /*/////////////////////////////////////////////////////////////// + ERC 165 / 721 / 2981 logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the URI for a given tokenId. + function tokenURI(uint256 _tokenId) public view override returns (string memory) { + (uint256 batchId, ) = _getBatchId(_tokenId); + string memory batchUri = _getBaseURI(_tokenId); + + if (isEncryptedBatch(batchId)) { + return string(abi.encodePacked(batchUri, "0")); + } else { + return string(abi.encodePacked(batchUri, _tokenId.toString())); + } + } + + /// @dev See ERC 165 + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC721AUpgradeable, IERC165) returns (bool) { + return super.supportsInterface(interfaceId) || type(IERC2981Upgradeable).interfaceId == interfaceId; + } + + /*/////////////////////////////////////////////////////////////// + Contract identifiers + //////////////////////////////////////////////////////////////*/ + + function contractType() external pure returns (bytes32) { + return bytes32("DropERC721"); + } + + function contractVersion() external pure returns (uint8) { + return uint8(4); + } + + /*/////////////////////////////////////////////////////////////// + Lazy minting + delayed-reveal logic + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Lets an account with `MINTER_ROLE` lazy mint 'n' NFTs. + * The URIs for each token is the provided `_baseURIForTokens` + `{tokenId}`. + */ + function lazyMint( + uint256 _amount, + string calldata _baseURIForTokens, + bytes calldata _data + ) public override returns (uint256 batchId) { + if (_data.length > 0) { + (bytes memory encryptedURI, bytes32 provenanceHash) = abi.decode(_data, (bytes, bytes32)); + if (encryptedURI.length != 0 && provenanceHash != "") { + _setEncryptedData(nextTokenIdToLazyMint + _amount, _data); + } + } + + return super.lazyMint(_amount, _baseURIForTokens, _data); + } + + /// @dev Lets an account with `METADATA_ROLE` reveal the URI for a batch of 'delayed-reveal' NFTs. + /// @param _index the ID of a token with the desired batch. + /// @param _key the key to decrypt the batch's URI. + function reveal( + uint256 _index, + bytes calldata _key + ) external onlyRole(metadataRole) returns (string memory revealedURI) { + uint256 batchId = getBatchIdAtIndex(_index); + revealedURI = getRevealURI(batchId, _key); + + _setEncryptedData(batchId, ""); + _setBaseURI(batchId, revealedURI); + + emit TokenURIRevealed(_index, revealedURI); + } + + /** + * @notice Updates the base URI for a batch of tokens. Can only be called if the batch has been revealed/is not encrypted. + * + * @param _index Index of the desired batch in batchIds array + * @param _uri the new base URI for the batch. + */ + function updateBatchBaseURI(uint256 _index, string calldata _uri) external onlyRole(metadataRole) { + require(!isEncryptedBatch(getBatchIdAtIndex(_index)), "Encrypted batch"); + uint256 batchId = getBatchIdAtIndex(_index); + _setBaseURI(batchId, _uri); + } + + /** + * @notice Freezes the base URI for a batch of tokens. + * + * @param _index Index of the desired batch in batchIds array. + */ + function freezeBatchBaseURI(uint256 _index) external onlyRole(metadataRole) { + require(!isEncryptedBatch(getBatchIdAtIndex(_index)), "Encrypted batch"); + uint256 batchId = getBatchIdAtIndex(_index); + _freezeBaseURI(batchId); + } + + /*/////////////////////////////////////////////////////////////// + Setter functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Lets a contract admin set the global maximum supply for collection's NFTs. + function setMaxTotalSupply(uint256 _maxTotalSupply) external onlyRole(DEFAULT_ADMIN_ROLE) { + maxTotalSupply = _maxTotalSupply; + emit MaxTotalSupplyUpdated(_maxTotalSupply); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Runs before every `claim` function call. + function _beforeClaim( + address, + uint256 _quantity, + address, + uint256, + AllowlistProof calldata, + bytes memory + ) internal view override { + require(_currentIndex + _quantity <= nextTokenIdToLazyMint, "!Tokens"); + require(maxTotalSupply == 0 || _currentIndex + _quantity <= maxTotalSupply, "!Supply"); + } + + /// @dev Collects and distributes the primary sale value of NFTs being claimed. + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override { + if (_pricePerToken == 0) { + require(msg.value == 0, "!V"); + return; + } + + (address platformFeeRecipient, uint16 platformFeeBps) = getPlatformFeeInfo(); + + address saleRecipient = _primarySaleRecipient == address(0) ? primarySaleRecipient() : _primarySaleRecipient; + + uint256 totalPrice = _quantityToClaim * _pricePerToken; + uint256 platformFeesTw = (totalPrice * DEFAULT_FEE_BPS) / MAX_BPS; + uint256 platformFees = (totalPrice * platformFeeBps) / MAX_BPS; + + bool validMsgValue; + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { + validMsgValue = msg.value == totalPrice; + } else { + validMsgValue = msg.value == 0; + } + require(validMsgValue, "!V"); + + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), DEFAULT_FEE_RECIPIENT, platformFeesTw); + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), platformFeeRecipient, platformFees); + CurrencyTransferLib.transferCurrency( + _currency, + _msgSender(), + saleRecipient, + totalPrice - platformFees - platformFeesTw + ); + } + + /// @dev Transfers the NFTs being claimed. + function _transferTokensOnClaim( + address _to, + uint256 _quantityBeingClaimed + ) internal override returns (uint256 startTokenId) { + startTokenId = _currentIndex; + _safeMint(_to, _quantityBeingClaimed); + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function _canSetPlatformFeeInfo() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether primary sale recipient can be set in the given execution context. + function _canSetPrimarySaleRecipient() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether owner can be set in the given execution context. + function _canSetOwner() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether royalty info can be set in the given execution context. + function _canSetRoyaltyInfo() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function _canSetClaimConditions() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Returns whether lazy minting can be done in the given execution context. + function _canLazyMint() internal view virtual override returns (bool) { + return hasRole(minterRole, _msgSender()); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + /** + * Returns the total amount of tokens minted in the contract. + */ + function totalMinted() external view returns (uint256) { + return _totalMinted(); + } + + /// @dev The tokenId of the next NFT that will be minted / lazy minted. + function nextTokenIdToMint() external view returns (uint256) { + return nextTokenIdToLazyMint; + } + + /// @dev The next token ID of the NFT that can be claimed. + function nextTokenIdToClaim() external view returns (uint256) { + return _currentIndex; + } + + /// @dev Burns `tokenId`. See {ERC721-_burn}. + function burn(uint256 tokenId) external virtual { + // note: ERC721AUpgradeable's `_burn(uint256,bool)` internally checks for token approvals. + _burn(tokenId, true); + } + + /// @dev See {ERC721-_beforeTokenTransfer}. + function _beforeTokenTransfers( + address from, + address to, + uint256 startTokenId, + uint256 quantity + ) internal virtual override { + super._beforeTokenTransfers(from, to, startTokenId, quantity); + + // if transfer is restricted on the contract, we still want to allow burning and minting + if (!hasRole(transferRole, address(0)) && from != address(0) && to != address(0)) { + if (!hasRole(transferRole, from) && !hasRole(transferRole, to)) { + revert("!Transfer-Role"); + } + } + } + + function _dropMsgSender() internal view virtual override returns (address) { + return _msgSender(); + } + + function _msgSender() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } + + function _msgData() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable) + returns (bytes calldata) + { + return ERC2771ContextUpgradeable._msgData(); + } +} diff --git a/contracts/prebuilts/drop/drop.md b/contracts/prebuilts/drop/drop.md new file mode 100644 index 000000000..a4de5e77a --- /dev/null +++ b/contracts/prebuilts/drop/drop.md @@ -0,0 +1,176 @@ +# Drop design document. + +This is a live document that explains what the [thirdweb](https://thirdweb.com/) `Drop` smart contracts are, how they work and can be used, and why they are written the way they are. + +The document is written for technical and non-technical readers. To ask further questions about any of thirdweb’s `Drop`, please join the [thirdweb discord](https://discord.gg/thirdweb) or create a github issue. + +--- + +## Background + +The thirdweb `Drop` contracts are distribution mechanisms for tokens. This distribution mechanism is offered for ERC20, ERC721 and ERC1155 tokens, as `DropERC20`, `DropERC721` and `DropERC1155`. + +The `Drop` contracts are meant to be used when the goal of the contract creator is for an audience to come in and claim tokens within certain restrictions e.g. — ‘only addresses in an allowlist can mint tokens’, or ‘minters must pay _x_ amount of price in _y_ currency to mint’, etc. + +The `Drop` contracts let the contract creator establish phases (periods of time), where each phase can specify multiple such restrictions on the minting of tokens during that period of time. We refer to such a phase as a ‘claim condition’. + +### Why we’re building `Drop` + +We’ve observed that there are largely three distinct contexts under which one mints tokens — + +1. Minting tokens for yourself on a contract you own. E.g. a person wants to mint their Twitter profile picture as an NFT. +2. Having an audience mint tokens on a contract you own. + 1. The nature of tokens to be minted by the audience is pre-determined by the contract admin. E.g. a 10k NFT drop where the contents of the NFTs to be minted by the audience is already known and determined by the contract admin before the audience comes in to mint NFTs. + 2. The nature of tokens to be minted by the audience is _not_ pre-determined by the contract admin. E.g. a course ‘certificate’ dynamically generated with the name of the course participant, to be minted by the course participant at the time of course completion. + +The thirdweb `Token` contracts serve the cases described in (1) and 2(ii). + +The thirdweb `Drop` contracts serve the case described in 2(i). They are written to give a contract creator granular control over restrictions around an audience minting tokens from the same contract (or ‘collection’, in the case of NFTs) over an extended period of time. + +## Technical Details + +The distribution mechanism of `Drop` is as follows — A contract admin establishes a series of ‘claim conditions’. A ‘claim condition’ is a period of time in which accounts can mint tokens on the respective `Drop` contract, within a set of restrictions defined by the ‘claim condition’. + +### Claim Conditions + +The following makes up a claim condition — + +```solidity +struct ClaimCondition { + uint256 startTimestamp; + uint256 maxClaimableSupply; + uint256 supplyClaimed; + uint256 quantityLimitPerWallet; + bytes32 merkleRoot; + uint256 pricePerToken; + address currency; +} + +``` + +| Parameters | Type | Description | +| ---------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| startTimestamp | uint256 | The unix timestamp after which the claim condition applies. The same claim condition applies until the startTimestamp of the next claim condition. | +| maxClaimableSupply | uint256 | The maximum total number of tokens that can be claimed under the claim condition. | +| supplyClaimed | uint256 | At any given point, the number of tokens that have been claimed under the claim condition. | +| quantityLimitPerWallet | uint256 | The maximum number of tokens that can be claimed by a wallet under a given claim condition. | +| merkleRoot | bytes32 | The allowlist of addresses that can claim tokens under the claim condition. | + +(Optional) The allowlist may specify quantity limits, price and currency for addresses in the list, overriding these values under that claim condition. + +The parameters that make up a claim condition can be composed in different ways to create specific restrictions around a mint. For example, a single claim condition where: + +- `quantityLimitPerWallet = 5` +- `merkleRoot = bytes32(0)` + +creates restrictions around a mint, where (1) a wallet can mint at most 5 tokens and (2) all wallets are subject to general claim condition limits, without any overrides. + +A `Drop` contract lets a contract admin establish a series of claim conditions, at once. Since each claim condition specifies a `startTime`, a contract admin can establish a series of claim conditions, ordered by their start time, to specify different set of restrictions around minting, during different periods of time. + +At any moment, there is only one active claim condition, and an account attempting to mint tokens on the respective `Drop` contract successfully or unsuccessfully, based on whether the account passes the restrictions defined by that moment’s active claim condition. + +A `Drop` contract natively keeps track of claim conditions set by a contract admin in a ‘claim conditions list’, which looks as follows — + +```solidity +struct ClaimConditionList { + uint256 currentStartId; + uint256 count; + mapping(uint256 => ClaimCondition) conditions; + mapping(uint256 => mapping(address => uint256)) supplyClaimedByWallet; +} + +``` + +| Parameter | Description | +| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| currentStartId | The uid for the first claim condition amongst the current set of claim conditions. The uid for each next claim condition is one more than the previous claim condition's uid. | +| count | The total number of claim conditions in the list of claim conditions. | +| conditions | The claim conditions at a given uid. Claim conditions are ordered in an ascending order by their startTimestamp. | +| supplyClaimedByWallet | Map from a claim condition uid and account to the supply claimed by that account. | + +### Allowlist as an override list + +As mentioned above, an allowlist can specify different conditions for addresses in the list. This way, it serves as an override over general/open restrictions for non-allowlisted addresses. +In this allowlist or override-list, an admin can set any/all of these three: + +- quantity limit +- price +- currency + +If a value is not set for any of these, then the value specified in general claim condition will be used. However, currency override will be considered only when a price override is set too, and not without it. + +> **IMPORTANT**: _The allowlist should not contain an address more than once in the same merkle tree. Multiple instances for the same address (with different/same quantity, price etc.) may not function as expected and may lead to unexpected behavior during claim. (More details in Limitations section)._ + +### Setting claim conditions + +In all `Drop` contracts, a contract admin specifies the following when setting claim conditions: + +| Parameter | Type | Description | +| --------------------- | ---------------- | ---------------------------------------------------------------------------------- | +| conditions | ClaimCondition[] | Claim conditions in ascending order by `startTimestamp`. | +| resetClaimEligibility | bool | Whether to reset `supplyClaimedByWallet` values when setting new claim conditions. | + +When setting claim conditions, any existing set of claim conditions stored in `ClaimConditionsList` are overwritten with the new claim conditions specified in `conditions`. + +The claim conditions specified in `conditions` are expected to be in ordered in ascending order, by their ‘start time’. As a result, only one claim condition is active during at any given time. + +Each of the claim conditions specified in `conditions` is assigned a unique integer ID. The UID of the first condition in `conditions` is stored as the `ClaimConditionList.currentStartId` and each next claim condition’s UID is one more than the previous condition’s UID. + +![claim-conditions-diagram-1.png](/assets/claim-conditions-diagram-1.png) + +The `resetClaimEligibility` boolean flag determines what UIDs are assigned to the claim conditions specified in `conditions`. Since `ClaimConditionList.supplyClaimedByWallet` is indexed by the UID of claim conditions, this gives a contract admin more granular control over the restrictions that claim conditions can express. We now illustrate this with an example: + +Let’s say an existing claim condition **C1** specifies the following restrictions: + +- `quantityLimitPerWallet = 1` +- `merkleRoot = bytes32(0)` +- `pricePerToken = 0.1 ether` +- `currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` (i.e. native token of the chain e.g ether for Ethereum mainnet) + +At a high level, **C1** expresses the following restrictions on minting — any address can claim at most one token, ever, by paying 0.1 ether in price. + +Let’s say the contract admin wants to increase the price per token from 0.1 ether to 0.2 ether, while ensuring that wallets that have already claimed tokens are not able to claim tokens again. Essentially, the contract admin now wants to instantiate a claim condition **C2** with the following restrictions: + +- `quantityLimitPerWallet = 1` +- `merkleRoot = bytes32(0)` +- `pricePerToken = 0.2 ether` +- `currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` (i.e. native token of the chain e.g ether for Ethereum mainnet) + +To go from **C1** to **C2** while ensuring that wallets that have already claimed tokens are not able to claim tokens again, the contract admin will set claim conditions while specifying `resetClaimEligibility == false`. As a result, the **C2** will be assigned the same UID as **C1**. Since `ClaimConditionList.supplyClaimedByWallet` is indexed by the UID of claim conditions, the information of the quantity of tokens claimed by the wallet during **C1** will not be lost. And so, wallets that claimed tokens during **C1** will now be ineligible to claim tokens during **C2** because of the following check: + +```solidity +// pseudo-code +supplyClaimedByWallet = claimCondition.supplyClaimedByWallet[conditionId][claimer]; + +require(quantityToClaim + supplyClaimedByWallet <= quantityLimitPerWallet); +``` + +### EIPs supported / implemented + +The distribution mechanism for tokens expressed by thirdweb’s `Drop` is implemented for ERC20, ERC721 and ERC1155 tokens, as `DropERC20`, `DropERC721` and `DropERC1155`. + +There are a few key differences between the three implementations — + +- `DropERC20` is written for the distribution of completely fungible, ERC20 tokens. On the other hand, `DropERC721` and `DropERC1155` are written for the distribution of NFTs, which requires ‘lazy minting’ i.e. defining the content of the NFTs before an audience comes in to mint them during a claim condition. +- Both `DropERC20` and `DropERC721` maintain a global, contract-wide `ClaimConditionsList` which stores the claim conditions under which tokens can be minted. The `DropERC1155` contract, on the other hand, maintains a `ClaimConditionList` for every integer token ID that an NFT can assume. And so, a contract admin can set up claim conditions per NFT i.e. per token ID, in the `DropERC1155` contract. + +## Limitations + +### Sybil attacks + +The distribution mechanism of thirdweb’s `Drop` contracts is vulnerable to [sybil attacks](https://en.wikipedia.org/wiki/Sybil_attack). That is, despite the various ways in which restrictions can be applied to the minting of tokens, some restrictions that claim conditions can express target wallets and not persons. + +For example, the restriction `quantityLimitPerWallet` expresses the max quantity a _wallet_ can claim during the respective claim condition. A sophisticated actor may generate multiple wallets to claim tokens in a way that undermines such restrictions, when viewing such restrictions as restrictions on unique persons, and not wallets. + +### Allowlist behavior + +When specifying allowlist of addresses, and quantities, price, etc. for those addresses, contract admins must ensure that they don't list an address more than once in the same merkle tree. + +For e.g. admin wishes to grant user X permission to mint 2 tokens at 0.25 ETH, and 4 tokens at 0.5 ETH. In this case, the contract design will not permit the user X to claim 6 tokens with different prices as desired. Instead, the user may be limited to claiming just 2 tokens or 4 tokens based on their order of claiming. + +To avoid such pitfalls, an address should be listed only once per merkle tree or allowlist. + +## Authors + +- [nkrishang](https://github.com/nkrishang) +- [thirdweb team](https://github.com/thirdweb-dev) diff --git a/contracts/prebuilts/evolving-nfts/EvolvingNFT.sol b/contracts/prebuilts/evolving-nfts/EvolvingNFT.sol new file mode 100644 index 000000000..0f73209b2 --- /dev/null +++ b/contracts/prebuilts/evolving-nfts/EvolvingNFT.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +import "@thirdweb-dev/dynamic-contracts/src/presets/BaseRouter.sol"; + +import "../../extension/Multicall.sol"; +import "../../extension/upgradeable/Initializable.sol"; +import "../../extension/upgradeable/init/ContractMetadataInit.sol"; +import "../../extension/upgradeable/init/RoyaltyInit.sol"; +import "../../extension/upgradeable/init/PrimarySaleInit.sol"; +import "../../extension/upgradeable/init/OwnableInit.sol"; +import "../../extension/upgradeable/init/PermissionsEnumerableInit.sol"; +import "../../extension/upgradeable/init/ERC2771ContextInit.sol"; +import "../../extension/upgradeable/init/ERC721AQueryableInit.sol"; + +contract EvolvingNFT is + Initializable, + BaseRouter, + Multicall, + ERC721AQueryableInit, + ERC2771ContextInit, + ContractMetadataInit, + RoyaltyInit, + PrimarySaleInit, + OwnableInit, + PermissionsEnumerableInit +{ + /// @dev Only EXTENSION_ROLE holders can update the contract's extensions. + bytes32 private constant EXTENSION_ROLE = keccak256("EXTENSION_ROLE"); + + constructor(Extension[] memory _extensions) BaseRouter(_extensions) { + _disableInitializers(); + } + + /// @dev Initializes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _royaltyRecipient, + uint128 _royaltyBps + ) external initializer initializerERC721A { + bytes32 _transferRole = keccak256("TRANSFER_ROLE"); + + // Initialize extensions + __BaseRouter_init(); + + // Initialize inherited contracts, most base-like -> most derived. + __ERC2771Context_init(_trustedForwarders); + __ERC721A_init(_name, _symbol); + + _setupContractURI(_contractURI); + _setupOwner(_defaultAdmin); + + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + _setupRole(EXTENSION_ROLE, _defaultAdmin); + _setupRole(keccak256("MINTER_ROLE"), _defaultAdmin); + _setupRole(_transferRole, _defaultAdmin); + _setupRole(_transferRole, address(0)); + + _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + _setupPrimarySaleRecipient(_saleRecipient); + + _setupRole(EXTENSION_ROLE, _defaultAdmin); + _setRoleAdmin(EXTENSION_ROLE, EXTENSION_ROLE); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev The start token ID for the contract. + function _startTokenId() internal pure override returns (uint256) { + return 1; + } + + /// @dev Returns whether all relevant permission and other checks are met before any upgrade. + function _isAuthorizedCallToUpgrade() internal view virtual override returns (bool) { + return _hasRole(EXTENSION_ROLE, msg.sender); + } + + /// @dev Checks whether an account has a particular role. + function _hasRole(bytes32 _role, address _account) internal view returns (bool) { + PermissionsStorage.Data storage data = PermissionsStorage.data(); + return data._hasRole[_role][_account]; + } +} diff --git a/contracts/prebuilts/evolving-nfts/EvolvingNFTLogic.sol b/contracts/prebuilts/evolving-nfts/EvolvingNFTLogic.sol new file mode 100644 index 000000000..f7c645d40 --- /dev/null +++ b/contracts/prebuilts/evolving-nfts/EvolvingNFTLogic.sol @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// ========== External imports ========== + +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; + +import "../../eip/queryable/ERC721AQueryableUpgradeable.sol"; + +// ========== Internal imports ========== + +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; +import { CurrencyTransferLib } from "../../lib/CurrencyTransferLib.sol"; + +// ========== Features ========== + +import "../../extension/Multicall.sol"; +import "../../extension/upgradeable/ContractMetadata.sol"; +import "../../extension/upgradeable/Royalty.sol"; +import "../../extension/upgradeable/PrimarySale.sol"; +import "../../extension/upgradeable/Ownable.sol"; +import "../../extension/upgradeable/Permissions.sol"; +import "../../extension/upgradeable/Drop.sol"; +import "../../extension/upgradeable/SharedMetadataBatch.sol"; +import { RulesEngine } from "../../extension/upgradeable/RulesEngine.sol"; + +contract EvolvingNFTLogic is + ContractMetadata, + Royalty, + PrimarySale, + Ownable, + SharedMetadataBatch, + Drop, + ERC2771ContextUpgradeable, + ERC721AQueryableUpgradeable +{ + using StringsUpgradeable for uint256; + using EnumerableSet for EnumerableSet.Bytes32Set; + + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + /// @dev Default admin role for all roles. Only accounts with this role can grant/revoke other roles. + bytes32 private constant DEFAULT_ADMIN_ROLE = 0x00; + /// @dev Only TRANSFER_ROLE holders can have tokens transferred from or to them, during restricted transfers. + bytes32 private constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE"); + /// @dev Only MINTER_ROLE holders can sign off on `MintRequest`s. + bytes32 private constant MINTER_ROLE = keccak256("MINTER_ROLE"); + + /*/////////////////////////////////////////////////////////////// + ERC 165 / 721 / 2981 logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the URI for a given tokenId. + function tokenURI( + uint256 _tokenId + ) public view virtual override(ERC721AUpgradeable, IERC721AUpgradeable) returns (string memory) { + if (!_exists(_tokenId)) { + revert("!ID"); + } + + // Get score + address owner = ownerOf(_tokenId); + uint256 score = 0; + + address engine = RulesEngine(address(this)).getRulesEngineOverride(); + if (engine != address(0)) { + score = RulesEngine(engine).getScore(owner); + } else { + score = RulesEngine(address(this)).getScore(owner); + } + + // Get the target ID i.e. `start` of the range that the score falls into. + bytes32[] memory ids = _sharedMetadataBatchStorage().ids.values(); + bytes32 targetId = 0; + uint256 check = 0; + + for (uint256 i = 0; i < ids.length; i += 1) { + uint256 id = uint256(ids[i]); + + if (id <= score && id >= check) { + targetId = ids[i]; + check = id; + } + } + return _getURIFromSharedMetadata(targetId, _tokenId); + } + + /// @dev See ERC 165 + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC721AUpgradeable, IERC721AUpgradeable, IERC165) returns (bool) { + return super.supportsInterface(interfaceId) || type(IERC2981Upgradeable).interfaceId == interfaceId; + } + + /// @dev The start token ID for the contract. + function _startTokenId() internal pure override returns (uint256) { + return 1; + } + + function startTokenId() public pure returns (uint256) { + return _startTokenId(); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Collects and distributes the primary sale value of NFTs being claimed. + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override { + if (_pricePerToken == 0) { + require(msg.value == 0, "!Value"); + return; + } + + uint256 totalPrice = _quantityToClaim * _pricePerToken; + + bool validMsgValue; + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { + validMsgValue = msg.value == totalPrice; + } else { + validMsgValue = msg.value == 0; + } + require(validMsgValue, "!V"); + + address saleRecipient = _primarySaleRecipient == address(0) ? primarySaleRecipient() : _primarySaleRecipient; + + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), saleRecipient, totalPrice); + } + + /// @dev Transfers the NFTs being claimed. + function _transferTokensOnClaim( + address _to, + uint256 _quantityBeingClaimed + ) internal override returns (uint256 startTokenId_) { + startTokenId_ = _nextTokenId(); + _safeMint(_to, _quantityBeingClaimed); + } + + /// @dev Checks whether primary sale recipient can be set in the given execution context. + function _canSetPrimarySaleRecipient() internal view override returns (bool) { + return _hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether owner can be set in the given execution context. + function _canSetOwner() internal view override returns (bool) { + return _hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether royalty info can be set in the given execution context. + function _canSetRoyaltyInfo() internal view override returns (bool) { + return _hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view override returns (bool) { + return _hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function _canSetClaimConditions() internal view override returns (bool) { + return _hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Returns whether shared metadata can be set in the given execution context. + function _canSetSharedMetadata() internal view virtual override returns (bool) { + return _hasRole(MINTER_ROLE, _msgSender()); + } + + /// @dev Checks whether an account has a particular role. + function _hasRole(bytes32 _role, address _account) internal view returns (bool) { + PermissionsStorage.Data storage data = PermissionsStorage.data(); + return data._hasRole[_role][_account]; + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + /** + * Returns the total amount of tokens minted in the contract. + */ + function totalMinted() external view returns (uint256) { + unchecked { + return _nextTokenId() - _startTokenId(); + } + } + + /// @dev The tokenId of the next NFT that will be minted / lazy minted. + function nextTokenIdToMint() external view returns (uint256) { + return _nextTokenId(); + } + + /// @dev The next token ID of the NFT that can be claimed. + function nextTokenIdToClaim() external view returns (uint256) { + return _nextTokenId(); + } + + /// @dev Burns `tokenId`. See {ERC721-_burn}. + function burn(uint256 tokenId) external virtual { + // note: ERC721AUpgradeable's `_burn(uint256,bool)` internally checks for token approvals. + _burn(tokenId, true); + } + + /// @dev See {ERC721-_beforeTokenTransfer}. + function _beforeTokenTransfers( + address from, + address to, + uint256 startTokenId_, + uint256 quantity + ) internal virtual override { + super._beforeTokenTransfers(from, to, startTokenId_, quantity); + + // if transfer is restricted on the contract, we still want to allow burning and minting + if (!_hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { + if (!_hasRole(TRANSFER_ROLE, from) && !_hasRole(TRANSFER_ROLE, to)) { + revert("!T"); + } + } + } + + function _dropMsgSender() internal view virtual override returns (address) { + return _msgSender(); + } + + function _msgSender() internal view virtual override returns (address sender) { + return ERC2771ContextUpgradeable._msgSender(); + } + + function _msgData() internal view virtual override returns (bytes calldata) { + return ERC2771ContextUpgradeable._msgData(); + } +} diff --git a/contracts/prebuilts/evolving-nfts/extension/RulesEngineExtension.sol b/contracts/prebuilts/evolving-nfts/extension/RulesEngineExtension.sol new file mode 100644 index 000000000..40e7abde4 --- /dev/null +++ b/contracts/prebuilts/evolving-nfts/extension/RulesEngineExtension.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +import { PermissionsStorage } from "../../../extension/upgradeable/Permissions.sol"; +import { RulesEngine } from "../../../extension/upgradeable/RulesEngine.sol"; + +contract RulesEngineExtension is RulesEngine { + /// @dev Returns whether the rules of the contract can be set in the given execution context. + function _canSetRules() internal view virtual override returns (bool) { + return _hasRole(keccak256("MINTER_ROLE"), msg.sender); + } + + /// @dev Returns whether the rules engine used by the contract can be overriden in the given execution context. + function _canOverrideRulesEngine() internal view virtual override returns (bool) { + // DEFAULT_ADMIN_ROLE + return _hasRole(0x00, msg.sender); + } + + /// @dev Checks whether an account has a particular role. + function _hasRole(bytes32 _role, address _account) internal view returns (bool) { + PermissionsStorage.Data storage data = PermissionsStorage.data(); + return data._hasRole[_role][_account]; + } +} diff --git a/contracts/prebuilts/interface/ILoyaltyCard.sol b/contracts/prebuilts/interface/ILoyaltyCard.sol new file mode 100644 index 000000000..52a9fdc9b --- /dev/null +++ b/contracts/prebuilts/interface/ILoyaltyCard.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../extension/interface/INFTMetadata.sol"; +import "../../extension/interface/ISignatureMintERC721.sol"; +import "../../eip/interface/IERC721.sol"; + +interface ILoyaltyCard { + /// @dev Emitted when an account with MINTER_ROLE mints an NFT. + event TokensMinted(address indexed mintedTo, uint256 indexed tokenIdMinted, string uri); + + /** + * @notice Lets an account with MINTER_ROLE mint an NFT. + * + * @param to The address to mint the NFT to. + * @param uri The URI to assign to the NFT. + * + * @return tokenId of the NFT minted. + */ + function mintTo(address to, string calldata uri) external returns (uint256); + + /// @notice Let's a loyalty card owner or approved operator cancel the loyalty card. + function cancel(uint256 tokenId) external; + + /// @notice Let's an approved party cancel the loyalty card (no approval needed). + function revoke(uint256 tokenId) external; +} diff --git a/contracts/prebuilts/interface/ILoyaltyPoints.sol b/contracts/prebuilts/interface/ILoyaltyPoints.sol new file mode 100644 index 000000000..47c7e92c9 --- /dev/null +++ b/contracts/prebuilts/interface/ILoyaltyPoints.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +interface ILoyaltyPoints { + /// @dev Emitted when an account with MINTER_ROLE mints an NFT. + event TokensMinted(address indexed mintedTo, uint256 quantityMinted); + + /// @notice Returns the total tokens minted to `owner` in the contract's lifetime. + function getTotalMintedInLifetime(address owner) external view returns (uint256); + + /** + * @notice Lets an account with MINTER_ROLE mint an NFT. + * + * @param to The address to mint tokens to. + * @param amount The amount of tokens to mint. + */ + function mintTo(address to, uint256 amount) external; + + /// @notice Let's a loyalty pointsß owner or approved operator cancel the given amount of loyalty points. + function cancel(address owner, uint256 amount) external; + + /// @notice Let's an approved party revoke a holder's loyalty points (no approval needed). + function revoke(address owner, uint256 amount) external; +} diff --git a/contracts/interfaces/IMultiwrap.sol b/contracts/prebuilts/interface/IMultiwrap.sol similarity index 96% rename from contracts/interfaces/IMultiwrap.sol rename to contracts/prebuilts/interface/IMultiwrap.sol index c72c7084b..16c6ef008 100644 --- a/contracts/interfaces/IMultiwrap.sol +++ b/contracts/prebuilts/interface/IMultiwrap.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.11; -import "../extension/interface/ITokenBundle.sol"; +import "../../extension/interface/ITokenBundle.sol"; /** * Thirdweb's Multiwrap contract lets you wrap arbitrary ERC20, ERC721 and ERC1155 diff --git a/contracts/interfaces/IPack.sol b/contracts/prebuilts/interface/IPack.sol similarity index 87% rename from contracts/interfaces/IPack.sol rename to contracts/prebuilts/interface/IPack.sol index 0035d3619..f28cac3f8 100644 --- a/contracts/interfaces/IPack.sol +++ b/contracts/prebuilts/interface/IPack.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.11; -import "../extension/interface/ITokenBundle.sol"; +import "../../extension/interface/ITokenBundle.sol"; /** * The thirdweb `Pack` contract is a lootbox mechanism. An account can bundle up arbitrary ERC20, ERC721 and ERC1155 tokens into @@ -24,12 +24,10 @@ interface IPack is ITokenBundle { } /// @notice Emitted when a set of packs is created. - event PackCreated( - uint256 indexed packId, - address indexed packCreator, - address recipient, - uint256 totalPacksCreated - ); + event PackCreated(uint256 indexed packId, address recipient, uint256 totalPacksCreated); + + /// @notice Emitted when more packs are minted for a packId. + event PackUpdated(uint256 indexed packId, address recipient, uint256 totalPacksCreated); /// @notice Emitted when a pack is opened. event PackOpened( @@ -49,7 +47,7 @@ interface IPack is ITokenBundle { * @param amountDistributedPerOpen The number of reward units distributed per open. * @param recipient The recipient of the packs created. * - * @return packId The unique identifer of the created set of packs. + * @return packId The unique identifier of the created set of packs. * @return packTotalSupply The total number of packs created. */ function createPack( diff --git a/contracts/prebuilts/interface/IPackVRFDirect.sol b/contracts/prebuilts/interface/IPackVRFDirect.sol new file mode 100644 index 000000000..116c9ec30 --- /dev/null +++ b/contracts/prebuilts/interface/IPackVRFDirect.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../extension/interface/ITokenBundle.sol"; + +/** + * The thirdweb `Pack` contract is a lootbox mechanism. An account can bundle up arbitrary ERC20, ERC721 and ERC1155 tokens into + * a set of packs. A pack can then be opened in return for a selection of the tokens in the pack. The selection of tokens distributed + * on opening a pack depends on the relative supply of all tokens in the packs. + */ + +interface IPackVRFDirect is ITokenBundle { + /** + * @notice All info relevant to packs. + * + * @param perUnitAmounts Mapping from a UID -> to the per-unit amount of that asset i.e. `Token` at that index. + * @param openStartTimestamp The timestamp after which packs can be opened. + * @param amountDistributedPerOpen The number of reward units distributed per open. + */ + struct PackInfo { + uint256[] perUnitAmounts; + uint128 openStartTimestamp; + uint128 amountDistributedPerOpen; + } + + /// @notice Emitted when a set of packs is created. + event PackCreated(uint256 indexed packId, address recipient, uint256 totalPacksCreated); + + /// @notice Emitted when the opening of a pack is requested. + event PackOpenRequested(address indexed opener, uint256 indexed packId, uint256 amountToOpen, uint256 requestId); + + /// @notice Emitted when Chainlink VRF fulfills a random number request. + event PackRandomnessFulfilled(uint256 indexed packId, uint256 indexed requestId); + + /// @notice Emitted when a pack is opened. + event PackOpened( + uint256 indexed packId, + address indexed opener, + uint256 numOfPacksOpened, + Token[] rewardUnitsDistributed + ); + + /** + * @notice Creates a pack with the stated contents. + * + * @param contents The reward units to pack in the packs. + * @param numOfRewardUnits The number of reward units to create, for each asset specified in `contents`. + * @param packUri The (metadata) URI assigned to the packs created. + * @param openStartTimestamp The timestamp after which packs can be opened. + * @param amountDistributedPerOpen The number of reward units distributed per open. + * @param recipient The recipient of the packs created. + * + * @return packId The unique identifier of the created set of packs. + * @return packTotalSupply The total number of packs created. + */ + function createPack( + Token[] calldata contents, + uint256[] calldata numOfRewardUnits, + string calldata packUri, + uint128 openStartTimestamp, + uint128 amountDistributedPerOpen, + address recipient + ) external payable returns (uint256 packId, uint256 packTotalSupply); + + /** + * @notice Lets a pack owner request to open a pack. + * + * @param packId The identifier of the pack to open. + * @param amountToOpen The number of packs to open at once. + */ + function openPack(uint256 packId, uint256 amountToOpen) external returns (uint256 requestId); + + /// @notice Called by a pack opener to claim rewards from the opened pack. + function claimRewards() external returns (Token[] memory rewardUnits); + + /// @notice Called by a pack opener to open a pack in a single transaction, instead of calling openPack and claimRewards separately. + function openPackAndClaimRewards( + uint256 _packId, + uint256 _amountToOpen, + uint32 _callBackGasLimit + ) external returns (uint256); + + /// @notice Returns whether a pack opener is ready to call `claimRewards`. + function canClaimRewards(address _opener) external view returns (bool); +} diff --git a/contracts/prebuilts/interface/airdrop/IAirdropERC1155.sol b/contracts/prebuilts/interface/airdrop/IAirdropERC1155.sol new file mode 100644 index 000000000..7350e1029 --- /dev/null +++ b/contracts/prebuilts/interface/airdrop/IAirdropERC1155.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/** + * Thirdweb's `Airdrop` contracts provide a lightweight and easy to use mechanism + * to drop tokens. + * + * `AirdropERC1155` contract is an airdrop contract for ERC1155 tokens. It follows a + * push mechanism for transfer of tokens to intended recipients. + */ + +interface IAirdropERC1155 { + /// @notice Emitted when an airdrop fails for a recipient address. + event AirdropFailed( + address indexed tokenAddress, + address indexed tokenOwner, + address indexed recipient, + uint256 tokenId, + uint256 amount + ); + + /** + * @notice Details of amount and recipient for airdropped token. + * + * @param recipient The recipient of the tokens. + * @param tokenId ID of the ERC1155 token being airdropped. + * @param amount The quantity of tokens to airdrop. + */ + struct AirdropContent { + address recipient; + uint256 tokenId; + uint256 amount; + } + + /** + * @notice Lets contract-owner send ERC1155 tokens to a list of addresses. + * @dev The token-owner should approve target tokens to Airdrop contract, + * which acts as operator for the tokens. + * + * @param tokenAddress The contract address of the tokens to transfer. + * @param tokenOwner The owner of the tokens to transfer. + * @param contents List containing recipient, tokenId to airdrop. + */ + function airdropERC1155(address tokenAddress, address tokenOwner, AirdropContent[] calldata contents) external; +} diff --git a/contracts/prebuilts/interface/airdrop/IAirdropERC1155Claimable.sol b/contracts/prebuilts/interface/airdrop/IAirdropERC1155Claimable.sol new file mode 100644 index 000000000..9fb65233b --- /dev/null +++ b/contracts/prebuilts/interface/airdrop/IAirdropERC1155Claimable.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/** + * Thirdweb's `Airdrop` contracts provide a lightweight and easy to use mechanism + * to drop tokens. + * + * `AirdropERC1155Claimable` contract is an airdrop contract for ERC1155 tokens. It follows a + * pull mechanism for transfer of tokens, where allowlisted recipients can claim tokens from + * the contract. + */ + +interface IAirdropERC1155Claimable { + /// @dev Emitted when tokens are claimed. + event TokensClaimed( + address indexed claimer, + address indexed receiver, + uint256 indexed tokenId, + uint256 quantityClaimed + ); + + /** + * @notice Lets an account claim a given quantity of ERC1155 tokens. + * + * @param receiver The receiver of the tokens to claim. + * @param quantity The quantity of tokens to claim. + * @param tokenId Token Id to claim. + * @param proofs The proof of the claimer's inclusion in the merkle root allowlist + * of the claim conditions that apply. + * @param proofMaxQuantityForWallet The maximum number of tokens an address included in an + * allowlist can claim. + */ + function claim( + address receiver, + uint256 quantity, + uint256 tokenId, + bytes32[] calldata proofs, + uint256 proofMaxQuantityForWallet + ) external; +} diff --git a/contracts/prebuilts/interface/airdrop/IAirdropERC20.sol b/contracts/prebuilts/interface/airdrop/IAirdropERC20.sol new file mode 100644 index 000000000..86964d74d --- /dev/null +++ b/contracts/prebuilts/interface/airdrop/IAirdropERC20.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/** + * Thirdweb's `Airdrop` contracts provide a lightweight and easy to use mechanism + * to drop tokens. + * + * `AirdropERC20` contract is an airdrop contract for ERC20 tokens. It follows a + * push mechanism for transfer of tokens to intended recipients. + */ + +interface IAirdropERC20 { + /// @notice Emitted when an airdrop fails for a recipient address. + event AirdropFailed( + address indexed tokenAddress, + address indexed tokenOwner, + address indexed recipient, + uint256 amount + ); + + /** + * @notice Details of amount and recipient for airdropped token. + * + * @param recipient The recipient of the tokens. + * @param amount The quantity of tokens to airdrop. + */ + struct AirdropContent { + address recipient; + uint256 amount; + } + + /** + * @notice Lets contract-owner send ERC20 tokens to a list of addresses. + * @dev The token-owner should approve target tokens to Airdrop contract, + * which acts as operator for the tokens. + * + * @param tokenAddress The contract address of the tokens to transfer. + * @param tokenOwner The owner of the tokens to transfer. + * @param contents List containing recipient, tokenId to airdrop. + */ + function airdropERC20( + address tokenAddress, + address tokenOwner, + AirdropContent[] calldata contents + ) external payable; +} diff --git a/contracts/prebuilts/interface/airdrop/IAirdropERC20Claimable.sol b/contracts/prebuilts/interface/airdrop/IAirdropERC20Claimable.sol new file mode 100644 index 000000000..e238910fa --- /dev/null +++ b/contracts/prebuilts/interface/airdrop/IAirdropERC20Claimable.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/** + * Thirdweb's `Airdrop` contracts provide a lightweight and easy to use mechanism + * to drop tokens. + * + * `AirdropERC20Claimable` contract is an airdrop contract for ERC20 tokens. It follows a + * pull mechanism for transfer of tokens, where allowlisted recipients can claim tokens from + * the contract. + */ + +interface IAirdropERC20Claimable { + /// @dev Emitted when tokens are claimed. + event TokensClaimed(address indexed claimer, address indexed receiver, uint256 quantityClaimed); + + /** + * @notice Lets an account claim a given quantity of NFTs. + * + * @param receiver The receiver of the NFTs to claim. + * @param quantity The quantity of NFTs to claim. + * @param proofs The proof of the claimer's inclusion in the merkle root allowlist + * of the claim conditions that apply. + * @param proofMaxQuantityForWallet The maximum number of NFTs an address included in an + * allowlist can claim. + */ + function claim( + address receiver, + uint256 quantity, + bytes32[] calldata proofs, + uint256 proofMaxQuantityForWallet + ) external; +} diff --git a/contracts/prebuilts/interface/airdrop/IAirdropERC721.sol b/contracts/prebuilts/interface/airdrop/IAirdropERC721.sol new file mode 100644 index 000000000..3d94d422c --- /dev/null +++ b/contracts/prebuilts/interface/airdrop/IAirdropERC721.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/** + * Thirdweb's `Airdrop` contracts provide a lightweight and easy to use mechanism + * to drop tokens. + * + * `AirdropERC721` contract is an airdrop contract for ERC721 tokens. It follows a + * push mechanism for transfer of tokens to intended recipients. + */ + +interface IAirdropERC721 { + /// @notice Emitted when an airdrop fails for a recipient address. + event AirdropFailed( + address indexed tokenAddress, + address indexed tokenOwner, + address indexed recipient, + uint256 tokenId + ); + + /** + * @notice Details of amount and recipient for airdropped token. + * + * @param recipient The recipient of the tokens. + * @param tokenId ID of the ERC721 token being airdropped. + */ + struct AirdropContent { + address recipient; + uint256 tokenId; + } + + /** + * @notice Lets contract-owner send ERC721 tokens to a list of addresses. + * @dev The token-owner should approve target tokens to Airdrop contract, + * which acts as operator for the tokens. + * + * @param tokenAddress The contract address of the tokens to transfer. + * @param tokenOwner The owner of the tokens to transfer. + * @param contents List containing recipient, tokenId to airdrop. + */ + function airdropERC721(address tokenAddress, address tokenOwner, AirdropContent[] calldata contents) external; +} diff --git a/contracts/prebuilts/interface/airdrop/IAirdropERC721Claimable.sol b/contracts/prebuilts/interface/airdrop/IAirdropERC721Claimable.sol new file mode 100644 index 000000000..4ca9aefd1 --- /dev/null +++ b/contracts/prebuilts/interface/airdrop/IAirdropERC721Claimable.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/** + * Thirdweb's `Airdrop` contracts provide a lightweight and easy to use mechanism + * to drop tokens. + * + * `AirdropERC721Claimable` contract is an airdrop contract for ERC721 tokens. It follows a + * pull mechanism for transfer of tokens, where allowlisted recipients can claim tokens from + * the contract. + */ + +interface IAirdropERC721Claimable { + /// @dev Emitted when tokens are claimed. + event TokensClaimed(address indexed claimer, address indexed receiver, uint256 quantityClaimed); + + /** + * @notice Lets an account claim a given quantity of NFTs. + * + * @param receiver The receiver of the NFTs to claim. + * @param quantity The quantity of NFTs to claim. + * @param proofs The proof of the claimer's inclusion in the merkle root allowlist + * of the claim conditions that apply. + * @param proofMaxQuantityForWallet The maximum number of NFTs an address included in an + * allowlist can claim. + */ + function claim( + address receiver, + uint256 quantity, + bytes32[] calldata proofs, + uint256 proofMaxQuantityForWallet + ) external; +} diff --git a/contracts/interfaces/drop/IDropClaimCondition.sol b/contracts/prebuilts/interface/drop/IDropClaimCondition.sol similarity index 91% rename from contracts/interfaces/drop/IDropClaimCondition.sol rename to contracts/prebuilts/interface/drop/IDropClaimCondition.sol index fe9409747..02c3ac5cb 100644 --- a/contracts/interfaces/drop/IDropClaimCondition.sol +++ b/contracts/prebuilts/interface/drop/IDropClaimCondition.sol @@ -26,8 +26,7 @@ interface IDropClaimCondition { * @param supplyClaimed At any given point, the number of tokens that have been claimed * under the claim condition. * - * @param quantityLimitPerTransaction The maximum number of tokens that can be claimed in a single - * transaction. + * @param quantityLimitPerWallet The maximum number of tokens that can be claimed by a wallet. * * @param waitTimeInSecondsBetweenClaims The least number of seconds an account must wait after claiming * tokens, to be able to claim tokens again. @@ -43,7 +42,7 @@ interface IDropClaimCondition { uint256 startTimestamp; uint256 maxClaimableSupply; uint256 supplyClaimed; - uint256 quantityLimitPerTransaction; + uint256 quantityLimitPerWallet; uint256 waitTimeInSecondsBetweenClaims; bytes32 merkleRoot; uint256 pricePerToken; @@ -69,6 +68,8 @@ interface IDropClaimCondition { * * @param limitMerkleProofClaim Map from a claim condition uid to whether an address in an allowlist * has already claimed tokens i.e. used their place in the allowlist. + * + * @param supplyClaimedByWallet Map from a claim condition uid and account to supply claimed by account. */ struct ClaimConditionList { uint256 currentStartId; @@ -76,5 +77,6 @@ interface IDropClaimCondition { mapping(uint256 => ClaimCondition) phases; mapping(uint256 => mapping(address => uint256)) limitLastClaimTimestamp; mapping(uint256 => BitMapsUpgradeable.BitMap) limitMerkleProofClaim; + mapping(uint256 => mapping(address => uint256)) supplyClaimedByWallet; } } diff --git a/contracts/interfaces/drop/IDropERC1155.sol b/contracts/prebuilts/interface/drop/IDropERC1155.sol similarity index 78% rename from contracts/interfaces/drop/IDropERC1155.sol rename to contracts/prebuilts/interface/drop/IDropERC1155.sol index bbd443d56..2c3d4181a 100644 --- a/contracts/interfaces/drop/IDropERC1155.sol +++ b/contracts/prebuilts/interface/drop/IDropERC1155.sol @@ -22,6 +22,11 @@ import "./IDropClaimCondition.sol"; */ interface IDropERC1155 is IERC1155Upgradeable, IDropClaimCondition { + struct AllowlistProof { + bytes32[] proof; + uint256 maxQuantityInAllowlist; + } + /// @dev Emitted when tokens are claimed. event TokensClaimed( uint256 indexed claimConditionIndex, @@ -40,12 +45,6 @@ interface IDropERC1155 is IERC1155Upgradeable, IDropClaimCondition { /// @dev Emitted when the global max supply of a token is updated. event MaxTotalSupplyUpdated(uint256 tokenId, uint256 maxTotalSupply); - /// @dev Emitted when the wallet claim count for a given tokenId and address is updated. - event WalletClaimCountUpdated(uint256 tokenId, address indexed wallet, uint256 count); - - /// @dev Emitted when the max wallet claim count for a given tokenId is updated. - event MaxWalletClaimCountUpdated(uint256 tokenId, uint256 count); - /// @dev Emitted when the sale recipient for a particular tokenId is updated. event SaleRecipientForTokenUpdated(uint256 indexed tokenId, address saleRecipient); @@ -61,15 +60,14 @@ interface IDropERC1155 is IERC1155Upgradeable, IDropClaimCondition { /** * @notice Lets an account claim a given quantity of NFTs. * - * @param receiver The receiver of the NFTs to claim. - * @param tokenId The unique ID of the token to claim. - * @param quantity The quantity of NFTs to claim. + * @param receiver The receiver of the NFT to claim. + * @param tokenId The tokenId of the NFT to claim. + * @param quantity The quantity of the NFT to claim. * @param currency The currency in which to pay for the claim. * @param pricePerToken The price per token to pay for the claim. - * @param proofs The proof of the claimer's inclusion in the merkle root allowlist + * @param allowlistProof The proof of the claimer's inclusion in the merkle root allowlist * of the claim conditions that apply. - * @param proofMaxQuantityPerTransaction (Optional) The maximum number of NFTs an address included in an - * allowlist can claim. + * @param data Arbitrary bytes data that can be leveraged in the implementation of this interface. */ function claim( address receiver, @@ -77,8 +75,8 @@ interface IDropERC1155 is IERC1155Upgradeable, IDropClaimCondition { uint256 quantity, address currency, uint256 pricePerToken, - bytes32[] calldata proofs, - uint256 proofMaxQuantityPerTransaction + AllowlistProof calldata allowlistProof, + bytes memory data ) external payable; /** @@ -90,9 +88,5 @@ interface IDropERC1155 is IERC1155Upgradeable, IDropClaimCondition { * `limitMerkleProofClaim` values when setting new * claim conditions. */ - function setClaimConditions( - uint256 tokenId, - ClaimCondition[] calldata phases, - bool resetClaimEligibility - ) external; + function setClaimConditions(uint256 tokenId, ClaimCondition[] calldata phases, bool resetClaimEligibility) external; } diff --git a/contracts/prebuilts/interface/drop/IDropERC20.sol b/contracts/prebuilts/interface/drop/IDropERC20.sol new file mode 100644 index 000000000..d0bde6c7c --- /dev/null +++ b/contracts/prebuilts/interface/drop/IDropERC20.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "./IDropClaimCondition.sol"; + +/** + * Thirdweb's 'Drop' contracts are distribution mechanisms for tokens. The + * `DropERC20` contract is a distribution mechanism for ERC20 tokens. + * + * A contract admin (i.e. holder of `DEFAULT_ADMIN_ROLE`) can create claim conditions + * with non-overlapping time windows, and accounts can claim the tokens according to + * restrictions defined in the claim condition that is active at the time of the transaction. + */ + +interface IDropERC20 is IERC20Upgradeable, IDropClaimCondition { + struct AllowlistProof { + bytes32[] proof; + uint256 maxQuantityInAllowlist; + } + + /// @dev Emitted when tokens are claimed. + event TokensClaimed( + uint256 indexed claimConditionIndex, + address indexed claimer, + address indexed receiver, + uint256 quantityClaimed + ); + + /// @dev Emitted when new claim conditions are set. + event ClaimConditionsUpdated(ClaimCondition[] claimConditions); + + /// @dev Emitted when the global max supply of tokens is updated. + event MaxTotalSupplyUpdated(uint256 maxTotalSupply); + + /// @dev Emitted when the contract URI is updated. + event ContractURIUpdated(string prevURI, string newURI); + + /** + * @notice Lets an account claim a given quantity of NFTs. + * + * @param receiver The receiver of the tokens to claim. + * @param quantity The quantity of tokens to claim. + * @param currency The currency in which to pay for the claim. + * @param pricePerToken The price per token to pay for the claim. + * @param allowlistProof The proof of the claimer's inclusion in the merkle root allowlist + * of the claim conditions that apply. + * @param data Arbitrary bytes data that can be leveraged in the implementation of this interface. + */ + function claim( + address receiver, + uint256 quantity, + address currency, + uint256 pricePerToken, + AllowlistProof calldata allowlistProof, + bytes memory data + ) external payable; + + /** + * @notice Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. + * + * @param phases Claim conditions in ascending order by `startTimestamp`. + * @param resetClaimEligibility Whether to reset `limitLastClaimTimestamp` and + * `limitMerkleProofClaim` values when setting new + * claim conditions. + */ + function setClaimConditions(ClaimCondition[] calldata phases, bool resetClaimEligibility) external; +} diff --git a/contracts/interfaces/drop/IDropERC721.sol b/contracts/prebuilts/interface/drop/IDropERC721.sol similarity index 83% rename from contracts/interfaces/drop/IDropERC721.sol rename to contracts/prebuilts/interface/drop/IDropERC721.sol index bd1b89377..e115de49c 100644 --- a/contracts/interfaces/drop/IDropERC721.sol +++ b/contracts/prebuilts/interface/drop/IDropERC721.sol @@ -22,6 +22,11 @@ import "./IDropClaimCondition.sol"; */ interface IDropERC721 is IERC721Upgradeable, IDropClaimCondition { + struct AllowlistProof { + bytes32[] proof; + uint256 maxQuantityInAllowlist; + } + /// @dev Emitted when tokens are claimed. event TokensClaimed( uint256 indexed claimConditionIndex, @@ -43,12 +48,6 @@ interface IDropERC721 is IERC721Upgradeable, IDropClaimCondition { /// @dev Emitted when the global max supply of tokens is updated. event MaxTotalSupplyUpdated(uint256 maxTotalSupply); - /// @dev Emitted when the wallet claim count for an address is updated. - event WalletClaimCountUpdated(address indexed wallet, uint256 count); - - /// @dev Emitted when the global max wallet claim count is updated. - event MaxWalletClaimCountUpdated(uint256 count); - /** * @notice Lets an account with `MINTER_ROLE` lazy mint 'n' NFTs. * The URIs for each token is the provided `_baseURIForTokens` + `{tokenId}`. @@ -61,11 +60,7 @@ interface IDropERC721 is IERC721Upgradeable, IDropClaimCondition { * result of encrypting the URI of the NFTs in the revealed * state. */ - function lazyMint( - uint256 amount, - string calldata baseURIForTokens, - bytes calldata encryptedBaseURI - ) external; + function lazyMint(uint256 amount, string calldata baseURIForTokens, bytes calldata encryptedBaseURI) external; /** * @notice Lets an account claim a given quantity of NFTs. @@ -74,18 +69,17 @@ interface IDropERC721 is IERC721Upgradeable, IDropClaimCondition { * @param quantity The quantity of NFTs to claim. * @param currency The currency in which to pay for the claim. * @param pricePerToken The price per token to pay for the claim. - * @param proofs The proof of the claimer's inclusion in the merkle root allowlist + * @param allowlistProof The proof of the claimer's inclusion in the merkle root allowlist * of the claim conditions that apply. - * @param proofMaxQuantityPerTransaction (Optional) The maximum number of NFTs an address included in an - * allowlist can claim. + * @param data Arbitrary bytes data that can be leveraged in the implementation of this interface. */ function claim( address receiver, uint256 quantity, address currency, uint256 pricePerToken, - bytes32[] calldata proofs, - uint256 proofMaxQuantityPerTransaction + AllowlistProof calldata allowlistProof, + bytes memory data ) external payable; /** diff --git a/contracts/prebuilts/interface/marketplace/IMarketplace.sol b/contracts/prebuilts/interface/marketplace/IMarketplace.sol new file mode 100644 index 000000000..d485451d4 --- /dev/null +++ b/contracts/prebuilts/interface/marketplace/IMarketplace.sol @@ -0,0 +1,329 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../../infra/interface/IThirdwebContract.sol"; +import "../../../extension/interface/IPlatformFee.sol"; + +interface IMarketplace is IThirdwebContract, IPlatformFee { + /// @notice Type of the tokens that can be listed for sale. + enum TokenType { + ERC1155, + ERC721 + } + + /** + * @notice The two types of listings. + * `Direct`: NFTs listed for sale at a fixed price. + * `Auction`: NFTs listed for sale in an auction. + */ + enum ListingType { + Direct, + Auction + } + + /** + * @notice The information related to either (1) an offer on a direct listing, or (2) a bid in an auction. + * + * @dev The type of the listing at ID `lisingId` determines how the `Offer` is interpreted. + * If the listing is of type `Direct`, the `Offer` is interpreted as an offer to a direct listing. + * If the listing is of type `Auction`, the `Offer` is interpreted as a bid in an auction. + * + * @param listingId The uid of the listing the offer is made to. + * @param offeror The account making the offer. + * @param quantityWanted The quantity of tokens from the listing wanted by the offeror. + * This is the entire listing quantity if the listing is an auction. + * @param currency The currency in which the offer is made. + * @param pricePerToken The price per token offered to the lister. + * @param expirationTimestamp The timestamp after which a seller cannot accept this offer. + */ + struct Offer { + uint256 listingId; + address offeror; + uint256 quantityWanted; + address currency; + uint256 pricePerToken; + uint256 expirationTimestamp; + } + + /** + * @dev For use in `createListing` as a parameter type. + * + * @param assetContract The contract address of the NFT to list for sale. + + * @param tokenId The tokenId on `assetContract` of the NFT to list for sale. + + * @param startTime The unix timestamp after which the listing is active. For direct listings: + * 'active' means NFTs can be bought from the listing. For auctions, + * 'active' means bids can be made in the auction. + * + * @param secondsUntilEndTime No. of seconds after `startTime`, after which the listing is inactive. + * For direct listings: 'inactive' means NFTs cannot be bought from the listing. + * For auctions: 'inactive' means bids can no longer be made in the auction. + * + * @param quantityToList The quantity of NFT of ID `tokenId` on the given `assetContract` to list. For + * ERC 721 tokens to list for sale, the contract strictly defaults this to `1`, + * Regardless of the value of `quantityToList` passed. + * + * @param currencyToAccept For direct listings: the currency in which a buyer must pay the listing's fixed price + * to buy the NFT(s). For auctions: the currency in which the bidders must make bids. + * + * @param reservePricePerToken For direct listings: this value is ignored. For auctions: the minimum bid amount of + * the auction is `reservePricePerToken * quantityToList` + * + * @param buyoutPricePerToken For direct listings: interpreted as 'price per token' listed. For auctions: if + * `buyoutPricePerToken` is greater than 0, and a bidder's bid is at least as great as + * `buyoutPricePerToken * quantityToList`, the bidder wins the auction, and the auction + * is closed. + * + * @param listingType The type of listing to create - a direct listing or an auction. + **/ + struct ListingParameters { + address assetContract; + uint256 tokenId; + uint256 startTime; + uint256 secondsUntilEndTime; + uint256 quantityToList; + address currencyToAccept; + uint256 reservePricePerToken; + uint256 buyoutPricePerToken; + ListingType listingType; + } + + /** + * @notice The information related to a listing; either (1) a direct listing, or (2) an auction listing. + * + * @dev For direct listings: + * (1) `reservePricePerToken` is ignored. + * (2) `buyoutPricePerToken` is simply interpreted as 'price per token'. + * + * @param listingId The uid for the listing. + * + * @param tokenOwner The owner of the tokens listed for sale. + * + * @param assetContract The contract address of the NFT to list for sale. + + * @param tokenId The tokenId on `assetContract` of the NFT to list for sale. + + * @param startTime The unix timestamp after which the listing is active. For direct listings: + * 'active' means NFTs can be bought from the listing. For auctions, + * 'active' means bids can be made in the auction. + * + * @param endTime The timestamp after which the listing is inactive. + * For direct listings: 'inactive' means NFTs cannot be bought from the listing. + * For auctions: 'inactive' means bids can no longer be made in the auction. + * + * @param quantity The quantity of NFT of ID `tokenId` on the given `assetContract` listed. For + * ERC 721 tokens to list for sale, the contract strictly defaults this to `1`, + * Regardless of the value of `quantityToList` passed. + * + * @param currency For direct listings: the currency in which a buyer must pay the listing's fixed price + * to buy the NFT(s). For auctions: the currency in which the bidders must make bids. + * + * @param reservePricePerToken For direct listings: this value is ignored. For auctions: the minimum bid amount of + * the auction is `reservePricePerToken * quantityToList` + * + * @param buyoutPricePerToken For direct listings: interpreted as 'price per token' listed. For auctions: if + * `buyoutPricePerToken` is greater than 0, and a bidder's bid is at least as great as + * `buyoutPricePerToken * quantityToList`, the bidder wins the auction, and the auction + * is closed. + * + * @param tokenType The type of the token(s) listed for for sale -- ERC721 or ERC1155 + * + * @param listingType The type of listing to create - a direct listing or an auction. + **/ + struct Listing { + uint256 listingId; + address tokenOwner; + address assetContract; + uint256 tokenId; + uint256 startTime; + uint256 endTime; + uint256 quantity; + address currency; + uint256 reservePricePerToken; + uint256 buyoutPricePerToken; + TokenType tokenType; + ListingType listingType; + } + + /// @dev Emitted when a new listing is created. + event ListingAdded( + uint256 indexed listingId, + address indexed assetContract, + address indexed lister, + Listing listing + ); + + /// @dev Emitted when the parameters of a listing are updated. + event ListingUpdated(uint256 indexed listingId, address indexed listingCreator); + + /// @dev Emitted when a listing is cancelled. + event ListingRemoved(uint256 indexed listingId, address indexed listingCreator); + + /** + * @dev Emitted when a buyer buys from a direct listing, or a lister accepts some + * buyer's offer to their direct listing. + */ + event NewSale( + uint256 indexed listingId, + address indexed assetContract, + address indexed lister, + address buyer, + uint256 quantityBought, + uint256 totalPricePaid + ); + + /// @dev Emitted when (1) a new offer is made to a direct listing, or (2) when a new bid is made in an auction. + event NewOffer( + uint256 indexed listingId, + address indexed offeror, + ListingType indexed listingType, + uint256 quantityWanted, + uint256 totalOfferAmount, + address currency + ); + + /// @dev Emitted when an auction is closed. + event AuctionClosed( + uint256 indexed listingId, + address indexed closer, + bool indexed cancelled, + address auctionCreator, + address winningBidder + ); + + /// @dev Emitted when auction buffers are updated. + event AuctionBuffersUpdated(uint256 timeBuffer, uint256 bidBufferBps); + + /** + * @notice Lets a token owner list tokens (ERC 721 or ERC 1155) for sale in a direct listing, or an auction. + * + * @dev NFTs to list for sale in an auction are escrowed in Marketplace. For direct listings, the contract + * only checks whether the listing's creator owns and has approved Marketplace to transfer the NFTs to list. + * + * @param _params The parameters that govern the listing to be created. + */ + function createListing(ListingParameters memory _params) external; + + /** + * @notice Lets a listing's creator edit the listing's parameters. A direct listing can be edited whenever. + * An auction listing cannot be edited after the auction has started. + * + * @param _listingId The uid of the listing to edit. + * + * @param _quantityToList The amount of NFTs to list for sale in the listing. For direct listings, the contract + * only checks whether the listing creator owns and has approved Marketplace to transfer + * `_quantityToList` amount of NFTs to list for sale. For auction listings, the contract + * ensures that exactly `_quantityToList` amount of NFTs to list are escrowed. + * + * @param _reservePricePerToken For direct listings: this value is ignored. For auctions: the minimum bid amount of + * the auction is `reservePricePerToken * quantityToList` + * + * @param _buyoutPricePerToken For direct listings: interpreted as 'price per token' listed. For auctions: if + * `buyoutPricePerToken` is greater than 0, and a bidder's bid is at least as great as + * `buyoutPricePerToken * quantityToList`, the bidder wins the auction, and the auction + * is closed. + * + * @param _currencyToAccept For direct listings: the currency in which a buyer must pay the listing's fixed price + * to buy the NFT(s). For auctions: the currency in which the bidders must make bids. + * + * @param _startTime The unix timestamp after which listing is active. For direct listings: + * 'active' means NFTs can be bought from the listing. For auctions, + * 'active' means bids can be made in the auction. + * + * @param _secondsUntilEndTime No. of seconds after the provided `_startTime`, after which the listing is inactive. + * For direct listings: 'inactive' means NFTs cannot be bought from the listing. + * For auctions: 'inactive' means bids can no longer be made in the auction. + */ + function updateListing( + uint256 _listingId, + uint256 _quantityToList, + uint256 _reservePricePerToken, + uint256 _buyoutPricePerToken, + address _currencyToAccept, + uint256 _startTime, + uint256 _secondsUntilEndTime + ) external; + + /** + * @notice Lets a direct listing creator cancel their listing. + * + * @param _listingId The unique Id of the listing to cancel. + */ + function cancelDirectListing(uint256 _listingId) external; + + /** + * @notice Lets someone buy a given quantity of tokens from a direct listing by paying the fixed price. + * + * @param _listingId The uid of the direct listing to buy from. + * @param _buyFor The receiver of the NFT being bought. + * @param _quantity The amount of NFTs to buy from the direct listing. + * @param _currency The currency to pay the price in. + * @param _totalPrice The total price to pay for the tokens being bought. + * + * @dev A sale will fail to execute if either: + * (1) buyer does not own or has not approved Marketplace to transfer the appropriate + * amount of currency (or hasn't sent the appropriate amount of native tokens) + * + * (2) the lister does not own or has removed Marketplace's + * approval to transfer the tokens listed for sale. + */ + function buy( + uint256 _listingId, + address _buyFor, + uint256 _quantity, + address _currency, + uint256 _totalPrice + ) external payable; + + /** + * @notice Lets someone make an offer to a direct listing, or bid in an auction. + * + * @dev Each (address, listing ID) pair maps to a single unique offer. So e.g. if a buyer makes + * makes two offers to the same direct listing, the last offer is counted as the buyer's + * offer to that listing. + * + * @param _listingId The unique ID of the listing to make an offer/bid to. + * + * @param _quantityWanted For auction listings: the 'quantity wanted' is the total amount of NFTs + * being auctioned, regardless of the value of `_quantityWanted` passed. + * For direct listings: `_quantityWanted` is the quantity of NFTs from the + * listing, for which the offer is being made. + * + * @param _currency For auction listings: the 'currency of the bid' is the currency accepted + * by the auction, regardless of the value of `_currency` passed. For direct + * listings: this is the currency in which the offer is made. + * + * @param _pricePerToken For direct listings: offered price per token. For auction listings: the bid + * amount per token. The total offer/bid amount is `_quantityWanted * _pricePerToken`. + * + * @param _expirationTimestamp For auction listings: inapplicable. For direct listings: The timestamp after which + * the seller can no longer accept the offer. + */ + function offer( + uint256 _listingId, + uint256 _quantityWanted, + address _currency, + uint256 _pricePerToken, + uint256 _expirationTimestamp + ) external payable; + + /** + * @notice Lets a listing's creator accept an offer to their direct listing. + * @param _listingId The unique ID of the listing for which to accept the offer. + * @param _offeror The address of the buyer whose offer is to be accepted. + * @param _currency The currency of the offer that is to be accepted. + * @param _totalPrice The total price of the offer that is to be accepted. + */ + function acceptOffer(uint256 _listingId, address _offeror, address _currency, uint256 _totalPrice) external; + + /** + * @notice Lets any account close an auction on behalf of either the (1) auction's creator, or (2) winning bidder. + * For (1): The auction creator is sent the winning bid amount. + * For (2): The winning bidder is sent the auctioned NFTs. + * + * @param _listingId The uid of the listing (the auction to close). + * @param _closeFor For whom the auction is being closed - the auction creator or winning bidder. + */ + function closeAuction(uint256 _listingId, address _closeFor) external; +} diff --git a/contracts/prebuilts/interface/staking/IEditionStake.sol b/contracts/prebuilts/interface/staking/IEditionStake.sol new file mode 100644 index 000000000..e8fd48353 --- /dev/null +++ b/contracts/prebuilts/interface/staking/IEditionStake.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/** + * Thirdweb's EditionStake smart contract allows users to stake their ERC-1155 NFTs + * and earn rewards in form of an ERC-20 token. + * + * note: + * - Reward token and staking token can't be changed after deployment. + * + * - ERC1155 tokens from only the specified contract can be staked. + * + * - All token/NFT transfers require approval on their respective contracts. + * + * - Admin must deposit reward tokens using the `depositRewardTokens` function only. + * Any direct transfers may cause unintended consequences, such as locking of tokens. + * + * - Users must stake NFTs using the `stake` function only. + * Any direct transfers may cause unintended consequences, such as locking of NFTs. + */ + +interface IEditionStake { + /// @dev Emitted when contract admin withdraws reward tokens. + event RewardTokensWithdrawnByAdmin(uint256 _amount); + + /// @dev Emitted when contract admin deposits reward tokens. + event RewardTokensDepositedByAdmin(uint256 _amount); + + /** + * @notice Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) deposit reward-tokens. + * + * note: Tokens should be approved on the reward-token contract before depositing. + * + * @param _amount Amount of tokens to deposit. + */ + function depositRewardTokens(uint256 _amount) external payable; + + /** + * @notice Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) withdraw reward-tokens. + * Useful for removing excess balance, thus preventing locking of tokens. + * + * @param _amount Amount of tokens to deposit. + */ + function withdrawRewardTokens(uint256 _amount) external; +} diff --git a/contracts/prebuilts/interface/staking/INFTStake.sol b/contracts/prebuilts/interface/staking/INFTStake.sol new file mode 100644 index 000000000..0ff235059 --- /dev/null +++ b/contracts/prebuilts/interface/staking/INFTStake.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/** + * Thirdweb's NFTStake smart contract allows users to stake their ERC-721 NFTs + * and earn rewards in form of an ERC-20 token. + * + * note: + * - Reward token and staking token can't be changed after deployment. + * + * - ERC721 tokens from only the specified contract can be staked. + * + * - All token/NFT transfers require approval on their respective contracts. + * + * - Admin must deposit reward tokens using the `depositRewardTokens` function only. + * Any direct transfers may cause unintended consequences, such as locking of tokens. + * + * - Users must stake NFTs using the `stake` function only. + * Any direct transfers may cause unintended consequences, such as locking of NFTs. + */ + +interface INFTStake { + /// @dev Emitted when contract admin withdraws reward tokens. + event RewardTokensWithdrawnByAdmin(uint256 _amount); + + /// @dev Emitted when contract admin deposits reward tokens. + event RewardTokensDepositedByAdmin(uint256 _amount); + + /** + * @notice Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) deposit reward-tokens. + * + * note: Tokens should be approved on the reward-token contract before depositing. + * + * @param _amount Amount of tokens to deposit. + */ + function depositRewardTokens(uint256 _amount) external payable; + + /** + * @notice Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) withdraw reward-tokens. + * Useful for removing excess balance, thus preventing locking of tokens. + * + * @param _amount Amount of tokens to deposit. + */ + function withdrawRewardTokens(uint256 _amount) external; +} diff --git a/contracts/prebuilts/interface/staking/ITokenStake.sol b/contracts/prebuilts/interface/staking/ITokenStake.sol new file mode 100644 index 000000000..25fb220fe --- /dev/null +++ b/contracts/prebuilts/interface/staking/ITokenStake.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/** + * Thirdweb's TokenStake smart contract allows users to stake their ERC-20 Tokens + * and earn rewards in form of a different ERC-20 token. + * + * note: + * - Reward token and staking token can't be changed after deployment. + * Reward token contract can't be same as the staking token contract. + * + * - ERC20 tokens from only the specified contract can be staked. + * + * - All token transfers require approval on their respective token-contracts. + * + * - Admin must deposit reward tokens using the `depositRewardTokens` function only. + * Any direct transfers may cause unintended consequences, such as locking of tokens. + * + * - Users must stake tokens using the `stake` function only. + * Any direct transfers may cause unintended consequences, such as locking of tokens. + */ + +interface ITokenStake { + /// @dev Emitted when contract admin withdraws reward tokens. + event RewardTokensWithdrawnByAdmin(uint256 _amount); + + /// @dev Emitted when contract admin deposits reward tokens. + event RewardTokensDepositedByAdmin(uint256 _amount); + + /** + * @notice Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) deposit reward-tokens. + * + * note: Tokens should be approved on the reward-token contract before depositing. + * + * @param _amount Amount of tokens to deposit. + */ + function depositRewardTokens(uint256 _amount) external payable; + + /** + * @notice Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) withdraw reward-tokens. + * Useful for removing excess balance, thus preventing locking of tokens. + * + * @param _amount Amount of tokens to deposit. + */ + function withdrawRewardTokens(uint256 _amount) external; +} diff --git a/contracts/interfaces/token/ITokenERC1155.sol b/contracts/prebuilts/interface/token/ITokenERC1155.sol similarity index 91% rename from contracts/interfaces/token/ITokenERC1155.sol rename to contracts/prebuilts/interface/token/ITokenERC1155.sol index 322f7d8ec..656878796 100644 --- a/contracts/interfaces/token/ITokenERC1155.sol +++ b/contracts/prebuilts/interface/token/ITokenERC1155.sol @@ -58,10 +58,10 @@ interface ITokenERC1155 is IERC1155Upgradeable { * * returns (success, signer) Result of verification and the recovered address. */ - function verify(MintRequest calldata req, bytes calldata signature) - external - view - returns (bool success, address signer); + function verify( + MintRequest calldata req, + bytes calldata signature + ) external view returns (bool success, address signer); /** * @notice Lets an account with MINTER_ROLE mint an NFT. @@ -72,12 +72,7 @@ interface ITokenERC1155 is IERC1155Upgradeable { * @param amount The number of copies of the NFT to mint. * */ - function mintTo( - address to, - uint256 tokenId, - string calldata uri, - uint256 amount - ) external; + function mintTo(address to, uint256 tokenId, string calldata uri, uint256 amount) external; /** * @notice Mints an NFT according to the provided mint request. diff --git a/contracts/interfaces/token/ITokenERC20.sol b/contracts/prebuilts/interface/token/ITokenERC20.sol similarity index 94% rename from contracts/interfaces/token/ITokenERC20.sol rename to contracts/prebuilts/interface/token/ITokenERC20.sol index 13ed053e7..4a97956df 100644 --- a/contracts/interfaces/token/ITokenERC20.sol +++ b/contracts/prebuilts/interface/token/ITokenERC20.sol @@ -42,10 +42,10 @@ interface ITokenERC20 is IERC20MetadataUpgradeable { * * returns (success, signer) Result of verification and the recovered address. */ - function verify(MintRequest calldata req, bytes calldata signature) - external - view - returns (bool success, address signer); + function verify( + MintRequest calldata req, + bytes calldata signature + ) external view returns (bool success, address signer); /** * @dev Creates `amount` new tokens for `to`. diff --git a/contracts/interfaces/token/ITokenERC721.sol b/contracts/prebuilts/interface/token/ITokenERC721.sol similarity index 94% rename from contracts/interfaces/token/ITokenERC721.sol rename to contracts/prebuilts/interface/token/ITokenERC721.sol index 22e2f0b63..caf0016d9 100644 --- a/contracts/interfaces/token/ITokenERC721.sol +++ b/contracts/prebuilts/interface/token/ITokenERC721.sol @@ -52,10 +52,10 @@ interface ITokenERC721 is IERC721Upgradeable { * * returns (success, signer) Result of verification and the recovered address. */ - function verify(MintRequest calldata req, bytes calldata signature) - external - view - returns (bool success, address signer); + function verify( + MintRequest calldata req, + bytes calldata signature + ) external view returns (bool success, address signer); /** * @notice Lets an account with MINTER_ROLE mint an NFT. diff --git a/contracts/prebuilts/loyalty/LoyaltyCard.sol b/contracts/prebuilts/loyalty/LoyaltyCard.sol new file mode 100644 index 000000000..1145ed72d --- /dev/null +++ b/contracts/prebuilts/loyalty/LoyaltyCard.sol @@ -0,0 +1,339 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// Interface +import "../interface/ILoyaltyCard.sol"; + +// Base +import "../../eip/ERC721AVirtualApproveUpgradeable.sol"; + +// Lib +import "../../lib/CurrencyTransferLib.sol"; + +// Extensions +import "../../extension/NFTMetadata.sol"; +import "../../extension/SignatureMintERC721Upgradeable.sol"; +import "../../extension/ContractMetadata.sol"; +import "../../extension/Ownable.sol"; +import "../../extension/Royalty.sol"; +import "../../extension/PrimarySale.sol"; +import "../../extension/PlatformFee.sol"; +import "../../extension/Multicall.sol"; +import "../../extension/PermissionsEnumerable.sol"; +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; + +/** + * @title LoyaltyCard + * + * @custom:description This contract is a loyalty card NFT collection. Each NFT represents a loyalty card, and the NFT's metadata + * contains the loyalty card's information. A loyalty card's metadata can be updated by an admin of the contract. + * A loyalty card can be cancelled (i.e. 'burned') by its owner or an approved operator. A loyalty card can be revoked + * (i.e. 'burned') without its owner's approval, by an admin of the contract. + */ +contract LoyaltyCard is + ILoyaltyCard, + ContractMetadata, + Ownable, + Royalty, + PrimarySale, + PlatformFee, + Multicall, + PermissionsEnumerable, + ReentrancyGuardUpgradeable, + ERC2771ContextUpgradeable, + NFTMetadata, + SignatureMintERC721Upgradeable, + ERC721AUpgradeable +{ + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + /// @dev Only TRANSFER_ROLE holders can have tokens transferred from or to them, during restricted transfers. + bytes32 private constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE"); + /// @dev Only MINTER_ROLE holders can sign off on `MintRequest`s. + bytes32 private constant MINTER_ROLE = keccak256("MINTER_ROLE"); + /// @dev Only METADATA_ROLE holders can update NFT metadata. + bytes32 private constant METADATA_ROLE = keccak256("METADATA_ROLE"); + /// @dev Only REVOKE_ROLE holders can revoke a loyalty card. + bytes32 private constant REVOKE_ROLE = keccak256("REVOKE_ROLE"); + + /// @dev Max bps in the thirdweb system. + uint256 private constant MAX_BPS = 10_000; + + address public constant DEFAULT_FEE_RECIPIENT = 0x1Af20C6B23373350aD464700B5965CE4B0D2aD94; + uint16 private constant DEFAULT_FEE_BPS = 100; + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer + //////////////////////////////////////////////////////////////*/ + + constructor() initializer {} + + /// @dev Initializes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _royaltyRecipient, + uint128 _royaltyBps, + uint128 _platformFeeBps, + address _platformFeeRecipient + ) external initializer { + // Initialize inherited contracts, most base-like -> most derived. + __ERC2771Context_init(_trustedForwarders); + __ERC721A_init(_name, _symbol); + __SignatureMintERC721_init(); + __ReentrancyGuard_init(); + + _setupContractURI(_contractURI); + _setupOwner(_defaultAdmin); + + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + _setupRole(MINTER_ROLE, _defaultAdmin); + _setupRole(TRANSFER_ROLE, _defaultAdmin); + + _setupRole(METADATA_ROLE, _defaultAdmin); + _setRoleAdmin(METADATA_ROLE, METADATA_ROLE); + + _setupRole(REVOKE_ROLE, _defaultAdmin); + _setRoleAdmin(REVOKE_ROLE, REVOKE_ROLE); + + _setupPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + _setupPrimarySaleRecipient(_saleRecipient); + } + + /*/////////////////////////////////////////////////////////////// + ERC 165 / 721 / 2981 logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the URI for a given tokenId. + function tokenURI(uint256 _tokenId) public view override returns (string memory) { + return _getTokenURI(_tokenId); + } + + /// @dev See ERC 165 + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC721AUpgradeable, IERC165) returns (bool) { + return super.supportsInterface(interfaceId) || type(IERC2981).interfaceId == interfaceId; + } + + /*/////////////////////////////////////////////////////////////// + External functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Mints an NFT according to the provided mint request. Always mints 1 NFT. + function mintWithSignature( + MintRequest calldata _req, + bytes calldata _signature + ) external payable nonReentrant returns (address signer) { + require(_req.quantity == 1, "LoyaltyCard: only 1 NFT can be minted at a time."); + + signer = _processRequest(_req, _signature); + address receiver = _req.to; + uint256 tokenIdMinted = _mintTo(receiver, _req.uri); + + // Set royalties, if applicable. + if (_req.royaltyRecipient != address(0) && _req.royaltyBps != 0) { + _setupRoyaltyInfoForToken(tokenIdMinted, _req.royaltyRecipient, _req.royaltyBps); + } + + _collectPrice(_req.primarySaleRecipient, _req.quantity, _req.currency, _req.pricePerToken); + + emit TokensMintedWithSignature(signer, receiver, tokenIdMinted, _req); + } + + /// @dev Lets an account with MINTER_ROLE mint an NFT. Always mints 1 NFT. + function mintTo( + address _to, + string calldata _uri + ) external onlyRole(MINTER_ROLE) nonReentrant returns (uint256 tokenIdMinted) { + tokenIdMinted = _mintTo(_to, _uri); + emit TokensMinted(_to, tokenIdMinted, _uri); + } + + /// @dev Burns `tokenId`. See {ERC721-_burn}. + function cancel(uint256 tokenId) external virtual override { + // note: ERC721AUpgradeable's `_burn(uint256,bool)` internally checks for token approvals. + _burn(tokenId, true); + } + + /// @dev Burns `tokenId`. See {ERC721-_burn}. + function revoke(uint256 tokenId) external virtual override onlyRole(REVOKE_ROLE) { + _burn(tokenId); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Returns the total amount of tokens minted in the contract. + */ + function totalMinted() external view returns (uint256) { + unchecked { + return _currentIndex - _startTokenId(); + } + } + + /// @dev The tokenId of the next NFT that will be minted / lazy minted. + function nextTokenIdToMint() external view returns (uint256) { + return _currentIndex; + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Collects and distributes the primary sale value of NFTs being claimed. + function _collectPrice( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal { + if (_pricePerToken == 0) { + require(msg.value == 0, "!Value"); + return; + } + + uint256 totalPrice = _quantityToClaim * _pricePerToken; + + bool validMsgValue; + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { + validMsgValue = msg.value == totalPrice; + } else { + validMsgValue = msg.value == 0; + } + require(validMsgValue, "Invalid msg value"); + + address saleRecipient = _primarySaleRecipient == address(0) ? primarySaleRecipient() : _primarySaleRecipient; + + uint256 fees; + address feeRecipient; + + uint256 platformFeesTw = (totalPrice * DEFAULT_FEE_BPS) / MAX_BPS; + + PlatformFeeType feeType = getPlatformFeeType(); + if (feeType == PlatformFeeType.Flat) { + (feeRecipient, fees) = getFlatPlatformFeeInfo(); + } else { + uint16 platformFeeBps; + (feeRecipient, platformFeeBps) = getPlatformFeeInfo(); + fees = (totalPrice * platformFeeBps) / MAX_BPS; + } + + require(totalPrice >= fees + platformFeesTw, "Fees greater than price"); + + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), DEFAULT_FEE_RECIPIENT, platformFeesTw); + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), feeRecipient, fees); + CurrencyTransferLib.transferCurrency( + _currency, + _msgSender(), + saleRecipient, + totalPrice - fees - platformFeesTw + ); + } + + /// @dev Mints an NFT to `to` + function _mintTo(address _to, string calldata _uri) internal returns (uint256 tokenIdToMint) { + tokenIdToMint = _currentIndex; + + _setTokenURI(tokenIdToMint, _uri); + _safeMint(_to, 1); + } + + /// @dev See {ERC721-_beforeTokenTransfer}. + function _beforeTokenTransfers( + address from, + address to, + uint256 startTokenId, + uint256 quantity + ) internal virtual override { + super._beforeTokenTransfers(from, to, startTokenId, quantity); + + // if transfer is restricted on the contract, we still want to allow burning and minting + if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { + if (!hasRole(TRANSFER_ROLE, from) && !hasRole(TRANSFER_ROLE, to)) { + revert("!Transfer-Role"); + } + } + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function _canSetPlatformFeeInfo() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether primary sale recipient can be set in the given execution context. + function _canSetPrimarySaleRecipient() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether owner can be set in the given execution context. + function _canSetOwner() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether royalty info can be set in the given execution context. + function _canSetRoyaltyInfo() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Returns whether a given address is authorized to sign mint requests. + function _isAuthorizedSigner(address _signer) internal view override returns (bool) { + return hasRole(MINTER_ROLE, _signer); + } + + /// @dev Returns whether metadata can be set in the given execution context. + function _canSetMetadata() internal view virtual override returns (bool) { + return hasRole(METADATA_ROLE, _msgSender()); + } + + /// @dev Returns whether metadata can be frozen in the given execution context. + function _canFreezeMetadata() internal view virtual override returns (bool) { + return hasRole(METADATA_ROLE, _msgSender()); + } + + function _msgSender() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } + + function _msgData() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable) + returns (bytes calldata) + { + return ERC2771ContextUpgradeable._msgData(); + } +} diff --git a/contracts/marketplace/Marketplace.sol b/contracts/prebuilts/marketplace-legacy/Marketplace.sol similarity index 92% rename from contracts/marketplace/Marketplace.sol rename to contracts/prebuilts/marketplace-legacy/Marketplace.sol index 874af5c84..0a7ea2301 100644 --- a/contracts/marketplace/Marketplace.sol +++ b/contracts/prebuilts/marketplace-legacy/Marketplace.sol @@ -1,6 +1,17 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.11; +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + // ========== External imports ========== import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; @@ -15,24 +26,23 @@ import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; // ========== Internal imports ========== -import { IMarketplace } from "../interfaces/marketplace/IMarketplace.sol"; -import { ITWFee } from "../interfaces/ITWFee.sol"; +import { IMarketplace } from "../interface/marketplace/IMarketplace.sol"; -import "../openzeppelin-presets/metatx/ERC2771ContextUpgradeable.sol"; +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; -import "../lib/CurrencyTransferLib.sol"; -import "../lib/FeeType.sol"; +import "../../extension/Multicall.sol"; +import "../../lib/CurrencyTransferLib.sol"; +import "../../lib/FeeType.sol"; contract Marketplace is Initializable, IMarketplace, ReentrancyGuardUpgradeable, ERC2771ContextUpgradeable, - MulticallUpgradeable, + Multicall, AccessControlEnumerableUpgradeable, IERC721ReceiverUpgradeable, IERC1155ReceiverUpgradeable @@ -52,9 +62,6 @@ contract Marketplace is /// @dev The address of the native token wrapper contract. address private immutable nativeTokenWrapper; - /// @dev The thirdweb contract with fee related information. - ITWFee public immutable thirdwebFee; - /// @dev Total number of listings ever created in the marketplace. uint256 public totalListings; @@ -113,12 +120,11 @@ contract Marketplace is Constructor + initializer logic //////////////////////////////////////////////////////////////*/ - constructor(address _nativeTokenWrapper, address _thirdwebFee) initializer { - thirdwebFee = ITWFee(_thirdwebFee); + constructor(address _nativeTokenWrapper) initializer { nativeTokenWrapper = _nativeTokenWrapper; } - /// @dev Initiliazes the contract, like a constructor. + /// @dev Initializes the contract, like a constructor. function initialize( address _defaultAdmin, string memory _contractURI, @@ -184,22 +190,13 @@ contract Marketplace is return this.onERC1155BatchReceived.selector; } - function onERC721Received( - address, - address, - uint256, - bytes calldata - ) external pure override returns (bytes4) { + function onERC721Received(address, address, uint256, bytes calldata) external pure override returns (bytes4) { return this.onERC721Received.selector; } - function supportsInterface(bytes4 interfaceId) - public - view - virtual - override(AccessControlEnumerableUpgradeable, IERC165Upgradeable) - returns (bool) - { + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(AccessControlEnumerableUpgradeable, IERC165Upgradeable) returns (bool) { return interfaceId == type(IERC1155ReceiverUpgradeable).interfaceId || interfaceId == type(IERC721ReceiverUpgradeable).interfaceId || @@ -258,7 +255,11 @@ contract Marketplace is // Tokens listed for sale in an auction are escrowed in Marketplace. if (newListing.listingType == ListingType.Auction) { - require(newListing.buyoutPricePerToken >= newListing.reservePricePerToken, "RESERVE"); + require( + newListing.buyoutPricePerToken == 0 || + newListing.buyoutPricePerToken >= newListing.reservePricePerToken, + "RESERVE" + ); transferListingTokens(tokenOwner, address(this), tokenAmountToList, newListing); } @@ -284,7 +285,7 @@ contract Marketplace is // Can only edit auction listing before it starts. if (isAuction) { require(block.timestamp < targetListing.startTime, "STARTED"); - require(_buyoutPricePerToken >= _reservePricePerToken, "RESERVE"); + require(_buyoutPricePerToken == 0 || _buyoutPricePerToken >= _reservePricePerToken, "RESERVE"); } if (_startTime < block.timestamp) { @@ -309,7 +310,7 @@ contract Marketplace is listingType: targetListing.listingType }); - // Must validate ownership and approval of the new quantity of tokens for diret listing. + // Must validate ownership and approval of the new quantity of tokens for direct listing. if (targetListing.quantity != safeNewQuantity) { // Transfer all escrowed tokens back to the lister, to be reflected in the lister's // balance for the upcoming ownership and approval check. @@ -466,6 +467,7 @@ contract Marketplace is if (targetListing.listingType == ListingType.Auction) { // A bid to an auction must be made in the auction's desired currency. require(newOffer.currency == targetListing.currency, "must use approved currency to bid"); + require(newOffer.pricePerToken != 0, "bidding zero amount"); // A bid must be made for all auction items. newOffer.quantityWanted = getSafeQuantity(targetListing.tokenType, targetListing.quantity); @@ -523,7 +525,7 @@ contract Marketplace is _closeAuctionForBidder(_targetListing, _incomingBid); } else { /** - * If there's an exisitng winning bid, incoming bid amount must be bid buffer % greater. + * If there's an existng winning bid, incoming bid amount must be bid buffer % greater. * Else, bid amount must be at least as great as reserve price */ require( @@ -593,12 +595,10 @@ contract Marketplace is //////////////////////////////////////////////////////////////*/ /// @dev Lets an account close an auction for either the (1) winning bidder, or (2) auction creator. - function closeAuction(uint256 _listingId, address _closeFor) - external - override - nonReentrant - onlyExistingListing(_listingId) - { + function closeAuction( + uint256 _listingId, + address _closeFor + ) external override nonReentrant onlyExistingListing(_listingId) { Listing memory targetListing = listings[_listingId]; require(targetListing.listingType == ListingType.Auction, "not an auction."); @@ -684,12 +684,7 @@ contract Marketplace is //////////////////////////////////////////////////////////////*/ /// @dev Transfers tokens listed for sale in a direct or auction listing. - function transferListingTokens( - address _from, - address _to, - uint256 _quantity, - Listing memory _listing - ) internal { + function transferListingTokens(address _from, address _to, uint256 _quantity, Listing memory _listing) internal { if (_listing.tokenType == TokenType.ERC1155) { IERC1155Upgradeable(_listing.assetContract).safeTransferFrom(_from, _to, _listing.tokenId, _quantity, ""); } else if (_listing.tokenType == TokenType.ERC721) { @@ -707,9 +702,6 @@ contract Marketplace is ) internal { uint256 platformFeeCut = (_totalPayoutAmount * platformFeeBps) / MAX_BPS; - (address twFeeRecipient, uint256 twFeeBps) = thirdwebFee.getFeeInfo(address(this), FeeType.MARKET_SALE); - uint256 twFeeCut = (_totalPayoutAmount * twFeeBps) / MAX_BPS; - uint256 royaltyCut; address royaltyRecipient; @@ -719,7 +711,7 @@ contract Marketplace is uint256 royaltyFeeAmount ) { if (royaltyFeeRecipient != address(0) && royaltyFeeAmount > 0) { - require(royaltyFeeAmount + platformFeeCut + twFeeCut <= _totalPayoutAmount, "fees exceed the price"); + require(royaltyFeeAmount + platformFeeCut <= _totalPayoutAmount, "fees exceed the price"); royaltyRecipient = royaltyFeeRecipient; royaltyCut = royaltyFeeAmount; } @@ -742,18 +734,11 @@ contract Marketplace is royaltyCut, _nativeTokenWrapper ); - CurrencyTransferLib.transferCurrencyWithWrapper( - _currencyToUse, - _payer, - twFeeRecipient, - twFeeCut, - _nativeTokenWrapper - ); CurrencyTransferLib.transferCurrencyWithWrapper( _currencyToUse, _payer, _payee, - _totalPayoutAmount - (platformFeeCut + royaltyCut + twFeeCut), + _totalPayoutAmount - (platformFeeCut + royaltyCut), _nativeTokenWrapper ); } @@ -837,11 +822,10 @@ contract Marketplace is //////////////////////////////////////////////////////////////*/ /// @dev Enforces quantity == 1 if tokenType is TokenType.ERC721. - function getSafeQuantity(TokenType _tokenType, uint256 _quantityToCheck) - internal - pure - returns (uint256 safeQuantity) - { + function getSafeQuantity( + TokenType _tokenType, + uint256 _quantityToCheck + ) internal pure returns (uint256 safeQuantity) { if (_quantityToCheck == 0) { safeQuantity = 0; } else { @@ -870,10 +854,10 @@ contract Marketplace is //////////////////////////////////////////////////////////////*/ /// @dev Lets a contract admin update platform fee recipient and bps. - function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) - external - onlyRole(DEFAULT_ADMIN_ROLE) - { + function setPlatformFeeInfo( + address _platformFeeRecipient, + uint256 _platformFeeBps + ) external onlyRole(DEFAULT_ADMIN_ROLE) { require(_platformFeeBps <= MAX_BPS, "bps <= 10000."); platformFeeBps = uint64(_platformFeeBps); @@ -905,7 +889,7 @@ contract Marketplace is internal view virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) + override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) returns (address sender) { return ERC2771ContextUpgradeable._msgSender(); diff --git a/contracts/marketplace/marketplace.md b/contracts/prebuilts/marketplace-legacy/marketplace.md similarity index 93% rename from contracts/marketplace/marketplace.md rename to contracts/prebuilts/marketplace-legacy/marketplace.md index 4f9d065f6..cdfb7683c 100644 --- a/contracts/marketplace/marketplace.md +++ b/contracts/prebuilts/marketplace-legacy/marketplace.md @@ -1,6 +1,6 @@ # Marketplace design document. -This is a live document that explains what the [thirdweb](https://thirdweb.com/) `Marketplace` smart contract is, how it works and can be used, and why it is written the way it is. +This is a live document that explains what the [thirdweb](https://thirdweb.com/) `Marketplace` smart contract is, how it works and can be used, and why it is written the way it is. The document is written for technical and non-technical readers. To ask further questions about `Marketplace`, please join the [thirdweb discord](https://discord.gg/thirdweb) or create a [github issue](https://github.com/thirdweb-dev/contracts/issues). @@ -38,7 +38,7 @@ To make an offer to a direct listing, a buyer specifies — When making an offer to a direct listing, the offer amount is not escrowed in the Marketplace. Instead, making an offer requires the buyer to approve Marketplace to transfer the appropriate amount of currency to let Marketplace transfer the offer amount from the buyer to the lister, in case the lister accepts the buyer's offer. -To buy NFTs from a direct listing buy paying the listing's specified price, a buyer specifes - +To buy NFTs from a direct listing buy paying the listing's specified price, a buyer specifies - | Parameter | Type | Description | | --- | --- | --- | @@ -48,7 +48,7 @@ To buy NFTs from a direct listing buy paying the listing's specified price, a bu | `currency` | address | The currency in which to pay for the NFTs being bought. | | `totalPrice` | uint256 | The total price to pay for the NFTs being bought. | -A sale will fail to execute if either (1) the buyer does not own or has not approved Marketplace to transfer the appropriate amount of currency (or hasn't sent the appropriate amount of native tokens), or (2) the lister does not own or has removed Markeplace's approval to transfer the tokens listed for sale. +A sale will fail to execute if either (1) the buyer does not own or has not approved Marketplace to transfer the appropriate amount of currency (or hasn't sent the appropriate amount of native tokens), or (2) the lister does not own or has removed Marketplace's approval to transfer the tokens listed for sale. A sale is executed when either a buyer pays the fixed price, or the seller accepts an offer made to the listing. @@ -78,11 +78,11 @@ Every auction listing obeys two 'buffers' to make it a fair auction: These buffer values are contract-wide, which means every auction conducted in the Marketplace obeys, at any given moment, the same buffers. These buffers can be configured by contract admins i.e. accounts with the `DEFAULT_ADMIN_ROLE` role. -The NFTs to list in an auction *do* leave the wallet of the lister, and are escrowed in the market until the closing of the auction. Whenever a new winning bid is made by a buyer, the buyer deposits this bid amount into the market; this bid amount is escrowed in the market until a new winning bid is made. The previous winning bid amount is automatically refunded to the respective bidder. +The NFTs to list in an auction *do* leave the wallet of the lister, and are escrowed in the market until the closing of the auction. Whenever a new winning bid is made by a buyer, the buyer deposits this bid amount into the market; this bid amount is escrowed in the market until a new winning bid is made. The previous winning bid amount is automatically refunded to the respective bidder. **Note:** As a result, the new winning bidder pays for the gas used in refunding the previous winning bidder. This trade-off is made for better UX for bidders — a bidder that has been outbid is automatically refunded, and does not need to pull out their deposited bid manually. This reduces bidding to a single action, instead of two actions — bidding, and pulling out the bid on being outbid. -If the lister sets a `buyoutPricePerToken`, the marketplace expects the `buyoutPricePerToken` to be greater than or equal to the `rerservePricePerToken` of the auction. +If the lister sets a `buyoutPricePerToken`, the marketplace expects the `buyoutPricePerToken` to be greater than or equal to the `reservePricePerToken` of the auction. Once the auction window ends, the seller collects the highest bid, and the buyer collects the auctioned NFTs. @@ -116,16 +116,16 @@ To thirdweb customers, the `Marketplace` can be set up like any of the other thi To the end users of thirdweb customers, the experience of using the marketplace will feel familiar to popular marketplace platforms like OpenSea, Zora, etc. The biggest difference in user experience will be that performing any action on the marketplace requires gas fees. - Thirdweb's customers - - Deploy the marketplace contract like any other thirdweb contract. - - Can set a % 'platform fee'. This % is collected on every sale — when a buyer buys tokens from a direct listing, and when a seller collects the highest bid on auction closing. This platform fee is distributed to the platform fee recipient (set by a contract admin). - - Can set auction buffers. These auction buffers apply to all auctions being conducted in the market. -- End users of thirdweb customers - - Can list NFTs for sale at a fixed price. - - Can edit an existing listing's parameters, e.g. the currency accepted. An auction's parameters cannot be edited once it has started. - - Can make offers to NFTs listed for a fixed price. - - Can auction NFTs. - - Can make bids to auctions. - - Must pay gas fees to perform any actions, including the actions just listed. + - Deploy the marketplace contract like any other thirdweb contract. + - Can set a % 'platform fee'. This % is collected on every sale — when a buyer buys tokens from a direct listing, and when a seller collects the highest bid on auction closing. This platform fee is distributed to the platform fee recipient (set by a contract admin). + - Can set auction buffers. These auction buffers apply to all auctions being conducted in the market. + - End users of thirdweb customers + - Can list NFTs for sale at a fixed price. + - Can edit an existing listing's parameters, e.g. the currency accepted. An auction's parameters cannot be edited once it has started. + - Can make offers to NFTs listed for a fixed price. + - Can auction NFTs. + - Can make bids to auctions. + - Must pay gas fees to perform any actions, including the actions just listed. ## Technical details @@ -160,7 +160,7 @@ We use common functions and data structures wherever an (1) action is common to **Example**: Common action and data handled. - Action: creating a listing | Data: `ListingParameters` - + ```solidity struct ListingParameters { address assetContract; @@ -183,9 +183,9 @@ An auction has the concept of formally being closed whereas a direct listing doe ### EIPs implemented / supported -To be able to escrow NFTs in the case of auctions, Marketplace implements the receiver interfaces for [ERC1155](https://eips.ethereum.org/EIPS/eip-1155) and [ERC721](https://eips.ethereum.org/EIPS/eip-721) tokens. +To be able to escrow NFTs in the case of auctions, Marketplace implements the receiver interfaces for [ERC1155](https://eips.ethereum.org/EIPS/eip-1155) and [ERC721](https://eips.ethereum.org/EIPS/eip-721) tokens. -To enable meta-transactions (gasless), Marketplace implements [ERC2771](https://eips.ethereum.org/EIPS/eip-2771). +To enable meta-transactions (gasless), Marketplace implements [ERC2771](https://eips.ethereum.org/EIPS/eip-2771). Marketplace also honors [ERC2981](https://eips.ethereum.org/EIPS/eip-2981) for the distribution of royalties on direct and auction listings. @@ -200,7 +200,7 @@ The `Marketplace` contract supports both ERC20 currencies, and a chain's native 💡 **Note**: The only exception is offers to direct listings — these can only be made with ERC20 tokens, since Marketplace needs to transfer the offer amount from the buyer to the lister, in case the lister accepts the buyer's offer. This cannot be done with native tokens without escrowing the requisite amount of currency. -The contract wraps all native tokens deposited into it as the canonical ERC20 wrapped version of the native token (e.g. WETH for ether). The contract unwraps the wrapped native token when transferring native tokens to a given address. +The contract wraps all native tokens deposited into it as the canonical ERC20 wrapped version of the native token (e.g. WETH for ether). The contract unwraps the wrapped native token when transferring native tokens to a given address. If the contract fails to transfer out native tokens, it wraps them back to wrapped native tokens, and transfers the wrapped native tokens to the concerned address. The contract may fail to transfer out native tokens to an address, if the address represents a smart contract that cannot accept native tokens transferred to it directly. @@ -208,7 +208,7 @@ If the contract fails to transfer out native tokens, it wraps them back to wrapp **Two contracts instead of one:** -The main alternative design considered for the `Marketplace` was to split the smart contract into two smart contracts, where each handles (1) only direct listings + offers, or (2) only auction listings + bids. +The main alternative design considered for the `Marketplace` was to split the smart contract into two smart contracts, where each handles (1) only direct listings + offers, or (2) only auction listings + bids. Such a design gives us two 'lean' contracts instead of one large one, and the cost for deploying just one of these two contracts is less than deploying the single, large `Marketplace` contract. Having two separate contracts positions the thirdweb system to be more modular, where a thirdweb customer can only deploy the smart contract that gives them the specific functionality they want. @@ -220,4 +220,4 @@ Having a single, large contract gives us less room to add the ability for the ma Marketplace platforms like OpenSea make actions like making an offer to a direct listing, gasless. End users of the marketplace sign messages expressing intent to perform an action (e.g. list *x* NFT for sale at the price of 10 ETH), and a centralized order-book infrastructure matches two seller-buyer intents, and send the respective signed messages by the seller and buyer to their market smart contract for the sale to be executed. -We're working on breaking up, sizing down and optimizing the `Marketplace` contract to accommodate such off-chain actions, and coming up with a central order-book infrastructure that each thirdweb customer can run on their own. \ No newline at end of file +We're working on breaking up, sizing down and optimizing the `Marketplace` contract to accommodate such off-chain actions, and coming up with a central order-book infrastructure that each thirdweb customer can run on their own. diff --git a/contracts/prebuilts/marketplace/IMarketplace.sol b/contracts/prebuilts/marketplace/IMarketplace.sol new file mode 100644 index 000000000..0b9e05bcf --- /dev/null +++ b/contracts/prebuilts/marketplace/IMarketplace.sol @@ -0,0 +1,512 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +/** + * @author thirdweb.com + * + * The `DirectListings` extension smart contract lets you buy and sell NFTs (ERC-721 or ERC-1155) for a fixed price. + */ +interface IDirectListings { + enum TokenType { + ERC721, + ERC1155 + } + + enum Status { + UNSET, + CREATED, + COMPLETED, + CANCELLED + } + + /** + * @notice The parameters a seller sets when creating or updating a listing. + * + * @param assetContract The address of the smart contract of the NFTs being listed. + * @param tokenId The tokenId of the NFTs being listed. + * @param quantity The quantity of NFTs being listed. This must be non-zero, and is expected to + * be `1` for ERC-721 NFTs. + * @param currency The currency in which the price must be paid when buying the listed NFTs. + * @param pricePerToken The price to pay per unit of NFTs listed. + * @param startTimestamp The UNIX timestamp at and after which NFTs can be bought from the listing. + * @param endTimestamp The UNIX timestamp at and after which NFTs cannot be bought from the listing. + * @param reserved Whether the listing is reserved to be bought from a specific set of buyers. + */ + struct ListingParameters { + address assetContract; + uint256 tokenId; + uint256 quantity; + address currency; + uint256 pricePerToken; + uint128 startTimestamp; + uint128 endTimestamp; + bool reserved; + } + + /** + * @notice The information stored for a listing. + * + * @param listingId The unique ID of the listing. + * @param listingCreator The creator of the listing. + * @param assetContract The address of the smart contract of the NFTs being listed. + * @param tokenId The tokenId of the NFTs being listed. + * @param quantity The quantity of NFTs being listed. This must be non-zero, and is expected to + * be `1` for ERC-721 NFTs. + * @param currency The currency in which the price must be paid when buying the listed NFTs. + * @param pricePerToken The price to pay per unit of NFTs listed. + * @param startTimestamp The UNIX timestamp at and after which NFTs can be bought from the listing. + * @param endTimestamp The UNIX timestamp at and after which NFTs cannot be bought from the listing. + * @param reserved Whether the listing is reserved to be bought from a specific set of buyers. + * @param status The status of the listing (created, completed, or cancelled). + * @param tokenType The type of token listed (ERC-721 or ERC-1155) + */ + struct Listing { + uint256 listingId; + uint256 tokenId; + uint256 quantity; + uint256 pricePerToken; + uint128 startTimestamp; + uint128 endTimestamp; + address listingCreator; + address assetContract; + address currency; + TokenType tokenType; + Status status; + bool reserved; + } + + /// @notice Emitted when a new listing is created. + event NewListing( + address indexed listingCreator, + uint256 indexed listingId, + address indexed assetContract, + Listing listing + ); + + /// @notice Emitted when a listing is updated. + event UpdatedListing( + address indexed listingCreator, + uint256 indexed listingId, + address indexed assetContract, + Listing listing + ); + + /// @notice Emitted when a listing is cancelled. + event CancelledListing(address indexed listingCreator, uint256 indexed listingId); + + /// @notice Emitted when a buyer is approved to buy from a reserved listing. + event BuyerApprovedForListing(uint256 indexed listingId, address indexed buyer, bool approved); + + /// @notice Emitted when a currency is approved as a form of payment for the listing. + event CurrencyApprovedForListing(uint256 indexed listingId, address indexed currency, uint256 pricePerToken); + + /// @notice Emitted when NFTs are bought from a listing. + event NewSale( + address indexed listingCreator, + uint256 indexed listingId, + address indexed assetContract, + uint256 tokenId, + address buyer, + uint256 quantityBought, + uint256 totalPricePaid + ); + + /** + * @notice List NFTs (ERC721 or ERC1155) for sale at a fixed price. + * + * @param _params The parameters of a listing a seller sets when creating a listing. + * + * @return listingId The unique integer ID of the listing. + */ + function createListing(ListingParameters memory _params) external returns (uint256 listingId); + + /** + * @notice Update parameters of a listing of NFTs. + * + * @param _listingId The ID of the listing to update. + * @param _params The parameters of a listing a seller sets when updating a listing. + */ + function updateListing(uint256 _listingId, ListingParameters memory _params) external; + + /** + * @notice Cancel a listing. + * + * @param _listingId The ID of the listing to cancel. + */ + function cancelListing(uint256 _listingId) external; + + /** + * @notice Approve a buyer to buy from a reserved listing. + * + * @param _listingId The ID of the listing to update. + * @param _buyer The address of the buyer to approve to buy from the listing. + * @param _toApprove Whether to approve the buyer to buy from the listing. + */ + function approveBuyerForListing(uint256 _listingId, address _buyer, bool _toApprove) external; + + /** + * @notice Approve a currency as a form of payment for the listing. + * + * @param _listingId The ID of the listing to update. + * @param _currency The address of the currency to approve as a form of payment for the listing. + * @param _pricePerTokenInCurrency The price per token for the currency to approve. + */ + function approveCurrencyForListing( + uint256 _listingId, + address _currency, + uint256 _pricePerTokenInCurrency + ) external; + + /** + * @notice Buy NFTs from a listing. + * + * @param _listingId The ID of the listing to update. + * @param _buyFor The recipient of the NFTs being bought. + * @param _quantity The quantity of NFTs to buy from the listing. + * @param _currency The currency to use to pay for NFTs. + * @param _expectedTotalPrice The expected total price to pay for the NFTs being bought. + */ + function buyFromListing( + uint256 _listingId, + address _buyFor, + uint256 _quantity, + address _currency, + uint256 _expectedTotalPrice + ) external payable; + + /** + * @notice Returns the total number of listings created. + * @dev At any point, the return value is the ID of the next listing created. + */ + function totalListings() external view returns (uint256); + + /// @notice Returns all listings between the start and end Id (both inclusive) provided. + function getAllListings(uint256 _startId, uint256 _endId) external view returns (Listing[] memory listings); + + /** + * @notice Returns all valid listings between the start and end Id (both inclusive) provided. + * A valid listing is where the listing creator still owns and has approved Marketplace + * to transfer the listed NFTs. + */ + function getAllValidListings(uint256 _startId, uint256 _endId) external view returns (Listing[] memory listings); + + /** + * @notice Returns a listing at the provided listing ID. + * + * @param _listingId The ID of the listing to fetch. + */ + function getListing(uint256 _listingId) external view returns (Listing memory listing); +} + +/** + * The `EnglishAuctions` extension smart contract lets you sell NFTs (ERC-721 or ERC-1155) in an english auction. + */ + +interface IEnglishAuctions { + enum TokenType { + ERC721, + ERC1155 + } + + enum Status { + UNSET, + CREATED, + COMPLETED, + CANCELLED + } + + /** + * @notice The parameters a seller sets when creating an auction listing. + * + * @param assetContract The address of the smart contract of the NFTs being auctioned. + * @param tokenId The tokenId of the NFTs being auctioned. + * @param quantity The quantity of NFTs being auctioned. This must be non-zero, and is expected to + * be `1` for ERC-721 NFTs. + * @param currency The currency in which the bid must be made when bidding for the auctioned NFTs. + * @param minimumBidAmount The minimum bid amount for the auction. + * @param buyoutBidAmount The total bid amount for which the bidder can directly purchase the auctioned items and close the auction as a result. + * @param timeBufferInSeconds This is a buffer e.g. x seconds. If a new winning bid is made less than x seconds before expirationTimestamp, the + * expirationTimestamp is increased by x seconds. + * @param bidBufferBps This is a buffer in basis points e.g. x%. To be considered as a new winning bid, a bid must be at least x% greater than + * the current winning bid. + * @param startTimestamp The timestamp at and after which bids can be made to the auction + * @param endTimestamp The timestamp at and after which bids cannot be made to the auction. + */ + struct AuctionParameters { + address assetContract; + uint256 tokenId; + uint256 quantity; + address currency; + uint256 minimumBidAmount; + uint256 buyoutBidAmount; + uint64 timeBufferInSeconds; + uint64 bidBufferBps; + uint64 startTimestamp; + uint64 endTimestamp; + } + + /** + * @notice The information stored for an auction. + * + * @param auctionId The unique ID of the auction. + * @param auctionCreator The creator of the auction. + * @param assetContract The address of the smart contract of the NFTs being auctioned. + * @param tokenId The tokenId of the NFTs being auctioned. + * @param quantity The quantity of NFTs being auctioned. This must be non-zero, and is expected to + * be `1` for ERC-721 NFTs. + * @param currency The currency in which the bid must be made when bidding for the auctioned NFTs. + * @param minimumBidAmount The minimum bid amount for the auction. + * @param buyoutBidAmount The total bid amount for which the bidder can directly purchase the auctioned items and close the auction as a result. + * @param timeBufferInSeconds This is a buffer e.g. x seconds. If a new winning bid is made less than x seconds before expirationTimestamp, the + * expirationTimestamp is increased by x seconds. + * @param bidBufferBps This is a buffer in basis points e.g. x%. To be considered as a new winning bid, a bid must be at least x% greater than + * the current winning bid. + * @param startTimestamp The timestamp at and after which bids can be made to the auction + * @param endTimestamp The timestamp at and after which bids cannot be made to the auction. + * @param status The status of the auction (created, completed, or cancelled). + * @param tokenType The type of NFTs auctioned (ERC-721 or ERC-1155) + */ + struct Auction { + uint256 auctionId; + uint256 tokenId; + uint256 quantity; + uint256 minimumBidAmount; + uint256 buyoutBidAmount; + uint64 timeBufferInSeconds; + uint64 bidBufferBps; + uint64 startTimestamp; + uint64 endTimestamp; + address auctionCreator; + address assetContract; + address currency; + TokenType tokenType; + Status status; + } + + /** + * @notice The information stored for a bid made in an auction. + * + * @param auctionId The unique ID of the auction. + * @param bidder The address of the bidder. + * @param bidAmount The total bid amount (in the currency specified by the auction). + */ + struct Bid { + uint256 auctionId; + address bidder; + uint256 bidAmount; + } + + struct AuctionPayoutStatus { + bool paidOutAuctionTokens; + bool paidOutBidAmount; + } + + /// @dev Emitted when a new auction is created. + event NewAuction( + address indexed auctionCreator, + uint256 indexed auctionId, + address indexed assetContract, + Auction auction + ); + + /// @dev Emitted when a new bid is made in an auction. + event NewBid( + uint256 indexed auctionId, + address indexed bidder, + address indexed assetContract, + uint256 bidAmount, + Auction auction + ); + + /// @notice Emitted when a auction is cancelled. + event CancelledAuction(address indexed auctionCreator, uint256 indexed auctionId); + + /// @dev Emitted when an auction is closed. + event AuctionClosed( + uint256 indexed auctionId, + address indexed assetContract, + address indexed closer, + uint256 tokenId, + address auctionCreator, + address winningBidder + ); + + /** + * @notice Put up NFTs (ERC721 or ERC1155) for an english auction. + * + * @param _params The parameters of an auction a seller sets when creating an auction. + * + * @return auctionId The unique integer ID of the auction. + */ + function createAuction(AuctionParameters memory _params) external returns (uint256 auctionId); + + /** + * @notice Cancel an auction. + * + * @param _auctionId The ID of the auction to cancel. + */ + function cancelAuction(uint256 _auctionId) external; + + /** + * @notice Distribute the winning bid amount to the auction creator. + * + * @param _auctionId The ID of an auction. + */ + function collectAuctionPayout(uint256 _auctionId) external; + + /** + * @notice Distribute the auctioned NFTs to the winning bidder. + * + * @param _auctionId The ID of an auction. + */ + function collectAuctionTokens(uint256 _auctionId) external; + + /** + * @notice Bid in an active auction. + * + * @param _auctionId The ID of the auction to bid in. + * @param _bidAmount The bid amount in the currency specified by the auction. + */ + function bidInAuction(uint256 _auctionId, uint256 _bidAmount) external payable; + + /** + * @notice Returns whether a given bid amount would make for a winning bid in an auction. + * + * @param _auctionId The ID of an auction. + * @param _bidAmount The bid amount to check. + */ + function isNewWinningBid(uint256 _auctionId, uint256 _bidAmount) external view returns (bool); + + /// @notice Returns the auction of the provided auction ID. + function getAuction(uint256 _auctionId) external view returns (Auction memory auction); + + /// @notice Returns all non-cancelled auctions. + function getAllAuctions(uint256 _startId, uint256 _endId) external view returns (Auction[] memory auctions); + + /// @notice Returns all active auctions. + function getAllValidAuctions(uint256 _startId, uint256 _endId) external view returns (Auction[] memory auctions); + + /// @notice Returns the winning bid of an active auction. + function getWinningBid( + uint256 _auctionId + ) external view returns (address bidder, address currency, uint256 bidAmount); + + /// @notice Returns whether an auction is active. + function isAuctionExpired(uint256 _auctionId) external view returns (bool); +} + +/** + * The `Offers` extension smart contract lets you make and accept offers made for NFTs (ERC-721 or ERC-1155). + */ + +interface IOffers { + enum TokenType { + ERC721, + ERC1155, + ERC20 + } + + enum Status { + UNSET, + CREATED, + COMPLETED, + CANCELLED + } + + /** + * @notice The parameters an offeror sets when making an offer for NFTs. + * + * @param assetContract The contract of the NFTs for which the offer is being made. + * @param tokenId The tokenId of the NFT for which the offer is being made. + * @param quantity The quantity of NFTs wanted. + * @param currency The currency offered for the NFTs. + * @param totalPrice The total offer amount for the NFTs. + * @param expirationTimestamp The timestamp at and after which the offer cannot be accepted. + */ + struct OfferParams { + address assetContract; + uint256 tokenId; + uint256 quantity; + address currency; + uint256 totalPrice; + uint256 expirationTimestamp; + } + + /** + * @notice The information stored for the offer made. + * + * @param offerId The ID of the offer. + * @param offeror The address of the offeror. + * @param assetContract The contract of the NFTs for which the offer is being made. + * @param tokenId The tokenId of the NFT for which the offer is being made. + * @param quantity The quantity of NFTs wanted. + * @param currency The currency offered for the NFTs. + * @param totalPrice The total offer amount for the NFTs. + * @param expirationTimestamp The timestamp at and after which the offer cannot be accepted. + * @param status The status of the offer (created, completed, or cancelled). + * @param tokenType The type of token (ERC-721 or ERC-1155) the offer is made for. + */ + struct Offer { + uint256 offerId; + uint256 tokenId; + uint256 quantity; + uint256 totalPrice; + uint256 expirationTimestamp; + address offeror; + address assetContract; + address currency; + TokenType tokenType; + Status status; + } + + /// @dev Emitted when a new offer is created. + event NewOffer(address indexed offeror, uint256 indexed offerId, address indexed assetContract, Offer offer); + + /// @dev Emitted when an offer is cancelled. + event CancelledOffer(address indexed offeror, uint256 indexed offerId); + + /// @dev Emitted when an offer is accepted. + event AcceptedOffer( + address indexed offeror, + uint256 indexed offerId, + address indexed assetContract, + uint256 tokenId, + address seller, + uint256 quantityBought, + uint256 totalPricePaid + ); + + /** + * @notice Make an offer for NFTs (ERC-721 or ERC-1155) + * + * @param _params The parameters of an offer. + * + * @return offerId The unique integer ID assigned to the offer. + */ + function makeOffer(OfferParams memory _params) external returns (uint256 offerId); + + /** + * @notice Cancel an offer. + * + * @param _offerId The ID of the offer to cancel. + */ + function cancelOffer(uint256 _offerId) external; + + /** + * @notice Accept an offer. + * + * @param _offerId The ID of the offer to accept. + */ + function acceptOffer(uint256 _offerId) external; + + /// @notice Returns an offer for the given offer ID. + function getOffer(uint256 _offerId) external view returns (Offer memory offer); + + /// @notice Returns all active (i.e. non-expired or cancelled) offers. + function getAllOffers(uint256 _startId, uint256 _endId) external view returns (Offer[] memory offers); + + /// @notice Returns all valid offers. An offer is valid if the offeror owns and has approved Marketplace to transfer the offer amount of currency. + function getAllValidOffers(uint256 _startId, uint256 _endId) external view returns (Offer[] memory offers); +} diff --git a/contracts/prebuilts/marketplace/direct-listings/DirectListingsLogic.sol b/contracts/prebuilts/marketplace/direct-listings/DirectListingsLogic.sol new file mode 100644 index 000000000..c0df04765 --- /dev/null +++ b/contracts/prebuilts/marketplace/direct-listings/DirectListingsLogic.sol @@ -0,0 +1,579 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +import "./DirectListingsStorage.sol"; + +// ====== External imports ====== +import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import "../../../eip/interface/IERC721.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/interfaces/IERC2981.sol"; + +// ====== Internal imports ====== + +import "../../../extension/interface/IPlatformFee.sol"; +import "../../../extension/upgradeable/ERC2771ContextConsumer.sol"; +import "../../../extension/upgradeable/ReentrancyGuard.sol"; +import "../../../extension/upgradeable/PermissionsEnumerable.sol"; +import { RoyaltyPaymentsLogic } from "../../../extension/upgradeable/RoyaltyPayments.sol"; +import { CurrencyTransferLib } from "../../../lib/CurrencyTransferLib.sol"; + +/** + * @author thirdweb.com + */ +contract DirectListingsLogic is IDirectListings, ReentrancyGuard, ERC2771ContextConsumer { + /*/////////////////////////////////////////////////////////////// + Constants / Immutables + //////////////////////////////////////////////////////////////*/ + + /// @dev Only lister role holders can create listings, when listings are restricted by lister address. + bytes32 private constant LISTER_ROLE = keccak256("LISTER_ROLE"); + /// @dev Only assets from NFT contracts with asset role can be listed, when listings are restricted by asset address. + bytes32 private constant ASSET_ROLE = keccak256("ASSET_ROLE"); + + /// @dev The max bps of the contract. So, 10_000 == 100 % + uint64 private constant MAX_BPS = 10_000; + + address private constant DEFAULT_FEE_RECIPIENT = 0x1Af20C6B23373350aD464700B5965CE4B0D2aD94; + uint16 private constant DEFAULT_FEE_BPS = 100; + + /// @dev The address of the native token wrapper contract. + address private immutable nativeTokenWrapper; + + /*/////////////////////////////////////////////////////////////// + Modifier + //////////////////////////////////////////////////////////////*/ + + /// @dev Checks whether the caller has LISTER_ROLE. + modifier onlyListerRole() { + require(Permissions(address(this)).hasRoleWithSwitch(LISTER_ROLE, _msgSender()), "!LISTER_ROLE"); + _; + } + + /// @dev Checks whether the caller has ASSET_ROLE. + modifier onlyAssetRole(address _asset) { + require(Permissions(address(this)).hasRoleWithSwitch(ASSET_ROLE, _asset), "!ASSET_ROLE"); + _; + } + + /// @dev Checks whether caller is a listing creator. + modifier onlyListingCreator(uint256 _listingId) { + require( + _directListingsStorage().listings[_listingId].listingCreator == _msgSender(), + "Marketplace: not listing creator." + ); + _; + } + + /// @dev Checks whether a listing exists. + modifier onlyExistingListing(uint256 _listingId) { + require( + _directListingsStorage().listings[_listingId].status == IDirectListings.Status.CREATED, + "Marketplace: invalid listing." + ); + _; + } + + /*/////////////////////////////////////////////////////////////// + Constructor logic + //////////////////////////////////////////////////////////////*/ + + constructor(address _nativeTokenWrapper) { + nativeTokenWrapper = _nativeTokenWrapper; + } + + /*/////////////////////////////////////////////////////////////// + External functions + //////////////////////////////////////////////////////////////*/ + + /// @notice List NFTs (ERC721 or ERC1155) for sale at a fixed price. + function createListing( + ListingParameters calldata _params + ) external onlyListerRole onlyAssetRole(_params.assetContract) returns (uint256 listingId) { + listingId = _getNextListingId(); + address listingCreator = _msgSender(); + TokenType tokenType = _getTokenType(_params.assetContract); + + uint128 startTime = _params.startTimestamp; + uint128 endTime = _params.endTimestamp; + require(startTime < endTime, "Marketplace: endTimestamp not greater than startTimestamp."); + if (startTime < block.timestamp) { + require(startTime + 60 minutes >= block.timestamp, "Marketplace: invalid startTimestamp."); + + startTime = uint128(block.timestamp); + endTime = endTime == type(uint128).max + ? endTime + : startTime + (_params.endTimestamp - _params.startTimestamp); + } + + _validateNewListing(_params, tokenType); + + Listing memory listing = Listing({ + listingId: listingId, + listingCreator: listingCreator, + assetContract: _params.assetContract, + tokenId: _params.tokenId, + quantity: _params.quantity, + currency: _params.currency, + pricePerToken: _params.pricePerToken, + startTimestamp: startTime, + endTimestamp: endTime, + reserved: _params.reserved, + tokenType: tokenType, + status: IDirectListings.Status.CREATED + }); + + _directListingsStorage().listings[listingId] = listing; + + emit NewListing(listingCreator, listingId, _params.assetContract, listing); + } + + /// @notice Update parameters of a listing of NFTs. + function updateListing( + uint256 _listingId, + ListingParameters memory _params + ) external onlyExistingListing(_listingId) onlyAssetRole(_params.assetContract) onlyListingCreator(_listingId) { + address listingCreator = _msgSender(); + Listing memory listing = _directListingsStorage().listings[_listingId]; + TokenType tokenType = _getTokenType(_params.assetContract); + + require(listing.endTimestamp > block.timestamp, "Marketplace: listing expired."); + + require( + listing.assetContract == _params.assetContract && listing.tokenId == _params.tokenId, + "Marketplace: cannot update what token is listed." + ); + + uint128 startTime = _params.startTimestamp; + uint128 endTime = _params.endTimestamp; + require(startTime < endTime, "Marketplace: endTimestamp not greater than startTimestamp."); + require( + listing.startTimestamp > block.timestamp || + (startTime == listing.startTimestamp && endTime > block.timestamp), + "Marketplace: listing already active." + ); + if (startTime != listing.startTimestamp && startTime < block.timestamp) { + require(startTime + 60 minutes >= block.timestamp, "Marketplace: invalid startTimestamp."); + + startTime = uint128(block.timestamp); + + endTime = endTime == listing.endTimestamp || endTime == type(uint128).max + ? endTime + : startTime + (_params.endTimestamp - _params.startTimestamp); + } + + { + uint256 _approvedCurrencyPrice = _directListingsStorage().currencyPriceForListing[_listingId][ + _params.currency + ]; + require( + _approvedCurrencyPrice == 0 || _params.pricePerToken == _approvedCurrencyPrice, + "Marketplace: price different from approved price" + ); + } + + _validateNewListing(_params, tokenType); + + listing = Listing({ + listingId: _listingId, + listingCreator: listingCreator, + assetContract: _params.assetContract, + tokenId: _params.tokenId, + quantity: _params.quantity, + currency: _params.currency, + pricePerToken: _params.pricePerToken, + startTimestamp: startTime, + endTimestamp: endTime, + reserved: _params.reserved, + tokenType: tokenType, + status: IDirectListings.Status.CREATED + }); + + _directListingsStorage().listings[_listingId] = listing; + + emit UpdatedListing(listingCreator, _listingId, _params.assetContract, listing); + } + + /// @notice Cancel a listing. + function cancelListing(uint256 _listingId) external onlyExistingListing(_listingId) onlyListingCreator(_listingId) { + _directListingsStorage().listings[_listingId].status = IDirectListings.Status.CANCELLED; + emit CancelledListing(_msgSender(), _listingId); + } + + /// @notice Approve a buyer to buy from a reserved listing. + function approveBuyerForListing( + uint256 _listingId, + address _buyer, + bool _toApprove + ) external onlyExistingListing(_listingId) onlyListingCreator(_listingId) { + require(_directListingsStorage().listings[_listingId].reserved, "Marketplace: listing not reserved."); + + _directListingsStorage().isBuyerApprovedForListing[_listingId][_buyer] = _toApprove; + + emit BuyerApprovedForListing(_listingId, _buyer, _toApprove); + } + + /// @notice Approve a currency as a form of payment for the listing. + function approveCurrencyForListing( + uint256 _listingId, + address _currency, + uint256 _pricePerTokenInCurrency + ) external onlyExistingListing(_listingId) onlyListingCreator(_listingId) { + Listing memory listing = _directListingsStorage().listings[_listingId]; + require( + _currency != listing.currency || _pricePerTokenInCurrency == listing.pricePerToken, + "Marketplace: approving listing currency with different price." + ); + require( + _directListingsStorage().currencyPriceForListing[_listingId][_currency] != _pricePerTokenInCurrency, + "Marketplace: price unchanged." + ); + + _directListingsStorage().currencyPriceForListing[_listingId][_currency] = _pricePerTokenInCurrency; + + emit CurrencyApprovedForListing(_listingId, _currency, _pricePerTokenInCurrency); + } + + /// @notice Buy NFTs from a listing. + function buyFromListing( + uint256 _listingId, + address _buyFor, + uint256 _quantity, + address _currency, + uint256 _expectedTotalPrice + ) external payable nonReentrant onlyExistingListing(_listingId) { + Listing memory listing = _directListingsStorage().listings[_listingId]; + address buyer = _msgSender(); + + require( + !listing.reserved || _directListingsStorage().isBuyerApprovedForListing[_listingId][buyer], + "buyer not approved" + ); + require(_quantity > 0 && _quantity <= listing.quantity, "Buying invalid quantity"); + require( + block.timestamp < listing.endTimestamp && block.timestamp >= listing.startTimestamp, + "not within sale window." + ); + + require( + _validateOwnershipAndApproval( + listing.listingCreator, + listing.assetContract, + listing.tokenId, + _quantity, + listing.tokenType + ), + "Marketplace: not owner or approved tokens." + ); + + uint256 targetTotalPrice; + + if (_directListingsStorage().currencyPriceForListing[_listingId][_currency] > 0) { + targetTotalPrice = _quantity * _directListingsStorage().currencyPriceForListing[_listingId][_currency]; + } else { + require(_currency == listing.currency, "Paying in invalid currency."); + targetTotalPrice = _quantity * listing.pricePerToken; + } + + require(targetTotalPrice == _expectedTotalPrice, "Unexpected total price"); + + // Check: buyer owns and has approved sufficient currency for sale. + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { + require(msg.value == targetTotalPrice, "Marketplace: msg.value must exactly be the total price."); + } else { + require(msg.value == 0, "Marketplace: invalid native tokens sent."); + _validateERC20BalAndAllowance(buyer, _currency, targetTotalPrice); + } + + if (listing.quantity == _quantity) { + _directListingsStorage().listings[_listingId].status = IDirectListings.Status.COMPLETED; + } + _directListingsStorage().listings[_listingId].quantity -= _quantity; + + _payout(buyer, listing.listingCreator, _currency, targetTotalPrice, listing); + _transferListingTokens(listing.listingCreator, _buyFor, _quantity, listing); + + emit NewSale( + listing.listingCreator, + listing.listingId, + listing.assetContract, + listing.tokenId, + buyer, + _quantity, + targetTotalPrice + ); + } + + /*/////////////////////////////////////////////////////////////// + View functions + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the total number of listings created. + * @dev At any point, the return value is the ID of the next listing created. + */ + function totalListings() external view returns (uint256) { + return _directListingsStorage().totalListings; + } + + /// @notice Returns whether a buyer is approved for a listing. + function isBuyerApprovedForListing(uint256 _listingId, address _buyer) external view returns (bool) { + return _directListingsStorage().isBuyerApprovedForListing[_listingId][_buyer]; + } + + /// @notice Returns whether a currency is approved for a listing. + function isCurrencyApprovedForListing(uint256 _listingId, address _currency) external view returns (bool) { + return _directListingsStorage().currencyPriceForListing[_listingId][_currency] > 0; + } + + /// @notice Returns the price per token for a listing, in the given currency. + function currencyPriceForListing(uint256 _listingId, address _currency) external view returns (uint256) { + if (_directListingsStorage().currencyPriceForListing[_listingId][_currency] == 0) { + revert("Currency not approved for listing"); + } + + return _directListingsStorage().currencyPriceForListing[_listingId][_currency]; + } + + /// @notice Returns all non-cancelled listings. + function getAllListings(uint256 _startId, uint256 _endId) external view returns (Listing[] memory _allListings) { + require(_startId <= _endId && _endId < _directListingsStorage().totalListings, "invalid range"); + + _allListings = new Listing[](_endId - _startId + 1); + + for (uint256 i = _startId; i <= _endId; i += 1) { + _allListings[i - _startId] = _directListingsStorage().listings[i]; + } + } + + /** + * @notice Returns all valid listings between the start and end Id (both inclusive) provided. + * A valid listing is where the listing creator still owns and has approved Marketplace + * to transfer the listed NFTs. + */ + function getAllValidListings( + uint256 _startId, + uint256 _endId + ) external view returns (Listing[] memory _validListings) { + require(_startId <= _endId && _endId < _directListingsStorage().totalListings, "invalid range"); + + Listing[] memory _listings = new Listing[](_endId - _startId + 1); + uint256 _listingCount; + + for (uint256 i = _startId; i <= _endId; i += 1) { + _listings[i - _startId] = _directListingsStorage().listings[i]; + if (_validateExistingListing(_listings[i - _startId])) { + _listingCount += 1; + } + } + + _validListings = new Listing[](_listingCount); + uint256 index = 0; + uint256 count = _listings.length; + for (uint256 i = 0; i < count; i += 1) { + if (_validateExistingListing(_listings[i])) { + _validListings[index++] = _listings[i]; + } + } + } + + /// @notice Returns a listing at a particular listing ID. + function getListing(uint256 _listingId) external view returns (Listing memory listing) { + listing = _directListingsStorage().listings[_listingId]; + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the next listing Id. + function _getNextListingId() internal returns (uint256 id) { + id = _directListingsStorage().totalListings; + _directListingsStorage().totalListings += 1; + } + + /// @dev Returns the interface supported by a contract. + function _getTokenType(address _assetContract) internal view returns (TokenType tokenType) { + if (IERC165(_assetContract).supportsInterface(type(IERC1155).interfaceId)) { + tokenType = TokenType.ERC1155; + } else if (IERC165(_assetContract).supportsInterface(type(IERC721).interfaceId)) { + tokenType = TokenType.ERC721; + } else { + revert("Marketplace: listed token must be ERC1155 or ERC721."); + } + } + + /// @dev Checks whether the listing creator owns and has approved marketplace to transfer listed tokens. + function _validateNewListing(ListingParameters memory _params, TokenType _tokenType) internal view { + require(_params.quantity > 0, "Marketplace: listing zero quantity."); + require(_params.quantity == 1 || _tokenType == TokenType.ERC1155, "Marketplace: listing invalid quantity."); + + require( + _validateOwnershipAndApproval( + _msgSender(), + _params.assetContract, + _params.tokenId, + _params.quantity, + _tokenType + ), + "Marketplace: not owner or approved tokens." + ); + } + + /// @dev Checks whether the listing exists, is active, and if the lister has sufficient balance. + function _validateExistingListing(Listing memory _targetListing) internal view returns (bool isValid) { + isValid = + _targetListing.startTimestamp <= block.timestamp && + _targetListing.endTimestamp > block.timestamp && + _targetListing.status == IDirectListings.Status.CREATED && + _validateOwnershipAndApproval( + _targetListing.listingCreator, + _targetListing.assetContract, + _targetListing.tokenId, + _targetListing.quantity, + _targetListing.tokenType + ); + } + + /// @dev Validates that `_tokenOwner` owns and has approved Marketplace to transfer NFTs. + function _validateOwnershipAndApproval( + address _tokenOwner, + address _assetContract, + uint256 _tokenId, + uint256 _quantity, + TokenType _tokenType + ) internal view returns (bool isValid) { + address market = address(this); + + if (_tokenType == TokenType.ERC1155) { + isValid = + IERC1155(_assetContract).balanceOf(_tokenOwner, _tokenId) >= _quantity && + IERC1155(_assetContract).isApprovedForAll(_tokenOwner, market); + } else if (_tokenType == TokenType.ERC721) { + address owner; + address operator; + + // failsafe for reverts in case of non-existent tokens + try IERC721(_assetContract).ownerOf(_tokenId) returns (address _owner) { + owner = _owner; + + // Nesting the approval check inside this try block, to run only if owner check doesn't revert. + // If the previous check for owner fails, then the return value will always evaluate to false. + try IERC721(_assetContract).getApproved(_tokenId) returns (address _operator) { + operator = _operator; + } catch {} + } catch {} + + isValid = + owner == _tokenOwner && + (operator == market || IERC721(_assetContract).isApprovedForAll(_tokenOwner, market)); + } + } + + /// @dev Validates that `_tokenOwner` owns and has approved Markeplace to transfer the appropriate amount of currency + function _validateERC20BalAndAllowance(address _tokenOwner, address _currency, uint256 _amount) internal view { + require( + IERC20(_currency).balanceOf(_tokenOwner) >= _amount && + IERC20(_currency).allowance(_tokenOwner, address(this)) >= _amount, + "!BAL20" + ); + } + + /// @dev Transfers tokens listed for sale in a direct or auction listing. + function _transferListingTokens(address _from, address _to, uint256 _quantity, Listing memory _listing) internal { + if (_listing.tokenType == TokenType.ERC1155) { + IERC1155(_listing.assetContract).safeTransferFrom(_from, _to, _listing.tokenId, _quantity, ""); + } else if (_listing.tokenType == TokenType.ERC721) { + IERC721(_listing.assetContract).safeTransferFrom(_from, _to, _listing.tokenId, ""); + } + } + + /// @dev Pays out stakeholders in a sale. + function _payout( + address _payer, + address _payee, + address _currencyToUse, + uint256 _totalPayoutAmount, + Listing memory _listing + ) internal { + address _nativeTokenWrapper = nativeTokenWrapper; + uint256 amountRemaining; + + // Payout platform fee + { + uint256 platformFeesTw = (_totalPayoutAmount * DEFAULT_FEE_BPS) / MAX_BPS; + (address platformFeeRecipient, uint16 platformFeeBps) = IPlatformFee(address(this)).getPlatformFeeInfo(); + uint256 platformFeeCut = (_totalPayoutAmount * platformFeeBps) / MAX_BPS; + + // Transfer platform fee + CurrencyTransferLib.transferCurrencyWithWrapper( + _currencyToUse, + _payer, + DEFAULT_FEE_RECIPIENT, + platformFeesTw, + _nativeTokenWrapper + ); + CurrencyTransferLib.transferCurrencyWithWrapper( + _currencyToUse, + _payer, + platformFeeRecipient, + platformFeeCut, + _nativeTokenWrapper + ); + + amountRemaining = _totalPayoutAmount - platformFeeCut - platformFeesTw; + } + + // Payout royalties + { + // Get royalty recipients and amounts + (address payable[] memory recipients, uint256[] memory amounts) = RoyaltyPaymentsLogic(address(this)) + .getRoyalty(_listing.assetContract, _listing.tokenId, _totalPayoutAmount); + + uint256 royaltyRecipientCount = recipients.length; + + if (royaltyRecipientCount != 0) { + uint256 royaltyCut; + address royaltyRecipient; + + for (uint256 i = 0; i < royaltyRecipientCount; ) { + royaltyRecipient = recipients[i]; + royaltyCut = amounts[i]; + + // Check payout amount remaining is enough to cover royalty payment + require(amountRemaining >= royaltyCut, "fees exceed the price"); + + // Transfer royalty + CurrencyTransferLib.transferCurrencyWithWrapper( + _currencyToUse, + _payer, + royaltyRecipient, + royaltyCut, + _nativeTokenWrapper + ); + + unchecked { + amountRemaining -= royaltyCut; + ++i; + } + } + } + } + + // Distribute price to token owner + CurrencyTransferLib.transferCurrencyWithWrapper( + _currencyToUse, + _payer, + _payee, + amountRemaining, + _nativeTokenWrapper + ); + } + + /// @dev Returns the DirectListings storage. + function _directListingsStorage() internal pure returns (DirectListingsStorage.Data storage data) { + data = DirectListingsStorage.data(); + } +} diff --git a/contracts/prebuilts/marketplace/direct-listings/DirectListingsStorage.sol b/contracts/prebuilts/marketplace/direct-listings/DirectListingsStorage.sol new file mode 100644 index 000000000..b9dfd1485 --- /dev/null +++ b/contracts/prebuilts/marketplace/direct-listings/DirectListingsStorage.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +import { IDirectListings } from "../IMarketplace.sol"; + +/** + * @author thirdweb.com + */ +library DirectListingsStorage { + /// @custom:storage-location erc7201:direct.listings.storage + /// @dev keccak256(abi.encode(uint256(keccak256("direct.listings.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant DIRECT_LISTINGS_STORAGE_POSITION = + 0xa5370dfa5e46a36b8e1214352e211aa04006b977c8fd45a98e6b8c6e230ba000; + + struct Data { + uint256 totalListings; + mapping(uint256 => IDirectListings.Listing) listings; + mapping(uint256 => mapping(address => bool)) isBuyerApprovedForListing; + mapping(uint256 => mapping(address => uint256)) currencyPriceForListing; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = DIRECT_LISTINGS_STORAGE_POSITION; + assembly { + data_.slot := position + } + } +} diff --git a/contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsLogic.sol b/contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsLogic.sol new file mode 100644 index 000000000..c1a51ffb8 --- /dev/null +++ b/contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsLogic.sol @@ -0,0 +1,546 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +import "./EnglishAuctionsStorage.sol"; + +// ====== External imports ====== +import "@openzeppelin/contracts/utils/Context.sol"; +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import "../../../eip/interface/IERC721.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/interfaces/IERC2981.sol"; + +// ====== Internal imports ====== + +import "../../../extension/interface/IPlatformFee.sol"; +import "../../../extension/upgradeable/ERC2771ContextConsumer.sol"; +import "../../../extension/upgradeable/ReentrancyGuard.sol"; +import "../../../extension/upgradeable/PermissionsEnumerable.sol"; +import { RoyaltyPaymentsLogic } from "../../../extension/upgradeable/RoyaltyPayments.sol"; +import { CurrencyTransferLib } from "../../../lib/CurrencyTransferLib.sol"; + +/** + * @author thirdweb.com + */ +contract EnglishAuctionsLogic is IEnglishAuctions, ReentrancyGuard, ERC2771ContextConsumer { + /*/////////////////////////////////////////////////////////////// + Constants / Immutables + //////////////////////////////////////////////////////////////*/ + + /// @dev Only lister role holders can create auctions, when auctions are restricted by lister address. + bytes32 private constant LISTER_ROLE = keccak256("LISTER_ROLE"); + /// @dev Only assets from NFT contracts with asset role can be auctioned, when auctions are restricted by asset address. + bytes32 private constant ASSET_ROLE = keccak256("ASSET_ROLE"); + + /// @dev The max bps of the contract. So, 10_000 == 100 % + uint64 private constant MAX_BPS = 10_000; + + address private constant DEFAULT_FEE_RECIPIENT = 0x1Af20C6B23373350aD464700B5965CE4B0D2aD94; + uint16 private constant DEFAULT_FEE_BPS = 100; + + /// @dev The address of the native token wrapper contract. + address private immutable nativeTokenWrapper; + + /*/////////////////////////////////////////////////////////////// + Modifiers + //////////////////////////////////////////////////////////////*/ + + modifier onlyListerRole() { + require(Permissions(address(this)).hasRoleWithSwitch(LISTER_ROLE, _msgSender()), "!LISTER_ROLE"); + _; + } + + modifier onlyAssetRole(address _asset) { + require(Permissions(address(this)).hasRoleWithSwitch(ASSET_ROLE, _asset), "!ASSET_ROLE"); + _; + } + + /// @dev Checks whether caller is a auction creator. + modifier onlyAuctionCreator(uint256 _auctionId) { + require( + _englishAuctionsStorage().auctions[_auctionId].auctionCreator == _msgSender(), + "Marketplace: not auction creator." + ); + _; + } + + /// @dev Checks whether an auction exists. + modifier onlyExistingAuction(uint256 _auctionId) { + require( + _englishAuctionsStorage().auctions[_auctionId].status == IEnglishAuctions.Status.CREATED, + "Marketplace: invalid auction." + ); + _; + } + + /*/////////////////////////////////////////////////////////////// + Constructor logic + //////////////////////////////////////////////////////////////*/ + + constructor(address _nativeTokenWrapper) { + nativeTokenWrapper = _nativeTokenWrapper; + } + + /*/////////////////////////////////////////////////////////////// + External functions + //////////////////////////////////////////////////////////////*/ + + /// @notice Auction ERC721 or ERC1155 NFTs. + function createAuction( + AuctionParameters calldata _params + ) external onlyListerRole onlyAssetRole(_params.assetContract) nonReentrant returns (uint256 auctionId) { + auctionId = _getNextAuctionId(); + address auctionCreator = _msgSender(); + TokenType tokenType = _getTokenType(_params.assetContract); + + _validateNewAuction(_params, tokenType); + + Auction memory auction = Auction({ + auctionId: auctionId, + auctionCreator: auctionCreator, + assetContract: _params.assetContract, + tokenId: _params.tokenId, + quantity: _params.quantity, + currency: _params.currency, + minimumBidAmount: _params.minimumBidAmount, + buyoutBidAmount: _params.buyoutBidAmount, + timeBufferInSeconds: _params.timeBufferInSeconds, + bidBufferBps: _params.bidBufferBps, + startTimestamp: _params.startTimestamp, + endTimestamp: _params.endTimestamp, + tokenType: tokenType, + status: IEnglishAuctions.Status.CREATED + }); + + _englishAuctionsStorage().auctions[auctionId] = auction; + + _transferAuctionTokens(auctionCreator, address(this), auction); + + emit NewAuction(auctionCreator, auctionId, _params.assetContract, auction); + } + + function bidInAuction( + uint256 _auctionId, + uint256 _bidAmount + ) external payable nonReentrant onlyExistingAuction(_auctionId) { + Auction memory _targetAuction = _englishAuctionsStorage().auctions[_auctionId]; + + require( + _targetAuction.endTimestamp > block.timestamp && _targetAuction.startTimestamp <= block.timestamp, + "Marketplace: inactive auction." + ); + require(_bidAmount != 0, "Marketplace: Bidding with zero amount."); + require( + _targetAuction.currency == CurrencyTransferLib.NATIVE_TOKEN || msg.value == 0, + "Marketplace: invalid native tokens sent." + ); + require( + _bidAmount <= _targetAuction.buyoutBidAmount || _targetAuction.buyoutBidAmount == 0, + "Marketplace: Bidding above buyout price." + ); + + Bid memory newBid = Bid({ auctionId: _auctionId, bidder: _msgSender(), bidAmount: _bidAmount }); + + _handleBid(_targetAuction, newBid); + } + + function collectAuctionPayout(uint256 _auctionId) external nonReentrant { + require( + !_englishAuctionsStorage().payoutStatus[_auctionId].paidOutBidAmount, + "Marketplace: payout already completed." + ); + _englishAuctionsStorage().payoutStatus[_auctionId].paidOutBidAmount = true; + + Auction memory _targetAuction = _englishAuctionsStorage().auctions[_auctionId]; + Bid memory _winningBid = _englishAuctionsStorage().winningBid[_auctionId]; + + require(_targetAuction.status != IEnglishAuctions.Status.CANCELLED, "Marketplace: invalid auction."); + require(_targetAuction.endTimestamp <= block.timestamp, "Marketplace: auction still active."); + require(_winningBid.bidder != address(0), "Marketplace: no bids were made."); + + _closeAuctionForAuctionCreator(_targetAuction, _winningBid); + + if (_targetAuction.status != IEnglishAuctions.Status.COMPLETED) { + _englishAuctionsStorage().auctions[_auctionId].status = IEnglishAuctions.Status.COMPLETED; + } + } + + function collectAuctionTokens(uint256 _auctionId) external nonReentrant { + Auction memory _targetAuction = _englishAuctionsStorage().auctions[_auctionId]; + Bid memory _winningBid = _englishAuctionsStorage().winningBid[_auctionId]; + + require(_targetAuction.status != IEnglishAuctions.Status.CANCELLED, "Marketplace: invalid auction."); + require(_targetAuction.endTimestamp <= block.timestamp, "Marketplace: auction still active."); + require(_winningBid.bidder != address(0), "Marketplace: no bids were made."); + + _closeAuctionForBidder(_targetAuction, _winningBid); + + if (_targetAuction.status != IEnglishAuctions.Status.COMPLETED) { + _englishAuctionsStorage().auctions[_auctionId].status = IEnglishAuctions.Status.COMPLETED; + } + } + + /// @dev Cancels an auction. + function cancelAuction( + uint256 _auctionId + ) external onlyExistingAuction(_auctionId) onlyAuctionCreator(_auctionId) nonReentrant { + Auction memory _targetAuction = _englishAuctionsStorage().auctions[_auctionId]; + Bid memory _winningBid = _englishAuctionsStorage().winningBid[_auctionId]; + + require(_winningBid.bidder == address(0), "Marketplace: bids already made."); + + _englishAuctionsStorage().auctions[_auctionId].status = IEnglishAuctions.Status.CANCELLED; + + _transferAuctionTokens(address(this), _targetAuction.auctionCreator, _targetAuction); + + emit CancelledAuction(_targetAuction.auctionCreator, _auctionId); + } + + /*/////////////////////////////////////////////////////////////// + View functions + //////////////////////////////////////////////////////////////*/ + + function isNewWinningBid( + uint256 _auctionId, + uint256 _bidAmount + ) external view onlyExistingAuction(_auctionId) returns (bool) { + Auction memory _targetAuction = _englishAuctionsStorage().auctions[_auctionId]; + Bid memory _currentWinningBid = _englishAuctionsStorage().winningBid[_auctionId]; + + return + _isNewWinningBid( + _targetAuction.minimumBidAmount, + _currentWinningBid.bidAmount, + _bidAmount, + _targetAuction.bidBufferBps + ); + } + + function totalAuctions() external view returns (uint256) { + return _englishAuctionsStorage().totalAuctions; + } + + function getAuction(uint256 _auctionId) external view returns (Auction memory _auction) { + _auction = _englishAuctionsStorage().auctions[_auctionId]; + } + + function getAllAuctions(uint256 _startId, uint256 _endId) external view returns (Auction[] memory _allAuctions) { + require(_startId <= _endId && _endId < _englishAuctionsStorage().totalAuctions, "invalid range"); + + _allAuctions = new Auction[](_endId - _startId + 1); + + for (uint256 i = _startId; i <= _endId; i += 1) { + _allAuctions[i - _startId] = _englishAuctionsStorage().auctions[i]; + } + } + + function getAllValidAuctions( + uint256 _startId, + uint256 _endId + ) external view returns (Auction[] memory _validAuctions) { + require(_startId <= _endId && _endId < _englishAuctionsStorage().totalAuctions, "invalid range"); + + Auction[] memory _auctions = new Auction[](_endId - _startId + 1); + uint256 _auctionCount; + + for (uint256 i = _startId; i <= _endId; i += 1) { + uint256 j = i - _startId; + _auctions[j] = _englishAuctionsStorage().auctions[i]; + if ( + _auctions[j].startTimestamp <= block.timestamp && + _auctions[j].endTimestamp > block.timestamp && + _auctions[j].status == IEnglishAuctions.Status.CREATED && + _auctions[j].assetContract != address(0) + ) { + _auctionCount += 1; + } + } + + _validAuctions = new Auction[](_auctionCount); + uint256 index = 0; + uint256 count = _auctions.length; + for (uint256 i = 0; i < count; i += 1) { + if ( + _auctions[i].startTimestamp <= block.timestamp && + _auctions[i].endTimestamp > block.timestamp && + _auctions[i].status == IEnglishAuctions.Status.CREATED && + _auctions[i].assetContract != address(0) + ) { + _validAuctions[index++] = _auctions[i]; + } + } + } + + function getWinningBid( + uint256 _auctionId + ) external view returns (address _bidder, address _currency, uint256 _bidAmount) { + Auction memory _targetAuction = _englishAuctionsStorage().auctions[_auctionId]; + Bid memory _currentWinningBid = _englishAuctionsStorage().winningBid[_auctionId]; + + _bidder = _currentWinningBid.bidder; + _currency = _targetAuction.currency; + _bidAmount = _currentWinningBid.bidAmount; + } + + function isAuctionExpired(uint256 _auctionId) external view onlyExistingAuction(_auctionId) returns (bool) { + return _englishAuctionsStorage().auctions[_auctionId].endTimestamp >= block.timestamp; + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the next auction Id. + function _getNextAuctionId() internal returns (uint256 id) { + id = _englishAuctionsStorage().totalAuctions; + _englishAuctionsStorage().totalAuctions += 1; + } + + /// @dev Returns the interface supported by a contract. + function _getTokenType(address _assetContract) internal view returns (TokenType tokenType) { + if (IERC165(_assetContract).supportsInterface(type(IERC1155).interfaceId)) { + tokenType = TokenType.ERC1155; + } else if (IERC165(_assetContract).supportsInterface(type(IERC721).interfaceId)) { + tokenType = TokenType.ERC721; + } else { + revert("Marketplace: auctioned token must be ERC1155 or ERC721."); + } + } + + /// @dev Checks whether the auction creator owns and has approved marketplace to transfer auctioned tokens. + function _validateNewAuction(AuctionParameters memory _params, TokenType _tokenType) internal view { + require(_params.quantity > 0, "Marketplace: auctioning zero quantity."); + require(_params.quantity == 1 || _tokenType == TokenType.ERC1155, "Marketplace: auctioning invalid quantity."); + require(_params.timeBufferInSeconds > 0, "Marketplace: no time-buffer."); + require(_params.bidBufferBps > 0, "Marketplace: no bid-buffer."); + require( + _params.startTimestamp + 60 minutes >= block.timestamp && _params.startTimestamp < _params.endTimestamp, + "Marketplace: invalid timestamps." + ); + require( + _params.buyoutBidAmount == 0 || _params.buyoutBidAmount >= _params.minimumBidAmount, + "Marketplace: invalid bid amounts." + ); + } + + /// @dev Processes an incoming bid in an auction. + function _handleBid(Auction memory _targetAuction, Bid memory _incomingBid) internal { + Bid memory currentWinningBid = _englishAuctionsStorage().winningBid[_targetAuction.auctionId]; + uint256 currentBidAmount = currentWinningBid.bidAmount; + uint256 incomingBidAmount = _incomingBid.bidAmount; + address _nativeTokenWrapper = nativeTokenWrapper; + + // Close auction and execute sale if there's a buyout price and incoming bid amount is buyout price. + if (_targetAuction.buyoutBidAmount > 0 && incomingBidAmount >= _targetAuction.buyoutBidAmount) { + incomingBidAmount = _targetAuction.buyoutBidAmount; + _incomingBid.bidAmount = _targetAuction.buyoutBidAmount; + + _closeAuctionForBidder(_targetAuction, _incomingBid); + } else { + /** + * If there's an exisitng winning bid, incoming bid amount must be bid buffer % greater. + * Else, bid amount must be at least as great as minimum bid amount + */ + require( + _isNewWinningBid( + _targetAuction.minimumBidAmount, + currentBidAmount, + incomingBidAmount, + _targetAuction.bidBufferBps + ), + "Marketplace: not winning bid." + ); + + // Update the winning bid and auction's end time before external contract calls. + _englishAuctionsStorage().winningBid[_targetAuction.auctionId] = _incomingBid; + + if (_targetAuction.endTimestamp - block.timestamp <= _targetAuction.timeBufferInSeconds) { + _targetAuction.endTimestamp += _targetAuction.timeBufferInSeconds; + _englishAuctionsStorage().auctions[_targetAuction.auctionId] = _targetAuction; + } + } + + // Payout previous highest bid. + if (currentWinningBid.bidder != address(0) && currentBidAmount > 0) { + CurrencyTransferLib.transferCurrencyWithWrapper( + _targetAuction.currency, + address(this), + currentWinningBid.bidder, + currentBidAmount, + _nativeTokenWrapper + ); + } + + // Collect incoming bid + CurrencyTransferLib.transferCurrencyWithWrapper( + _targetAuction.currency, + _incomingBid.bidder, + address(this), + incomingBidAmount, + _nativeTokenWrapper + ); + + emit NewBid( + _targetAuction.auctionId, + _incomingBid.bidder, + _targetAuction.assetContract, + _incomingBid.bidAmount, + _targetAuction + ); + } + + /// @dev Checks whether an incoming bid is the new current highest bid. + function _isNewWinningBid( + uint256 _minimumBidAmount, + uint256 _currentWinningBidAmount, + uint256 _incomingBidAmount, + uint256 _bidBufferBps + ) internal pure returns (bool isValidNewBid) { + if (_currentWinningBidAmount == 0) { + isValidNewBid = _incomingBidAmount >= _minimumBidAmount; + } else { + isValidNewBid = (_incomingBidAmount > _currentWinningBidAmount && + ((_incomingBidAmount - _currentWinningBidAmount) * MAX_BPS) / _currentWinningBidAmount >= + _bidBufferBps); + } + } + + /// @dev Closes an auction for the winning bidder; distributes auction items to the winning bidder. + function _closeAuctionForBidder(Auction memory _targetAuction, Bid memory _winningBid) internal { + require( + !_englishAuctionsStorage().payoutStatus[_targetAuction.auctionId].paidOutAuctionTokens, + "Marketplace: payout already completed." + ); + _englishAuctionsStorage().payoutStatus[_targetAuction.auctionId].paidOutAuctionTokens = true; + + _targetAuction.endTimestamp = uint64(block.timestamp); + + _englishAuctionsStorage().winningBid[_targetAuction.auctionId] = _winningBid; + _englishAuctionsStorage().auctions[_targetAuction.auctionId] = _targetAuction; + + _transferAuctionTokens(address(this), _winningBid.bidder, _targetAuction); + + emit AuctionClosed( + _targetAuction.auctionId, + _targetAuction.assetContract, + _msgSender(), + _targetAuction.tokenId, + _targetAuction.auctionCreator, + _winningBid.bidder + ); + } + + /// @dev Closes an auction for an auction creator; distributes winning bid amount to auction creator. + function _closeAuctionForAuctionCreator(Auction memory _targetAuction, Bid memory _winningBid) internal { + uint256 payoutAmount = _winningBid.bidAmount; + _payout(address(this), _targetAuction.auctionCreator, _targetAuction.currency, payoutAmount, _targetAuction); + + emit AuctionClosed( + _targetAuction.auctionId, + _targetAuction.assetContract, + _msgSender(), + _targetAuction.tokenId, + _targetAuction.auctionCreator, + _winningBid.bidder + ); + } + + /// @dev Transfers tokens for auction. + function _transferAuctionTokens(address _from, address _to, Auction memory _auction) internal { + if (_auction.tokenType == TokenType.ERC1155) { + IERC1155(_auction.assetContract).safeTransferFrom(_from, _to, _auction.tokenId, _auction.quantity, ""); + } else if (_auction.tokenType == TokenType.ERC721) { + IERC721(_auction.assetContract).safeTransferFrom(_from, _to, _auction.tokenId, ""); + } + } + + /// @dev Pays out stakeholders in auction. + function _payout( + address _payer, + address _payee, + address _currencyToUse, + uint256 _totalPayoutAmount, + Auction memory _targetAuction + ) internal { + address _nativeTokenWrapper = nativeTokenWrapper; + uint256 amountRemaining; + + // Payout platform fee + { + uint256 platformFeesTw = (_totalPayoutAmount * DEFAULT_FEE_BPS) / MAX_BPS; + (address platformFeeRecipient, uint16 platformFeeBps) = IPlatformFee(address(this)).getPlatformFeeInfo(); + uint256 platformFeeCut = (_totalPayoutAmount * platformFeeBps) / MAX_BPS; + + // Transfer platform fee + CurrencyTransferLib.transferCurrencyWithWrapper( + _currencyToUse, + _payer, + DEFAULT_FEE_RECIPIENT, + platformFeesTw, + _nativeTokenWrapper + ); + CurrencyTransferLib.transferCurrencyWithWrapper( + _currencyToUse, + _payer, + platformFeeRecipient, + platformFeeCut, + _nativeTokenWrapper + ); + + amountRemaining = _totalPayoutAmount - platformFeeCut - platformFeesTw; + } + + // Payout royalties + { + // Get royalty recipients and amounts + (address payable[] memory recipients, uint256[] memory amounts) = RoyaltyPaymentsLogic(address(this)) + .getRoyalty(_targetAuction.assetContract, _targetAuction.tokenId, _totalPayoutAmount); + + uint256 royaltyRecipientCount = recipients.length; + + if (royaltyRecipientCount != 0) { + uint256 royaltyCut; + address royaltyRecipient; + + for (uint256 i = 0; i < royaltyRecipientCount; ) { + royaltyRecipient = recipients[i]; + royaltyCut = amounts[i]; + + // Check payout amount remaining is enough to cover royalty payment + require(amountRemaining >= royaltyCut, "fees exceed the price"); + + // Transfer royalty + CurrencyTransferLib.transferCurrencyWithWrapper( + _currencyToUse, + _payer, + royaltyRecipient, + royaltyCut, + _nativeTokenWrapper + ); + + unchecked { + amountRemaining -= royaltyCut; + ++i; + } + } + } + } + + // Distribute price to token owner + CurrencyTransferLib.transferCurrencyWithWrapper( + _currencyToUse, + _payer, + _payee, + amountRemaining, + _nativeTokenWrapper + ); + } + + /// @dev Returns the EnglishAuctions storage. + function _englishAuctionsStorage() internal pure returns (EnglishAuctionsStorage.Data storage data) { + data = EnglishAuctionsStorage.data(); + } +} diff --git a/contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsStorage.sol b/contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsStorage.sol new file mode 100644 index 000000000..d886289ce --- /dev/null +++ b/contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsStorage.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +import { IEnglishAuctions } from "../IMarketplace.sol"; + +/** + * @author thirdweb.com + */ +library EnglishAuctionsStorage { + /// @custom:storage-location erc7201:english.auctions.storage + /// @dev keccak256(abi.encode(uint256(keccak256("english.auctions.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant ENGLISH_AUCTIONS_STORAGE_POSITION = + 0x89032daddd224983b4d69fda31dc440901185d9636f6e798dbe1e433d9d34c00; + + struct Data { + uint256 totalAuctions; + mapping(uint256 => IEnglishAuctions.Auction) auctions; + mapping(uint256 => IEnglishAuctions.Bid) winningBid; + mapping(uint256 => IEnglishAuctions.AuctionPayoutStatus) payoutStatus; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = ENGLISH_AUCTIONS_STORAGE_POSITION; + assembly { + data_.slot := position + } + } +} diff --git a/contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol b/contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol new file mode 100644 index 000000000..b560a057b --- /dev/null +++ b/contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// ====== External imports ====== +import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; + +import { ERC721Holder } from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; +import { ERC1155Holder, ERC1155Receiver } from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; + +// ========== Internal imports ========== +import { BaseRouter, IRouter, IRouterState } from "@thirdweb-dev/dynamic-contracts/src/presets/BaseRouter.sol"; +import { ERC165 } from "../../../eip/ERC165.sol"; + +import "../../../extension/Multicall.sol"; +import "../../../extension/upgradeable/Initializable.sol"; +import "../../../extension/upgradeable/ContractMetadata.sol"; +import "../../../extension/upgradeable/PlatformFee.sol"; +import "../../../extension/upgradeable/PermissionsEnumerable.sol"; +import "../../../extension/upgradeable/init/ReentrancyGuardInit.sol"; +import "../../../extension/upgradeable/ERC2771ContextUpgradeable.sol"; +import { RoyaltyPaymentsLogic } from "../../../extension/upgradeable/RoyaltyPayments.sol"; + +/** + * @author thirdweb.com + */ +contract MarketplaceV3 is + Initializable, + Multicall, + BaseRouter, + ContractMetadata, + PlatformFee, + PermissionsEnumerable, + ReentrancyGuardInit, + ERC2771ContextUpgradeable, + RoyaltyPaymentsLogic, + ERC721Holder, + ERC1155Holder, + ERC165 +{ + /// @dev Only EXTENSION_ROLE holders can perform upgrades. + bytes32 private constant EXTENSION_ROLE = keccak256("EXTENSION_ROLE"); + + bytes32 private constant MODULE_TYPE = bytes32("MarketplaceV3"); + uint256 private constant VERSION = 3; + + /// @dev The address of the native token wrapper contract. + address private immutable nativeTokenWrapper; + + address public constant DEFAULT_FEE_RECIPIENT = 0x1Af20C6B23373350aD464700B5965CE4B0D2aD94; + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + /// @dev We accept constructor params as a struct to avoid `Stack too deep` errors. + struct MarketplaceConstructorParams { + Extension[] extensions; + address royaltyEngineAddress; + address nativeTokenWrapper; + } + + constructor( + MarketplaceConstructorParams memory _marketplaceV3Params + ) BaseRouter(_marketplaceV3Params.extensions) RoyaltyPaymentsLogic(_marketplaceV3Params.royaltyEngineAddress) { + nativeTokenWrapper = _marketplaceV3Params.nativeTokenWrapper; + _disableInitializers(); + } + + receive() external payable { + assert(msg.sender == nativeTokenWrapper); // only accept ETH via fallback from the native token wrapper contract + } + + /// @dev Initializes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _contractURI, + address[] memory _trustedForwarders, + address _platformFeeRecipient, + uint16 _platformFeeBps + ) external initializer { + // Initialize BaseRouter + __BaseRouter_init(); + + // Initialize inherited contracts, most base-like -> most derived. + __ReentrancyGuard_init(); + __ERC2771Context_init(_trustedForwarders); + + // Initialize this contract's state. + _setupContractURI(_contractURI); + _setupPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + _setupRole(EXTENSION_ROLE, _defaultAdmin); + _setupRole(keccak256("LISTER_ROLE"), address(0)); + _setupRole(keccak256("ASSET_ROLE"), address(0)); + + _setupRole(EXTENSION_ROLE, _defaultAdmin); + _setRoleAdmin(EXTENSION_ROLE, EXTENSION_ROLE); + } + + /*/////////////////////////////////////////////////////////////// + Generic contract logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the type of the contract. + function contractType() external pure returns (bytes32) { + return MODULE_TYPE; + } + + /// @dev Returns the version of the contract. + function contractVersion() external pure returns (uint8) { + return uint8(VERSION); + } + + /*/////////////////////////////////////////////////////////////// + ERC 165 / 721 / 1155 logic + //////////////////////////////////////////////////////////////*/ + + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC165, IERC165, ERC1155Receiver) returns (bool) { + return + interfaceId == type(IERC1155Receiver).interfaceId || + interfaceId == type(IERC721Receiver).interfaceId || + interfaceId == type(IRouter).interfaceId || + interfaceId == type(IRouterState).interfaceId || + super.supportsInterface(interfaceId); + } + + /*/////////////////////////////////////////////////////////////// + Overridable Permissions + //////////////////////////////////////////////////////////////*/ + + /// @dev Checks whether platform fee info can be set in the given execution context. + function _canSetPlatformFeeInfo() internal view override returns (bool) { + return _hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view override returns (bool) { + return _hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Returns whether royalty engine address can be set in the given execution context. + function _canSetRoyaltyEngine() internal view override returns (bool) { + return _hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether an account has a particular role. + function _hasRole(bytes32 _role, address _account) internal view returns (bool) { + PermissionsStorage.Data storage data = PermissionsStorage.data(); + return data._hasRole[_role][_account]; + } + + /// @dev Returns whether all relevant permission and other checks are met before any upgrade. + function _isAuthorizedCallToUpgrade() internal view virtual override returns (bool) { + return _hasRole(EXTENSION_ROLE, msg.sender); + } + + function _msgSender() + internal + view + override(ERC2771ContextUpgradeable, Permissions, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } + + function _msgData() internal view override(ERC2771ContextUpgradeable, Permissions) returns (bytes calldata) { + return ERC2771ContextUpgradeable._msgData(); + } +} diff --git a/contracts/prebuilts/marketplace/marketplace-v3.md b/contracts/prebuilts/marketplace/marketplace-v3.md new file mode 100644 index 000000000..d224b95fe --- /dev/null +++ b/contracts/prebuilts/marketplace/marketplace-v3.md @@ -0,0 +1,794 @@ +# Marketplace V3 design document. + +This is a live document that explains what the [thirdweb](https://thirdweb.com/) `Marketplace V3` smart contract is, how it works and can be used, and why it is written the way it is. + +The document is written for technical and non-technical readers. To ask further questions about `Marketplace V3`, please join the [thirdweb discord](https://discord.gg/thirdweb) or create a [github issue](https://github.com/thirdweb-dev/contracts/issues). + +--- + +## Background + +The [thirdweb](https://thirdweb.com/) `Marketplace V3` is a marketplace where where people can sell NFTs — [ERC 721](https://eips.ethereum.org/EIPS/eip-721) or [ERC 1155](https://eips.ethereum.org/EIPS/eip-1155) tokens — at a fixed price ( what we'll refer to as a "Direct listing"), or auction them (what we'll refer to as an "Auction listing"). It also allows users to make "Offers" on unlisted NFTs. + +`Marketplace V3` offers improvements over previous version in terms of design and features, which are discussed in this document. You can refer to previous (v2) `Marketplace` design document [here](https://github.com/thirdweb-dev/contracts/blob/main/contracts/prebuilts/marketplace-legacy/marketplace.md). + +## Context behind this update + +We have given `Marketplace` an update that was long overdue. The marketplace product is still made up of three core ways of exchanging NFTs for money: + +1. Selling NFTs via a ‘direct listing’. +2. Auctioning off NFTs. +3. Making offers for NFTs not on sale at all, or at favorable prices. + +The core improvement about the `Marketplace V3` smart contract is better developer experience of working with the contract. + +Previous version had some limitations, arising due to (1) the smart contract size limit of `~24.576 kb` on Ethereum mainnet (and other thirdweb supported chains), and (2) the way the smart contract code is organized (single, large smart contract that inherits other contracts). The previous `Marketplace` smart contract has functions that have multiple jobs, behave in many different ways under different circumstances, and a lack of convenient view functions to read data easily. + +Moreover, over time, we received feature requests for `Marketplace`, some of which have been incorporated in `Marketplace V3`, for e.g.: + +- Ability to accept multiple currencies for direct listings +- Ability to explicitly cancel listings +- Explicit getter functions for fetching high level states e.g. “has an auction ended”, “who is the winning bidder”, etc. +- Simplify start time and expiration time for listings + +For all these reasons and feature additions, the `Marketplace` contract is getting an update, and being rolled out as `Marketplace V3`. In this update: + +- the contract has been broken down into independent extensions (later offered in ContractKit). +- the contract provides explicit functions for each important action (something that is missing from the contract, today). +- the contract provides convenient view functions for all relevant state of the contract, without expecting users to rely on events to read critical information. + +Finally, to accomplish all these things without the constraint of the smart contract size limit, the `Marketplace V3` contract is written in the following new code pattern, which we call `Plugin Pattern`. It was influenced by [EIP-2535](https://eips.ethereum.org/EIPS/eip-2535). You can read more about Plugin Pattern [here](https://blog.thirdweb.com/). + +## Extensions that make up `Marketplace V3` + +The `Marketplace V3` smart contract is now written as the sum of three main extension smart contracts: + +1. `DirectListings`: List NFTs for sale at a fixed price. Buy NFTs from listings. +2. `EnglishAuctions`: Put NFTs up for auction. Bid for NFTs up on auction. The highest bid within an auction’s duration wins. +3. `Offers`: Make offers of ERC20 or native token currency for NFTs. Accept a favorable offer if you own the NFTs wanted. + +Each of these extension smart contracts is independent, and does not care about the state of the other extension contracts. + +### What the Marketplace will look like to users + +There are two groups of users — (1) thirdweb's customers who'll set up the marketplace, and (2) the end users of thirdweb customers' marketplaces. + +To thirdweb customers, the marketplace can be set up like any of the other thirdweb contract (e.g. 'NFT Collection') through the thirdweb dashboard, the thirdweb SDK, or by directly consuming the open sourced marketplace smart contract. + +To the end users of thirdweb customers, the experience of using the marketplace will feel familiar to popular marketplace platforms like OpenSea, Zora, etc. The biggest difference in user experience will be that performing any action on the marketplace requires gas fees. + +- Thirdweb's customers + - Deploy the marketplace contract like any other thirdweb contract. + - Can set a % 'platform fee'. This % is collected on every sale — when a buyer buys tokens from a direct listing, and when a seller collects the highest bid on auction closing. This platform fee is distributed to the platform fee recipient (set by a contract admin). + - Can list NFTs for sale at a fixed price. + - Can edit an existing listing's parameters, e.g. the currency accepted. An auction's parameters cannot be edited once it has started. + - Can make offers to NFTs listed/unlisted for a fixed price. + - Can auction NFTs. + - Can make bids to auctions. + - Must pay gas fees to perform any actions, including the actions just listed. + +### EIPs implemented / supported + +To be able to escrow NFTs in the case of auctions, Marketplace implements the receiver interfaces for [ERC1155](https://eips.ethereum.org/EIPS/eip-1155) and [ERC721](https://eips.ethereum.org/EIPS/eip-721) tokens. + +To enable meta-transactions (gasless), Marketplace implements [ERC2771](https://eips.ethereum.org/EIPS/eip-2771). + +Marketplace also honors [ERC2981](https://eips.ethereum.org/EIPS/eip-2981) for the distribution of royalties on direct and auction listings. + +### Events emitted + +All events emitted by the contract, as well as when they're emitted, can be found in the interface of the contract, [here](https://github.com/thirdweb-dev/contracts/blob/main/contracts/prebuilts/marketplace/IMarketplace.sol). In general, events are emitted whenever there is a state change in the contract. + +### Currency transfers + +The contract supports both ERC20 currencies and a chain's native token (e.g. ether for Ethereum mainnet). This means that any action that involves transferring currency (e.g. buying a token from a direct listing) can be performed with either an ERC20 token or the chain's native token. + +💡 **Note**: The exception is offers — these can only be made with ERC20 tokens, since Marketplace needs to transfer the offer amount from the buyer to the seller, in case the latter accepts the offer. This cannot be done with native tokens without escrowing the requisite amount of currency. + +The contract wraps all native tokens deposited into it as the canonical ERC20 wrapped version of the native token (e.g. WETH for ether). The contract unwraps the wrapped native token when transferring native tokens to a given address. + +If the contract fails to transfer out native tokens, it wraps them back to wrapped native tokens, and transfers the wrapped native tokens to the concerned address. The contract may fail to transfer out native tokens to an address, if the address represents a smart contract that cannot accept native tokens transferred to it directly. + +# API Reference for Extensions + +## Direct listings + +The `DirectListings` extension smart contract lets you buy and sell NFTs (ERC-721 or ERC-1155) for a fixed price. + +### `createListing` + +**What:** List NFTs (ERC721 or ERC1155) for sale at a fixed price. + +- Interface + + ```solidity + struct ListingParameters { + address assetContract; + uint256 tokenId; + uint256 quantity; + address currency; + uint256 pricePerToken; + uint128 startTimestamp; + uint128 endTimestamp; + bool reserved; + } + + function createListing(ListingParameters memory params) external returns (uint256 listingId); + + ``` + +- Parameters + | Parameter | Description | + | ------------- | --------------------------------------------------------------------------------------------------- | + | assetContract | The address of the smart contract of the NFTs being listed. | + | tokenId | The tokenId of the NFTs being listed. | + | quantity | The quantity of NFTs being listed. This must be non-zero, and is expected to be 1 for ERC-721 NFTs. | + | currency | The currency in which the price must be paid when buying the listed NFTs. The address considered for native tokens of the chain is 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE | + | pricePerToken | The price to pay per unit of NFTs listed. | + | startTimestamp | The UNIX timestamp at and after which NFTs can be bought from the listing. | + | expirationTimestamp | The UNIX timestamp at and after which NFTs cannot be bought from the listing. | + | reserved | Whether the listing is reserved to be bought from a specific set of buyers. | +- Criteria that must be satisfied + - The listing creator must own the NFTs being listed. + - The listing creator must have already approved Marketplace to transfer the NFTs being listed (since the creator is not required to escrow NFTs in the Marketplace). + - The listing creator must list a non-zero quantity of tokens. If listing ERC-721 tokens, the listing creator must list only quantity `1`. + - The listing start time must not be less than 1+ hour before the block timestamp of the transaction. The listing end time must be after the listing start time. + - Only ERC-721 or ERC-1155 tokens must be listed. + - The listing creator must have `LISTER_ROLE` if role restrictions are active. + - The asset being listed must have `ASSET_ROLE` if role restrictions are active. + +### `updateListing` + +**What:** Update information (e.g. price) for one of your listings on the marketplace. + +- Interface + + ```solidity + struct ListingParameters { + address assetContract; + uint256 tokenId; + uint256 quantity; + address currency; + uint256 pricePerToken; + uint128 startTimestamp; + uint128 endTimestamp; + bool reserved; + } + + function updateListing(uint256 listingId, ListingParameters memory params) external + ``` + +- Parameters + | Parameter | Description | + | ------------- | --------------------------------------------------------------------------------------------------- | + | listingId | The unique ID of the listing being updated. | + | assetContract | The address of the smart contract of the NFTs being listed. | + | tokenId | The tokenId of the NFTs being listed. | + | quantity | The quantity of NFTs being listed. This must be non-zero, and is expected to be 1 for ERC-721 NFTs. | + | currency | The currency in which the price must be paid when buying the listed NFTs. The address considered for native tokens of the chain is 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE | + | pricePerToken | The price to pay per unit of NFTs listed. | + | startTimestamp | The UNIX timestamp at and after which NFTs can be bought from the listing. | + | expirationTimestamp | The UNIX timestamp at and after which NFTs cannot be bought from the listing. | + | reserved | Whether the listing is reserved to be bought from a specific set of buyers. | +- Criteria that must be satisfied + - The caller of the function _must_ be the creator of the listing being updated. + - The listing creator must own the NFTs being listed. + - The listing creator must have already approved Marketplace to transfer the NFTs being listed (since the creator is not required to escrow NFTs in the Marketplace). + - The listing creator must list a non-zero quantity of tokens. If listing ERC-721 tokens, the listing creator must list only quantity `1`. + - Only ERC-721 or ERC-1155 tokens must be listed. + - The listing start time must be greater than or equal to the incumbent start timestamp. The listing end time must be after the listing start time. + - The asset being listed must have `ASSET_ROLE` if role restrictions are active. + +### `cancelListing` + +**What:** Cancel (i.e. delete) one of your listings on the marketplace. + +- Interface + + ```solidity + function cancelListing(uint256 listingId) external; + + ``` + +- Parameters + | Parameter | Description | + | --------- | --------------------------------------------------- | + | listingId | The unique ID of the listing to cancel i.e. delete. | +- Criteria that must be satisfied + - The caller of the function _must_ be the creator of the listing being cancelled. + - The listing must exist. + +### `approveBuyerForListing` + +**What:** Approve a buyer to buy from a reserved listing. + +- Interface + + ```solidity + function approveBuyerForListing( + uint256 listingId, + address buyer, + bool toApprove + ) external; + + ``` + +- Parameters + | Parameter | Description | + | --------- | ------------------------------------------------------------ | + | listingId | The unique ID of the listing. | + | buyer | The address of the buyer to approve to buy from the listing. | + | toApprove | Whether to approve the buyer to buy from the listing. | +- Criteria that must be satisfied + - The caller of the function _must_ be the creator of the listing in question. + - The listing must be reserved. + +### `approveCurrencyForListing` + +**What:** Approve a currency as a form of payment for the listing. + +- Interface + + ```solidity + function approveCurrencyForListing( + uint256 listingId, + address currency, + uint256 pricePerTokenInCurrency, + ) external; + + ``` + +- Parameters + | Parameter | Description | + | ----------------------- | ---------------------------------------------------------------------------- | + | listingId | The unique ID of the listing. | + | currency | The address of the currency to approve as a form of payment for the listing. | + | pricePerTokenInCurrency | The price per token for the currency to approve. A value of 0 here disapprove a currency. | +- Criteria that must be satisfied + - The caller of the function _must_ be the creator of the listing in question. + - The currency being approved must not be the main currency accepted by the listing. + +### `buyFromListing` + +**What:** Buy NFTs from a listing. + +- Interface + + ```solidity + function buyFromListing( + uint256 listingId, + address buyFor, + uint256 quantity, + address currency, + uint256 expectedTotalPrice + ) external payable; + + ``` + +- Parameters + | Parameter | Description | + | ------------------ | ---------------------------------------------------------- | + | listingId | The unique ID of the listing to buy NFTs from. | + | buyFor | The recipient of the NFTs being bought. | + | quantity | The quantity of NFTs to buy from the listing. | + | currency | The currency to use to pay for NFTs. | + | expectedTotalPrice | The expected total price to pay for the NFTs being bought. | +- Criteria that must be satisfied + - The buyer must own the total price amount to pay for the NFTs being bought. + - The buyer must approve the Marketplace to transfer the total price amount to pay for the NFTs being bought. + - If paying in native tokens, the buyer must send exactly the expected total price amount of native tokens along with the transaction. + - The buyer’s expected total price must match the actual total price for the NFTs being bought. + - The buyer must buy a non-zero quantity of NFTs. + - The buyer must not attempt to buy more NFTs than are listed at the time. + - The buyer must pay in a currency approved by the listing creator. + +### `totalListings` + +**What:** Returns the total number of listings created so far. + +- Interface + + ```solidity + function totalListings() external view returns (uint256); + + ``` + +### `getAllListings` + +**What:** Returns all listings between the start and end Id (both inclusive) provided. + +- Interface + + ```solidity + enum TokenType { + ERC721, + ERC1155 + } + + enum Status { + UNSET, + CREATED, + COMPLETED, + CANCELLED + } + + struct Listing { + uint256 listingId; + address listingCreator; + address assetContract; + uint256 tokenId; + uint256 quantity; + address currency; + uint256 pricePerToken; + uint128 startTimestamp; + uint128 endTimestamp; + bool reserved; + TokenType tokenType; + Status status; + } + + function getAllListings(uint256 startId, uint256 endId) external view returns (Listing[] memory listings); + + ``` + +- Parameters + | Parameter | Description | + | --------- | -------------------------- | + | startId | Inclusive start listing Id | + | endId | Inclusive end listing Id | + +### `getAllValidListings` + +**What:** Returns all valid listings between the start and end Id (both inclusive) provided. A valid listing is where the listing is active, as well as the creator still owns and has approved Marketplace to transfer the listed NFTs. + +- Interface + + ```solidity + function getAllValidListings(uint256 startId, uint256 endId) external view returns (Listing[] memory listings); + + ``` + +- Parameters + | Parameter | Description | + | --------- | -------------------------- | + | startId | Inclusive start listing Id | + | endId | Inclusive end listing Id | + +### `getListing` + +**What:** Returns a listing at the provided listing ID. + +- Interface + + ```solidity + function getListing(uint256 listingId) external view returns (Listing memory listing); + + ``` + +- Parameters + | Parameter | Description | + | --------- | ------------------------------- | + | listingId | The ID of the listing to fetch. | + +## English auctions + +The `EnglishAuctions` extension smart contract lets you sell NFTs (ERC-721 or ERC-1155) in an english auction. + +### `createAuction` + +**What:** Put up NFTs (ERC721 or ERC1155) for an english auction. + +- **What is an English auction?** + - `Alice` deposits her NFTs in the Marketplace contract and specifies: **[1]** a minimum bid amount, and **[2]** a duration for the auction. + - `Bob` is the first person to make a bid. + - _Before_ the auction duration ends, `Bob` makes a bid in the auction (≥ minimum bid). + - `Bob`'s bid is now deposited and locked in the Marketplace. + - `Tom` also wants the auctioned NFTs. `Tom`'s bid _must_ be greater than `Bob`'s bid. + - _Before_ the auction duration ends, `Tom` makes a bid in the auction (≥ `Bob`'s bid). + - `Tom`'s bid is now deposited and locked in the Marketplace. `Bob`'s is _automatically_ refunded his bid. + - _After_ the auction duration ends: + - `Alice` collects the highest bid that has been deposited in Marketplace. + - The “highest bidder” e.g. `Tom` collects the auctioned NFTs. +- Interface + + ```solidity + struct AuctionParameters { + address assetContract; + uint256 tokenId; + uint256 quantity; + address currency; + uint256 minimumBidAmount; + uint256 buyoutBidAmount; + uint64 timeBufferInSeconds; + uint64 bidBufferBps; + uint64 startTimestamp; + uint64 endTimestamp; + } + + function createAuction(AuctionParameters memory params) external returns (uint256 auctionId); + + ``` + +- Parameters + | Parameter | Description | + | ------------------- | ---------------------------------------------------------------------------------------------------------------------- | + | assetContract | The address of the smart contract of the NFTs being auctioned. | + | tokenId | The tokenId of the NFTs being auctioned. | + | quantity | The quantity of NFTs being auctioned. This must be non-zero, and is expected to be 1 for ERC-721 NFTs. | + | currency | The currency in which the bid must be made when bidding for the auctioned NFTs. | + | minimumBidAmount | The minimum bid amount for the auction. | + | buyoutBidAmount | The total bid amount for which the bidder can directly purchase the auctioned items and close the auction as a result. | + | timeBufferInSeconds | This is a buffer e.g. x seconds. If a new winning bid is made less than x seconds before expirationTimestamp, the expirationTimestamp is increased by x seconds. | + | bidBufferBps | This is a buffer in basis points e.g. x%. To be considered as a new winning bid, a bid must be at least x% greater than the current winning bid. | + | startTimestamp | The timestamp at and after which bids can be made to the auction | + | expirationTimestamp | The timestamp at and after which bids cannot be made to the auction. | +- Criteria that must be satisfied + - The auction creator must own and approve Marketplace to transfer the auctioned tokens to itself. + - The auction creator must auction a non-zero quantity of tokens. If the auctioned token is ERC721, the quantity must be `1`. + - The auction creator must specify a non-zero time and bid buffers. + - The minimum bid amount must be less than the buyout bid amount. + - The auction start time must not be less than 1+ hour before the block timestamp of the transaction. The auction end time must be after the auction start time. + - The auctioned token must be ERC-721 or ERC-1155. + - The auction creator must have `LISTER_ROLE` if role restrictions are active. + - The asset being auctioned must have `ASSET_ROLE` if role restrictions are active. + +### `cancelAuction` + +**What:** Cancel an auction. + +- Interface + + ```solidity + function cancelAuction(uint256 auctionId) external; + + ``` + +- Parameters + | Parameter | Description | + | --------- | --------------------------------------- | + | auctionId | The unique ID of the auction to cancel. | +- Criteria that must be satisfied + - The caller of the function must be the auction creator. + - There must be no bids placed in the ongoing auction. (Default true for all auctions that haven’t started) + +### `collectAuctionPayout` + +**What:** Once the auction ends, collect the highest bid made for your auctioned NFTs. + +- Interface + + ```solidity + function collectAuctionPayout(uint256 auctionId) external; + + ``` + +- Parameters + | Parameter | Description | + | --------- | ------------------------------------------------------- | + | auctionId | The unique ID of the auction to collect the payout for. | +- Criteria that must be satisfied + - The auction must be expired. + - The auction must have received at least one valid bid. + +### `collectAuctionTokens` + +**What:** Once the auction ends, collect the auctioned NFTs for which you were the highest bidder. + +- Interface + + ```solidity + function collectAuctionTokens(uint256 auctionId) external; + + ``` + +- Parameters + | Parameter | Description | + | --------- | ------------------------------------------------------- | + | auctionId | The unique ID of the auction to collect the payout for. | +- Criteria that must be satisfied + - The auction must be expired. + - The caller must be the winning bidder. + +### `bidInAuction` + +**What:** Make a bid in an auction. + +- Interface + + ```solidity + function bidInAuction(uint256 auctionId, uint256 bidAmount) external payable; + + ``` + +- Parameters + | Parameter | Description | + | --------- | --------------------------------------- | + | auctionId | The unique ID of the auction to bid in. | + | bidAmount | The total bid amount. | +- Criteria that must be satisfied + - Auction must not be expired. + - The caller must own and approve Marketplace to transfer the requisite bid amount to itself. + - The bid amount must be a winning bid amount. (For convenience, this can be verified by calling `isNewWinningBid`) + +### `isNewWinningBid` + +**What:** Check whether a given bid amount would make for a new winning bid. + +- Interface + + ```solidity + function isNewWinningBid(uint256 auctionId, uint256 bidAmount) external view returns (bool); + + ``` + +- Parameters + | Parameter | Description | + | --------- | --------------------------------------- | + | auctionId | The unique ID of the auction to bid in. | + | bidAmount | The total bid amount. | +- Criteria that must be satisfied + - The auction must not have been cancelled or expired. + +### `totalAuctions` + +**What:** Returns the total number of auctions created so far. + +- Interface + + ```solidity + function totalAuctions() external view returns (uint256); + + ``` + +### `getAuction` + +**What:** Fetch the auction info at a particular auction ID. + +- Interface + + ```solidity + struct Auction { + uint256 auctionId; + address auctionCreator; + address assetContract; + uint256 tokenId; + uint256 quantity; + address currency; + uint256 minimumBidAmount; + uint256 buyoutBidAmount; + uint64 timeBufferInSeconds; + uint64 bidBufferBps; + uint64 startTimestamp; + uint64 endTimestamp; + TokenType tokenType; + Status status; + } + + function getAuction(uint256 auctionId) external view returns (Auction memory auction); + + ``` + +- Parameters + | Parameter | Description | + | --------- | ----------------------------- | + | auctionId | The unique ID of the auction. | + +### `getAllAuctions` + +**What:** Returns all auctions between the start and end Id (both inclusive) provided. + +- Interface + + ```solidity + function getAllAuctions(uint256 startId, uint256 endId) external view returns (Auction[] memory auctions); + + ``` + +- Parameters + | Parameter | Description | + | --------- | -------------------------- | + | startId | Inclusive start auction Id | + | endId | Inclusive end auction Id | + +### `getAllValidAuctions` + +**What:** Returns all valid auctions between the start and end Id (both inclusive) provided. A valid auction is where the auction is active, as well as the creator still owns and has approved Marketplace to transfer the auctioned NFTs. + +- Interface + + ```solidity + function getAllValidAuctions(uint256 startId, uint256 endId) external view returns (Auction[] memory auctions); + + ``` + +- Parameters + | Parameter | Description | + | --------- | -------------------------- | + | startId | Inclusive start auction Id | + | endId | Inclusive end auction Id | + +### `getWinningBid` + +**What:** Get the winning bid of an auction. + +- Interface + + ```solidity + function getWinningBid(uint256 auctionId) + external + view + returns ( + address bidder, + address currency, + uint256 bidAmount + ); + + ``` + +- Parameters + | Parameter | Description | + | --------- | ---------------------------- | + | auctionId | The unique ID of an auction. | + +### `isAuctionExpired` + +**What:** Returns whether an auction is expired or not. + +- Interface + + ```solidity + function isAuctionExpired(uint256 auctionId) external view returns (bool); + + ``` + +- Parameters + | Parameter | Description | + | --------- | ---------------------------- | + | auctionId | The unique ID of an auction. | + +## Offers + +### `makeOffer` + +**What:** Make an offer for any ERC721 or ERC1155 NFTs (unless `ASSET_ROLE` restrictions apply) + +- Interface + + ```solidity + struct OfferParams { + address assetContract; + uint256 tokenId; + uint256 quantity; + address currency; + uint256 totalPrice; + uint256 expirationTimestamp; + } + + function makeOffer(OfferParams memory params) external returns (uint256 offerId); + + ``` + +- Parameters + | Parameter | Description | + | ------------------- | ----------------------------------------- | + | assetContract | The contract address of the NFTs wanted. | + | tokenId | The tokenId of the NFTs wanted. | + | quantity | The quantity of NFTs wanted. | + | currency | The currency offered for the NFT wanted. | + | totalPrice | The price offered for the NFTs wanted. | + | expirationTimestamp | The timestamp at which the offer expires. | +- Criteria that must be satisfied + - The offeror must own and approve Marketplace to transfer the requisite amount currency offered for the NFTs wanted. + - The offeror must make an offer for non-zero quantity of NFTs. If offering for ERC721 tokens, the quantity wanted must be `1`. + - Expiration timestamp must be greater than block timestamp, or within 1 hour of block timestamp. + +### `cancelOffer` + +**What:** Cancel an existing offer. + +- Interface + + ```solidity + function cancelOffer(uint256 offerId) external; + + ``` + +- Parameters + | Parameter | Description | + | --------- | -------------------------- | + | offerId | The unique ID of the offer | +- Criteria that must be satisfied + - The caller of the function must be the offeror. + +### `acceptOffer` + +**What:** Accept an offer made for your NFTs. + +- Interface + + ```solidity + function acceptOffer(uint256 offerId) external; + + ``` + +- Parameters + | Parameter | Description | + | --------- | --------------------------- | + | offerId | The unique ID of the offer. | +- Criteria that must be satisfied + - The caller of the function must own and approve Marketplace to transfer the tokens for which the offer is made. + - The offeror must still own and have approved Marketplace to transfer the requisite amount currency offered for the NFTs wanted. + +### `totalOffers` + +**What:** Returns the total number of offers created so far. + +- Interface + + ```solidity + function totalOffers() external view returns (uint256); + + ``` + +### `getOffer` + +**What:** Returns the offer at a particular offer Id. + +- Interface + + ```solidity + struct Offer { + uint256 offerId; + address offeror; + address assetContract; + uint256 tokenId; + uint256 quantity; + address currency; + uint256 totalPrice; + uint256 expirationTimestamp; + TokenType tokenType; + Status status; + } + + function getOffer(uint256 offerId) external view returns (Offer memory offer); + + ``` + +- Parameters + | Parameter | Description | + | --------- | -------------------------- | + | offerId | The unique ID of an offer. | + +### `getAllOffers` + +**What:** Returns all offers between the start and end Id (both inclusive) provided. + +- Interface + + ```solidity + function getAllOffers(uint256 startId, uint256 endId) external view returns (Offer[] memory offers); + + ``` + +- Parameters + | Parameter | Description | + | --------- | -------------------------- | + | startId | Inclusive start offer Id | + | endId | Inclusive end offer Id | + +### `getAllValidOffers` + +**What:** Returns all valid offers between the start and end Id (both inclusive) provided. A valid offer is where the offer is active, as well as the offeror still owns and has approved Marketplace to transfer the currency tokens. + +- Interface + + ```solidity + function getAllValidOffer(uint256 startId, uint256 endId) external view returns (Offer[] memory offers); + + ``` + +- Parameters + | Parameter | Description | + | --------- | -------------------------- | + | startId | Inclusive start offer Id | + | endId | Inclusive end offer Id | diff --git a/contracts/prebuilts/marketplace/offers/OffersLogic.sol b/contracts/prebuilts/marketplace/offers/OffersLogic.sol new file mode 100644 index 000000000..538ead228 --- /dev/null +++ b/contracts/prebuilts/marketplace/offers/OffersLogic.sol @@ -0,0 +1,358 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +import "./OffersStorage.sol"; + +// ====== External imports ====== +import "@openzeppelin/contracts/utils/Context.sol"; +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import "../../../eip/interface/IERC721.sol"; +import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/interfaces/IERC2981.sol"; + +// ====== Internal imports ====== + +import "../../../extension/interface/IPlatformFee.sol"; +import "../../../extension/upgradeable/ERC2771ContextConsumer.sol"; +import "../../../extension/upgradeable/ReentrancyGuard.sol"; +import "../../../extension/upgradeable/PermissionsEnumerable.sol"; +import { RoyaltyPaymentsLogic } from "../../../extension/upgradeable/RoyaltyPayments.sol"; +import { CurrencyTransferLib } from "../../../lib/CurrencyTransferLib.sol"; + +/** + * @author thirdweb.com + */ +contract OffersLogic is IOffers, ReentrancyGuard, ERC2771ContextConsumer { + /*/////////////////////////////////////////////////////////////// + Constants / Immutables + //////////////////////////////////////////////////////////////*/ + /// @dev Can create offer for only assets from NFT contracts with asset role, when offers are restricted by asset address. + bytes32 private constant ASSET_ROLE = keccak256("ASSET_ROLE"); + + /// @dev The max bps of the contract. So, 10_000 == 100 % + uint64 private constant MAX_BPS = 10_000; + + address private constant DEFAULT_FEE_RECIPIENT = 0x1Af20C6B23373350aD464700B5965CE4B0D2aD94; + uint16 private constant DEFAULT_FEE_BPS = 100; + + /*/////////////////////////////////////////////////////////////// + Modifiers + //////////////////////////////////////////////////////////////*/ + + modifier onlyAssetRole(address _asset) { + require(Permissions(address(this)).hasRoleWithSwitch(ASSET_ROLE, _asset), "!ASSET_ROLE"); + _; + } + + /// @dev Checks whether caller is a offer creator. + modifier onlyOfferor(uint256 _offerId) { + require(_offersStorage().offers[_offerId].offeror == _msgSender(), "!Offeror"); + _; + } + + /// @dev Checks whether an auction exists. + modifier onlyExistingOffer(uint256 _offerId) { + require(_offersStorage().offers[_offerId].status == IOffers.Status.CREATED, "Marketplace: invalid offer."); + _; + } + + /*/////////////////////////////////////////////////////////////// + Constructor logic + //////////////////////////////////////////////////////////////*/ + + constructor() {} + + /*/////////////////////////////////////////////////////////////// + External functions + //////////////////////////////////////////////////////////////*/ + + function makeOffer( + OfferParams memory _params + ) external onlyAssetRole(_params.assetContract) returns (uint256 _offerId) { + _offerId = _getNextOfferId(); + address _offeror = _msgSender(); + TokenType _tokenType = _getTokenType(_params.assetContract); + + _validateNewOffer(_params, _tokenType); + + Offer memory _offer = Offer({ + offerId: _offerId, + offeror: _offeror, + assetContract: _params.assetContract, + tokenId: _params.tokenId, + tokenType: _tokenType, + quantity: _params.quantity, + currency: _params.currency, + totalPrice: _params.totalPrice, + expirationTimestamp: _params.expirationTimestamp, + status: IOffers.Status.CREATED + }); + + _offersStorage().offers[_offerId] = _offer; + + emit NewOffer(_offeror, _offerId, _params.assetContract, _offer); + } + + function cancelOffer(uint256 _offerId) external onlyExistingOffer(_offerId) onlyOfferor(_offerId) { + _offersStorage().offers[_offerId].status = IOffers.Status.CANCELLED; + + emit CancelledOffer(_msgSender(), _offerId); + } + + function acceptOffer(uint256 _offerId) external nonReentrant onlyExistingOffer(_offerId) { + Offer memory _targetOffer = _offersStorage().offers[_offerId]; + + require(_targetOffer.expirationTimestamp > block.timestamp, "EXPIRED"); + + require( + _validateERC20BalAndAllowance(_targetOffer.offeror, _targetOffer.currency, _targetOffer.totalPrice), + "Marketplace: insufficient currency balance." + ); + + _validateOwnershipAndApproval( + _msgSender(), + _targetOffer.assetContract, + _targetOffer.tokenId, + _targetOffer.quantity, + _targetOffer.tokenType + ); + + _offersStorage().offers[_offerId].status = IOffers.Status.COMPLETED; + + _payout(_targetOffer.offeror, _msgSender(), _targetOffer.currency, _targetOffer.totalPrice, _targetOffer); + _transferOfferTokens(_msgSender(), _targetOffer.offeror, _targetOffer.quantity, _targetOffer); + + emit AcceptedOffer( + _targetOffer.offeror, + _targetOffer.offerId, + _targetOffer.assetContract, + _targetOffer.tokenId, + _msgSender(), + _targetOffer.quantity, + _targetOffer.totalPrice + ); + } + + /*/////////////////////////////////////////////////////////////// + View functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns total number of offers + function totalOffers() public view returns (uint256) { + return _offersStorage().totalOffers; + } + + /// @dev Returns existing offer with the given uid. + function getOffer(uint256 _offerId) external view returns (Offer memory _offer) { + _offer = _offersStorage().offers[_offerId]; + } + + /// @dev Returns all existing offers within the specified range. + function getAllOffers(uint256 _startId, uint256 _endId) external view returns (Offer[] memory _allOffers) { + require(_startId <= _endId && _endId < _offersStorage().totalOffers, "invalid range"); + + _allOffers = new Offer[](_endId - _startId + 1); + + for (uint256 i = _startId; i <= _endId; i += 1) { + _allOffers[i - _startId] = _offersStorage().offers[i]; + } + } + + /// @dev Returns offers within the specified range, where offeror has sufficient balance. + function getAllValidOffers(uint256 _startId, uint256 _endId) external view returns (Offer[] memory _validOffers) { + require(_startId <= _endId && _endId < _offersStorage().totalOffers, "invalid range"); + + Offer[] memory _offers = new Offer[](_endId - _startId + 1); + uint256 _offerCount; + + for (uint256 i = _startId; i <= _endId; i += 1) { + uint256 j = i - _startId; + _offers[j] = _offersStorage().offers[i]; + if (_validateExistingOffer(_offers[j])) { + _offerCount += 1; + } + } + + _validOffers = new Offer[](_offerCount); + uint256 index = 0; + uint256 count = _offers.length; + for (uint256 i = 0; i < count; i += 1) { + if (_validateExistingOffer(_offers[i])) { + _validOffers[index++] = _offers[i]; + } + } + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the next offer Id. + function _getNextOfferId() internal returns (uint256 id) { + id = _offersStorage().totalOffers; + _offersStorage().totalOffers += 1; + } + + /// @dev Returns the interface supported by a contract. + function _getTokenType(address _assetContract) internal view returns (TokenType tokenType) { + if (IERC165(_assetContract).supportsInterface(type(IERC1155).interfaceId)) { + tokenType = TokenType.ERC1155; + } else if (IERC165(_assetContract).supportsInterface(type(IERC721).interfaceId)) { + tokenType = TokenType.ERC721; + } else { + revert("Marketplace: token must be ERC1155 or ERC721."); + } + } + + /// @dev Checks whether the auction creator owns and has approved marketplace to transfer auctioned tokens. + function _validateNewOffer(OfferParams memory _params, TokenType _tokenType) internal view { + require(_params.totalPrice > 0, "zero price."); + require(_params.quantity > 0, "Marketplace: wanted zero tokens."); + require(_params.quantity == 1 || _tokenType == TokenType.ERC1155, "Marketplace: wanted invalid quantity."); + require( + _params.expirationTimestamp + 60 minutes > block.timestamp, + "Marketplace: invalid expiration timestamp." + ); + + require( + _validateERC20BalAndAllowance(_msgSender(), _params.currency, _params.totalPrice), + "Marketplace: insufficient currency balance." + ); + } + + /// @dev Checks whether the offer exists, is active, and if the offeror has sufficient balance. + function _validateExistingOffer(Offer memory _targetOffer) internal view returns (bool isValid) { + isValid = + _targetOffer.expirationTimestamp > block.timestamp && + _targetOffer.status == IOffers.Status.CREATED && + _validateERC20BalAndAllowance(_targetOffer.offeror, _targetOffer.currency, _targetOffer.totalPrice); + } + + /// @dev Validates that `_tokenOwner` owns and has approved Marketplace to transfer NFTs. + function _validateOwnershipAndApproval( + address _tokenOwner, + address _assetContract, + uint256 _tokenId, + uint256 _quantity, + TokenType _tokenType + ) internal view { + address market = address(this); + bool isValid; + + if (_tokenType == TokenType.ERC1155) { + isValid = + IERC1155(_assetContract).balanceOf(_tokenOwner, _tokenId) >= _quantity && + IERC1155(_assetContract).isApprovedForAll(_tokenOwner, market); + } else if (_tokenType == TokenType.ERC721) { + isValid = + IERC721(_assetContract).ownerOf(_tokenId) == _tokenOwner && + (IERC721(_assetContract).getApproved(_tokenId) == market || + IERC721(_assetContract).isApprovedForAll(_tokenOwner, market)); + } + + require(isValid, "Marketplace: not owner or approved tokens."); + } + + /// @dev Validates that `_tokenOwner` owns and has approved Markeplace to transfer the appropriate amount of currency + function _validateERC20BalAndAllowance( + address _tokenOwner, + address _currency, + uint256 _amount + ) internal view returns (bool isValid) { + isValid = + IERC20(_currency).balanceOf(_tokenOwner) >= _amount && + IERC20(_currency).allowance(_tokenOwner, address(this)) >= _amount; + } + + /// @dev Transfers tokens. + function _transferOfferTokens(address _from, address _to, uint256 _quantity, Offer memory _offer) internal { + if (_offer.tokenType == TokenType.ERC1155) { + IERC1155(_offer.assetContract).safeTransferFrom(_from, _to, _offer.tokenId, _quantity, ""); + } else if (_offer.tokenType == TokenType.ERC721) { + IERC721(_offer.assetContract).safeTransferFrom(_from, _to, _offer.tokenId, ""); + } + } + + /// @dev Pays out stakeholders in a sale. + function _payout( + address _payer, + address _payee, + address _currencyToUse, + uint256 _totalPayoutAmount, + Offer memory _offer + ) internal { + uint256 amountRemaining; + + // Payout platform fee + { + uint256 platformFeesTw = (_totalPayoutAmount * DEFAULT_FEE_BPS) / MAX_BPS; + (address platformFeeRecipient, uint16 platformFeeBps) = IPlatformFee(address(this)).getPlatformFeeInfo(); + uint256 platformFeeCut = (_totalPayoutAmount * platformFeeBps) / MAX_BPS; + + // Transfer platform fee + CurrencyTransferLib.transferCurrencyWithWrapper( + _currencyToUse, + _payer, + DEFAULT_FEE_RECIPIENT, + platformFeesTw, + address(0) + ); + CurrencyTransferLib.transferCurrencyWithWrapper( + _currencyToUse, + _payer, + platformFeeRecipient, + platformFeeCut, + address(0) + ); + + amountRemaining = _totalPayoutAmount - platformFeeCut - platformFeesTw; + } + + // Payout royalties + { + // Get royalty recipients and amounts + (address payable[] memory recipients, uint256[] memory amounts) = RoyaltyPaymentsLogic(address(this)) + .getRoyalty(_offer.assetContract, _offer.tokenId, _totalPayoutAmount); + + uint256 royaltyRecipientCount = recipients.length; + + if (royaltyRecipientCount != 0) { + uint256 royaltyCut; + address royaltyRecipient; + + for (uint256 i = 0; i < royaltyRecipientCount; ) { + royaltyRecipient = recipients[i]; + royaltyCut = amounts[i]; + + // Check payout amount remaining is enough to cover royalty payment + require(amountRemaining >= royaltyCut, "fees exceed the price"); + + // Transfer royalty + CurrencyTransferLib.transferCurrencyWithWrapper( + _currencyToUse, + _payer, + royaltyRecipient, + royaltyCut, + address(0) + ); + + unchecked { + amountRemaining -= royaltyCut; + ++i; + } + } + } + } + + // Distribute price to token owner + CurrencyTransferLib.transferCurrencyWithWrapper(_currencyToUse, _payer, _payee, amountRemaining, address(0)); + } + + /// @dev Returns the Offers storage. + function _offersStorage() internal pure returns (OffersStorage.Data storage data) { + data = OffersStorage.data(); + } +} diff --git a/contracts/prebuilts/marketplace/offers/OffersStorage.sol b/contracts/prebuilts/marketplace/offers/OffersStorage.sol new file mode 100644 index 000000000..2716b9ed0 --- /dev/null +++ b/contracts/prebuilts/marketplace/offers/OffersStorage.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +import { IOffers } from "../IMarketplace.sol"; + +/** + * @author thirdweb.com + */ +library OffersStorage { + /// @custom:storage-location erc7201:offers.storage + /// @dev keccak256(abi.encode(uint256(keccak256("offers.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant OFFERS_STORAGE_POSITION = + 0x8f8effea55e8d961f30e12024b944289ed8a7f60abcf4b3989df2dc98a914300; + + struct Data { + uint256 totalOffers; + mapping(uint256 => IOffers.Offer) offers; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = OFFERS_STORAGE_POSITION; + assembly { + data_.slot := position + } + } +} diff --git a/contracts/multiwrap/Multiwrap.sol b/contracts/prebuilts/multiwrap/Multiwrap.sol similarity index 84% rename from contracts/multiwrap/Multiwrap.sol rename to contracts/prebuilts/multiwrap/Multiwrap.sol index 1b8b7b33c..e074ba1d8 100644 --- a/contracts/multiwrap/Multiwrap.sol +++ b/contracts/prebuilts/multiwrap/Multiwrap.sol @@ -1,25 +1,35 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.11; -// ========== External imports ========== -import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ -import "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; +// ========== External imports ========== +import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; // ========== Internal imports ========== -import "../interfaces/IMultiwrap.sol"; -import "../openzeppelin-presets/metatx/ERC2771ContextUpgradeable.sol"; +import "../interface/IMultiwrap.sol"; +import "../../extension/Multicall.sol"; +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; // ========== Features ========== -import "../extension/ContractMetadata.sol"; -import "../extension/Royalty.sol"; -import "../extension/Ownable.sol"; -import "../extension/PermissionsEnumerable.sol"; -import { TokenStore, ERC1155Receiver, IERC1155Receiver } from "../extension/TokenStore.sol"; +import "../../extension/ContractMetadata.sol"; +import "../../extension/Royalty.sol"; +import "../../extension/Ownable.sol"; +import "../../extension/PermissionsEnumerable.sol"; +import { TokenStore, ERC1155Receiver, IERC1155Receiver } from "../../extension/TokenStore.sol"; contract Multiwrap is Initializable, @@ -30,8 +40,8 @@ contract Multiwrap is TokenStore, ReentrancyGuardUpgradeable, ERC2771ContextUpgradeable, - MulticallUpgradeable, - ERC721Upgradeable, + Multicall, + ERC721EnumerableUpgradeable, IMultiwrap { /*/////////////////////////////////////////////////////////////// @@ -59,7 +69,7 @@ contract Multiwrap is constructor(address _nativeTokenWrapper) TokenStore(_nativeTokenWrapper) initializer {} - /// @dev Initiliazes the contract, like a constructor. + /// @dev Initializes the contract, like a constructor. function initialize( address _defaultAdmin, string memory _name, @@ -131,13 +141,9 @@ contract Multiwrap is } /// @dev See ERC 165 - function supportsInterface(bytes4 interfaceId) - public - view - virtual - override(ERC1155Receiver, ERC721Upgradeable, IERC165) - returns (bool) - { + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC1155Receiver, ERC721EnumerableUpgradeable, IERC165) returns (bool) { return super.supportsInterface(interfaceId) || interfaceId == type(IERC721Upgradeable).interfaceId || @@ -225,9 +231,10 @@ contract Multiwrap is function _beforeTokenTransfer( address from, address to, - uint256 tokenId + uint256 tokenId, + uint256 batchSize ) internal virtual override { - super._beforeTokenTransfer(from, to, tokenId); + super._beforeTokenTransfer(from, to, tokenId, batchSize); // if transfer is restricted on the contract, we still want to allow burning and minting if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { @@ -239,7 +246,7 @@ contract Multiwrap is internal view virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) + override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) returns (address sender) { return ERC2771ContextUpgradeable._msgSender(); diff --git a/contracts/multiwrap/multiwrap.md b/contracts/prebuilts/multiwrap/multiwrap.md similarity index 95% rename from contracts/multiwrap/multiwrap.md rename to contracts/prebuilts/multiwrap/multiwrap.md index 015fbdce6..9d49b82e9 100644 --- a/contracts/multiwrap/multiwrap.md +++ b/contracts/prebuilts/multiwrap/multiwrap.md @@ -20,7 +20,7 @@ The single wrapped token received on bundling up multiple assets, as mentioned a A token owner should be able to wrap any combination of *n* ERC20, ERC721 or ERC1155 tokens as a wrapped NFT. When wrapping, the token owner should be able to specify a recipient for the wrapped NFT. At the time of wrapping, the token owner should be able to set the metadata of the wrapped NFT that will be minted. -The wrapped NFT owner should be able to unwrap the the NFT to retrieve the underlying tokens of the wrapped NFT. At the time of unwrapping, the wrapped NFT owner should be able to specify a recipient for the underlying tokens of the wrapped NFT. +The wrapped NFT owner should be able to unwrap the NFT to retrieve the underlying tokens of the wrapped NFT. At the time of unwrapping, the wrapped NFT owner should be able to specify a recipient for the underlying tokens of the wrapped NFT. The `Multiwrap` contract creator should be able to apply the following role-based restrictions: @@ -136,4 +136,4 @@ What does **Type (Switch / !Switch)** mean? ## Authors - [nkrishang](https://github.com/nkrishang) -- [thirdweb team](https://github.com/thirdweb-dev) \ No newline at end of file +- [thirdweb team](https://github.com/thirdweb-dev) diff --git a/contracts/prebuilts/open-edition/OpenEditionERC721.sol b/contracts/prebuilts/open-edition/OpenEditionERC721.sol new file mode 100644 index 000000000..f802b2f14 --- /dev/null +++ b/contracts/prebuilts/open-edition/OpenEditionERC721.sol @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// ========== External imports ========== + +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; + +import "../../eip/queryable/ERC721AQueryableUpgradeable.sol"; + +// ========== Internal imports ========== + +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; +import "../../lib/CurrencyTransferLib.sol"; + +// ========== Features ========== + +import "../../extension/Multicall.sol"; +import "../../extension/ContractMetadata.sol"; +import "../../extension/Royalty.sol"; +import "../../extension/PrimarySale.sol"; +import "../../extension/Ownable.sol"; +import "../../extension/SharedMetadata.sol"; +import "../../extension/PermissionsEnumerable.sol"; +import "../../extension/Drop.sol"; + +contract OpenEditionERC721 is + Initializable, + ContractMetadata, + Royalty, + PrimarySale, + Ownable, + SharedMetadata, + PermissionsEnumerable, + Drop, + ERC2771ContextUpgradeable, + Multicall, + ERC721AQueryableUpgradeable +{ + using StringsUpgradeable for uint256; + + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. + bytes32 private transferRole; + /// @dev Only MINTER_ROLE holders can update the shared metadata of tokens. + bytes32 private minterRole; + + /// @dev Max bps in the thirdweb system. + uint256 private constant MAX_BPS = 10_000; + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + constructor() initializer {} + + /// @dev Initializes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _royaltyRecipient, + uint128 _royaltyBps + ) external initializerERC721A initializer { + bytes32 _transferRole = keccak256("TRANSFER_ROLE"); + bytes32 _minterRole = keccak256("MINTER_ROLE"); + + // Initialize inherited contracts, most base-like -> most derived. + __ERC2771Context_init(_trustedForwarders); + __ERC721A_init(_name, _symbol); + + _setupContractURI(_contractURI); + _setupOwner(_defaultAdmin); + + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + _setupRole(_minterRole, _defaultAdmin); + _setupRole(_transferRole, _defaultAdmin); + _setupRole(_transferRole, address(0)); + + _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + _setupPrimarySaleRecipient(_saleRecipient); + + transferRole = _transferRole; + minterRole = _minterRole; + } + + /*/////////////////////////////////////////////////////////////// + ERC 165 / 721 / 2981 logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the URI for a given tokenId. + function tokenURI( + uint256 _tokenId + ) public view virtual override(ERC721AUpgradeable, IERC721AUpgradeable) returns (string memory) { + if (!_exists(_tokenId)) { + revert("!ID"); + } + + return _getURIFromSharedMetadata(_tokenId); + } + + /// @dev See ERC 165 + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC721AUpgradeable, IERC165, IERC721AUpgradeable) returns (bool) { + return super.supportsInterface(interfaceId) || type(IERC2981Upgradeable).interfaceId == interfaceId; + } + + /// @dev The start token ID for the contract. + function _startTokenId() internal pure override returns (uint256) { + return 1; + } + + function startTokenId() public pure returns (uint256) { + return _startTokenId(); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Collects and distributes the primary sale value of NFTs being claimed. + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override { + if (_pricePerToken == 0) { + require(msg.value == 0, "!Value"); + return; + } + + uint256 totalPrice = _quantityToClaim * _pricePerToken; + + bool validMsgValue; + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { + validMsgValue = msg.value == totalPrice; + } else { + validMsgValue = msg.value == 0; + } + require(validMsgValue, "!V"); + + address saleRecipient = _primarySaleRecipient == address(0) ? primarySaleRecipient() : _primarySaleRecipient; + + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), saleRecipient, totalPrice); + } + + /// @dev Transfers the NFTs being claimed. + function _transferTokensOnClaim( + address _to, + uint256 _quantityBeingClaimed + ) internal override returns (uint256 startTokenId_) { + startTokenId_ = _nextTokenId(); + _safeMint(_to, _quantityBeingClaimed); + } + + /// @dev Checks whether primary sale recipient can be set in the given execution context. + function _canSetPrimarySaleRecipient() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether owner can be set in the given execution context. + function _canSetOwner() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether royalty info can be set in the given execution context. + function _canSetRoyaltyInfo() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function _canSetClaimConditions() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Returns whether the shared metadata of tokens can be set in the given execution context. + function _canSetSharedMetadata() internal view virtual override returns (bool) { + return hasRole(minterRole, _msgSender()); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + /** + * Returns the total amount of tokens minted in the contract. + */ + function totalMinted() external view returns (uint256) { + unchecked { + return _nextTokenId() - _startTokenId(); + } + } + + /// @dev The tokenId of the next NFT that will be minted / lazy minted. + function nextTokenIdToMint() external view returns (uint256) { + return _nextTokenId(); + } + + /// @dev The next token ID of the NFT that can be claimed. + function nextTokenIdToClaim() external view returns (uint256) { + return _nextTokenId(); + } + + /// @dev Burns `tokenId`. See {ERC721-_burn}. + function burn(uint256 tokenId) external virtual { + // note: ERC721AUpgradeable's `_burn(uint256,bool)` internally checks for token approvals. + _burn(tokenId, true); + } + + /// @dev See {ERC721-_beforeTokenTransfer}. + function _beforeTokenTransfers( + address from, + address to, + uint256 startTokenId_, + uint256 quantity + ) internal virtual override { + super._beforeTokenTransfers(from, to, startTokenId_, quantity); + + // if transfer is restricted on the contract, we still want to allow burning and minting + if (!hasRole(transferRole, address(0)) && from != address(0) && to != address(0)) { + if (!hasRole(transferRole, from) && !hasRole(transferRole, to)) { + revert("!T"); + } + } + } + + function _dropMsgSender() internal view virtual override returns (address) { + return _msgSender(); + } + + function _msgSenderERC721A() internal view virtual override returns (address) { + return _msgSender(); + } + + function _msgSender() + internal + view + virtual + override(ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } +} diff --git a/contracts/prebuilts/open-edition/OpenEditionERC721FlatFee.sol b/contracts/prebuilts/open-edition/OpenEditionERC721FlatFee.sol new file mode 100644 index 000000000..77c61a3f7 --- /dev/null +++ b/contracts/prebuilts/open-edition/OpenEditionERC721FlatFee.sol @@ -0,0 +1,301 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// ========== External imports ========== + +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; + +import "../../eip/queryable/ERC721AQueryableUpgradeable.sol"; + +// ========== Internal imports ========== + +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; +import "../../lib/CurrencyTransferLib.sol"; + +// ========== Features ========== + +import "../../extension/Multicall.sol"; +import "../../extension/ContractMetadata.sol"; +import "../../extension/Royalty.sol"; +import "../../extension/PrimarySale.sol"; +import "../../extension/Ownable.sol"; +import "../../extension/SharedMetadata.sol"; +import "../../extension/PermissionsEnumerable.sol"; +import "../../extension/Drop.sol"; +import "../../extension/PlatformFee.sol"; + +contract OpenEditionERC721FlatFee is + Initializable, + ContractMetadata, + PlatformFee, + Royalty, + PrimarySale, + Ownable, + SharedMetadata, + PermissionsEnumerable, + Drop, + ERC2771ContextUpgradeable, + Multicall, + ERC721AQueryableUpgradeable +{ + using StringsUpgradeable for uint256; + + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. + bytes32 private transferRole; + /// @dev Only MINTER_ROLE holders can update the shared metadata of tokens. + bytes32 private minterRole; + + /// @dev Max bps in the thirdweb system. + uint256 private constant MAX_BPS = 10_000; + + address public constant DEFAULT_FEE_RECIPIENT = 0x1Af20C6B23373350aD464700B5965CE4B0D2aD94; + uint16 private constant DEFAULT_FEE_BPS = 100; + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + constructor() initializer {} + + /// @dev Initializes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _royaltyRecipient, + uint128 _royaltyBps, + uint128 _platformFeeBps, + address _platformFeeRecipient + ) external initializerERC721A initializer { + bytes32 _transferRole = keccak256("TRANSFER_ROLE"); + bytes32 _minterRole = keccak256("MINTER_ROLE"); + + // Initialize inherited contracts, most base-like -> most derived. + __ERC2771Context_init(_trustedForwarders); + __ERC721A_init(_name, _symbol); + + _setupContractURI(_contractURI); + _setupOwner(_defaultAdmin); + + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + _setupRole(_minterRole, _defaultAdmin); + _setupRole(_transferRole, _defaultAdmin); + _setupRole(_transferRole, address(0)); + + _setupPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + _setupPrimarySaleRecipient(_saleRecipient); + + transferRole = _transferRole; + minterRole = _minterRole; + } + + /*/////////////////////////////////////////////////////////////// + ERC 165 / 721 / 2981 logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the URI for a given tokenId. + function tokenURI( + uint256 _tokenId + ) public view virtual override(ERC721AUpgradeable, IERC721AUpgradeable) returns (string memory) { + if (!_exists(_tokenId)) { + revert("!ID"); + } + + return _getURIFromSharedMetadata(_tokenId); + } + + /// @dev See ERC 165 + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC721AUpgradeable, IERC165, IERC721AUpgradeable) returns (bool) { + return super.supportsInterface(interfaceId) || type(IERC2981Upgradeable).interfaceId == interfaceId; + } + + /// @dev The start token ID for the contract. + function _startTokenId() internal pure override returns (uint256) { + return 1; + } + + function startTokenId() public pure returns (uint256) { + return _startTokenId(); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Collects and distributes the primary sale value of NFTs being claimed. + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override { + if (_pricePerToken == 0) { + require(msg.value == 0, "!Value"); + return; + } + + uint256 totalPrice = _quantityToClaim * _pricePerToken; + uint256 platformFees; + address platformFeeRecipient; + + uint256 platformFeesTw = (totalPrice * DEFAULT_FEE_BPS) / MAX_BPS; + + if (getPlatformFeeType() == IPlatformFee.PlatformFeeType.Flat) { + (platformFeeRecipient, platformFees) = getFlatPlatformFeeInfo(); + } else { + (address recipient, uint16 platformFeeBps) = getPlatformFeeInfo(); + platformFeeRecipient = recipient; + platformFees = ((totalPrice * platformFeeBps) / MAX_BPS); + } + require(totalPrice >= platformFees + platformFeesTw, "price less than platform fee"); + + bool validMsgValue; + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { + validMsgValue = msg.value == totalPrice; + } else { + validMsgValue = msg.value == 0; + } + require(validMsgValue, "!V"); + + address saleRecipient = _primarySaleRecipient == address(0) ? primarySaleRecipient() : _primarySaleRecipient; + + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), DEFAULT_FEE_RECIPIENT, platformFeesTw); + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), platformFeeRecipient, platformFees); + CurrencyTransferLib.transferCurrency( + _currency, + _msgSender(), + saleRecipient, + totalPrice - platformFees - platformFeesTw + ); + } + + /// @dev Transfers the NFTs being claimed. + function _transferTokensOnClaim( + address _to, + uint256 _quantityBeingClaimed + ) internal override returns (uint256 startTokenId_) { + startTokenId_ = _nextTokenId(); + _safeMint(_to, _quantityBeingClaimed); + } + + /// @dev Checks whether primary sale recipient can be set in the given execution context. + function _canSetPrimarySaleRecipient() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether owner can be set in the given execution context. + function _canSetOwner() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether royalty info can be set in the given execution context. + function _canSetRoyaltyInfo() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function _canSetClaimConditions() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Returns whether the shared metadata of tokens can be set in the given execution context. + function _canSetSharedMetadata() internal view virtual override returns (bool) { + return hasRole(minterRole, _msgSender()); + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function _canSetPlatformFeeInfo() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + /** + * Returns the total amount of tokens minted in the contract. + */ + function totalMinted() external view returns (uint256) { + unchecked { + return _nextTokenId() - _startTokenId(); + } + } + + /// @dev The tokenId of the next NFT that will be minted / lazy minted. + function nextTokenIdToMint() external view returns (uint256) { + return _nextTokenId(); + } + + /// @dev The next token ID of the NFT that can be claimed. + function nextTokenIdToClaim() external view returns (uint256) { + return _nextTokenId(); + } + + /// @dev Burns `tokenId`. See {ERC721-_burn}. + function burn(uint256 tokenId) external virtual { + // note: ERC721AUpgradeable's `_burn(uint256,bool)` internally checks for token approvals. + _burn(tokenId, true); + } + + /// @dev See {ERC721-_beforeTokenTransfer}. + function _beforeTokenTransfers( + address from, + address to, + uint256 startTokenId_, + uint256 quantity + ) internal virtual override { + super._beforeTokenTransfers(from, to, startTokenId_, quantity); + + // if transfer is restricted on the contract, we still want to allow burning and minting + if (!hasRole(transferRole, address(0)) && from != address(0) && to != address(0)) { + if (!hasRole(transferRole, from) && !hasRole(transferRole, to)) { + revert("!T"); + } + } + } + + function _dropMsgSender() internal view virtual override returns (address) { + return _msgSender(); + } + + function _msgSenderERC721A() internal view virtual override returns (address) { + return _msgSender(); + } + + function _msgSender() + internal + view + virtual + override(ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } +} diff --git a/contracts/prebuilts/pack/Pack.sol b/contracts/prebuilts/pack/Pack.sol new file mode 100644 index 000000000..196448def --- /dev/null +++ b/contracts/prebuilts/pack/Pack.sol @@ -0,0 +1,463 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// ========== External imports ========== + +import "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/ERC1155PausableUpgradeable.sol"; + +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; +import "@openzeppelin/contracts/interfaces/IERC721Receiver.sol"; +import { IERC1155Receiver } from "@openzeppelin/contracts/interfaces/IERC1155Receiver.sol"; + +// ========== Internal imports ========== + +import "../interface/IPack.sol"; +import "../../extension/Multicall.sol"; +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; + +// ========== Features ========== + +import "../../extension/ContractMetadata.sol"; +import "../../extension/Royalty.sol"; +import "../../extension/Ownable.sol"; +import "../../extension/PermissionsEnumerable.sol"; +import { TokenStore, ERC1155Receiver } from "../../extension/TokenStore.sol"; + +contract Pack is + Initializable, + ContractMetadata, + Ownable, + Royalty, + PermissionsEnumerable, + TokenStore, + ReentrancyGuardUpgradeable, + ERC2771ContextUpgradeable, + Multicall, + ERC1155Upgradeable, + IPack +{ + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + bytes32 private constant MODULE_TYPE = bytes32("Pack"); + uint256 private constant VERSION = 2; + + /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. + bytes32 private constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE"); + /// @dev Only MINTER_ROLE holders can create packs. + bytes32 private constant MINTER_ROLE = keccak256("MINTER_ROLE"); + /// @dev Only assets with ASSET_ROLE can be packed, when packing is restricted to particular assets. + bytes32 private constant ASSET_ROLE = keccak256("ASSET_ROLE"); + + // Token name + string public name; + + // Token symbol + string public symbol; + + /// @dev The token Id of the next set of packs to be minted. + uint256 public nextTokenIdToMint; + + /*/////////////////////////////////////////////////////////////// + Mappings + //////////////////////////////////////////////////////////////*/ + + /// @dev Mapping from token ID => total circulating supply of token with that ID. + mapping(uint256 => uint256) public totalSupply; + + /// @dev Mapping from pack ID => The state of that set of packs. + mapping(uint256 => PackInfo) private packInfo; + + /// @dev Checks if pack-creator allowed to add more tokens to a packId; set to false after first transfer + mapping(uint256 => bool) public canUpdatePack; + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + constructor(address _nativeTokenWrapper) TokenStore(_nativeTokenWrapper) initializer {} + + /// @dev Initializes the contract, like a constructor. + /* solhint-disable no-unused-vars */ + function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _royaltyRecipient, + uint256 _royaltyBps + ) external initializer { + __ERC1155_init(_contractURI); + + name = _name; + symbol = _symbol; + + _setupContractURI(_contractURI); + _setupOwner(_defaultAdmin); + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + _setupRole(MINTER_ROLE, _defaultAdmin); + + // note: see `onlyRoleWithSwitch` for ASSET_ROLE behaviour. + _setupRole(ASSET_ROLE, address(0)); + _setupRole(TRANSFER_ROLE, address(0)); + + _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + } + + /* solhint-enable no-unused-vars */ + + receive() external payable { + require(msg.sender == nativeTokenWrapper, "!nativeTokenWrapper."); + } + + /*/////////////////////////////////////////////////////////////// + Modifiers + //////////////////////////////////////////////////////////////*/ + + modifier onlyRoleWithSwitch(bytes32 role) { + _checkRoleWithSwitch(role, _msgSender()); + _; + } + + /*/////////////////////////////////////////////////////////////// + Generic contract logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the type of the contract. + function contractType() external pure returns (bytes32) { + return MODULE_TYPE; + } + + /// @dev Returns the version of the contract. + function contractVersion() external pure returns (uint8) { + return uint8(VERSION); + } + + /*/////////////////////////////////////////////////////////////// + ERC 165 / 1155 / 2981 logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the URI for a given tokenId. + function uri(uint256 _tokenId) public view override returns (string memory) { + return getUriOfBundle(_tokenId); + } + + /// @dev See ERC 165 + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC1155Receiver, ERC1155Upgradeable, IERC165) returns (bool) { + return + super.supportsInterface(interfaceId) || + type(IERC2981Upgradeable).interfaceId == interfaceId || + type(IERC721Receiver).interfaceId == interfaceId || + type(IERC1155Receiver).interfaceId == interfaceId; + } + + /*/////////////////////////////////////////////////////////////// + Pack logic: create | open packs. + //////////////////////////////////////////////////////////////*/ + + /// @dev Creates a pack with the stated contents. + function createPack( + Token[] calldata _contents, + uint256[] calldata _numOfRewardUnits, + string memory _packUri, + uint128 _openStartTimestamp, + uint128 _amountDistributedPerOpen, + address _recipient + ) external payable onlyRoleWithSwitch(MINTER_ROLE) nonReentrant returns (uint256 packId, uint256 packTotalSupply) { + require(_contents.length > 0 && _contents.length == _numOfRewardUnits.length, "!Len"); + + if (!hasRole(ASSET_ROLE, address(0))) { + for (uint256 i = 0; i < _contents.length; i += 1) { + _checkRole(ASSET_ROLE, _contents[i].assetContract); + } + } + + packId = nextTokenIdToMint; + nextTokenIdToMint += 1; + + packTotalSupply = escrowPackContents( + _contents, + _numOfRewardUnits, + _packUri, + packId, + _amountDistributedPerOpen, + false + ); + + packInfo[packId].openStartTimestamp = _openStartTimestamp; + packInfo[packId].amountDistributedPerOpen = _amountDistributedPerOpen; + + canUpdatePack[packId] = true; + + _mint(_recipient, packId, packTotalSupply, ""); + + emit PackCreated(packId, _recipient, packTotalSupply); + } + + /// @dev Add contents to an existing packId. + function addPackContents( + uint256 _packId, + Token[] calldata _contents, + uint256[] calldata _numOfRewardUnits, + address _recipient + ) + external + payable + onlyRoleWithSwitch(MINTER_ROLE) + nonReentrant + returns (uint256 packTotalSupply, uint256 newSupplyAdded) + { + require(canUpdatePack[_packId], "!Allowed"); + require(_contents.length > 0 && _contents.length == _numOfRewardUnits.length, "!Len"); + require(balanceOf(_recipient, _packId) != 0, "!Bal"); + + if (!hasRole(ASSET_ROLE, address(0))) { + for (uint256 i = 0; i < _contents.length; i += 1) { + _checkRole(ASSET_ROLE, _contents[i].assetContract); + } + } + + uint256 amountPerOpen = packInfo[_packId].amountDistributedPerOpen; + + newSupplyAdded = escrowPackContents(_contents, _numOfRewardUnits, "", _packId, amountPerOpen, true); + packTotalSupply = totalSupply[_packId] + newSupplyAdded; + + _mint(_recipient, _packId, newSupplyAdded, ""); + + emit PackUpdated(_packId, _recipient, newSupplyAdded); + } + + /// @notice Lets a pack owner open packs and receive the packs' reward units. + function openPack(uint256 _packId, uint256 _amountToOpen) external nonReentrant returns (Token[] memory) { + address opener = _msgSender(); + + require(opener == tx.origin, "!EOA"); + require(balanceOf(opener, _packId) >= _amountToOpen, "!Bal"); + + PackInfo memory pack = packInfo[_packId]; + require(pack.openStartTimestamp <= block.timestamp, "cant open"); + + Token[] memory rewardUnits = getRewardUnits(_packId, _amountToOpen, pack.amountDistributedPerOpen, pack); + + _burn(opener, _packId, _amountToOpen); + + _transferTokenBatch(address(this), opener, rewardUnits); + + emit PackOpened(_packId, opener, _amountToOpen, rewardUnits); + + return rewardUnits; + } + + /// @dev Stores assets within the contract. + function escrowPackContents( + Token[] calldata _contents, + uint256[] calldata _numOfRewardUnits, + string memory _packUri, + uint256 packId, + uint256 amountPerOpen, + bool isUpdate + ) internal returns (uint256 supplyToMint) { + uint256 sumOfRewardUnits; + + for (uint256 i = 0; i < _contents.length; i += 1) { + require(_contents[i].totalAmount != 0, "0 amt"); + require(_contents[i].totalAmount % _numOfRewardUnits[i] == 0, "!R"); + require(_contents[i].tokenType != TokenType.ERC721 || _contents[i].totalAmount == 1, "!R"); + + sumOfRewardUnits += _numOfRewardUnits[i]; + + packInfo[packId].perUnitAmounts.push(_contents[i].totalAmount / _numOfRewardUnits[i]); + } + + require(sumOfRewardUnits % amountPerOpen == 0, "!Amt"); + supplyToMint = sumOfRewardUnits / amountPerOpen; + + if (isUpdate) { + for (uint256 i = 0; i < _contents.length; i += 1) { + _addTokenInBundle(_contents[i], packId); + } + _transferTokenBatch(_msgSender(), address(this), _contents); + } else { + _storeTokens(_msgSender(), _contents, _packUri, packId); + } + } + + /// @dev Returns the reward units to distribute. + function getRewardUnits( + uint256 _packId, + uint256 _numOfPacksToOpen, + uint256 _rewardUnitsPerOpen, + PackInfo memory pack + ) internal returns (Token[] memory rewardUnits) { + uint256 numOfRewardUnitsToDistribute = _numOfPacksToOpen * _rewardUnitsPerOpen; + rewardUnits = new Token[](numOfRewardUnitsToDistribute); + uint256 totalRewardUnits = totalSupply[_packId] * _rewardUnitsPerOpen; + uint256 totalRewardKinds = getTokenCountOfBundle(_packId); + + uint256 random = generateRandomValue(); + + (Token[] memory _token, ) = getPackContents(_packId); + bool[] memory _isUpdated = new bool[](totalRewardKinds); + for (uint256 i; i < numOfRewardUnitsToDistribute; ) { + uint256 randomVal = uint256(keccak256(abi.encode(random, i))); + uint256 target = randomVal % totalRewardUnits; + uint256 step; + for (uint256 j; j < totalRewardKinds; ) { + uint256 perUnitAmount = pack.perUnitAmounts[j]; + uint256 totalRewardUnitsOfKind = _token[j].totalAmount / perUnitAmount; + if (target < step + totalRewardUnitsOfKind) { + _token[j].totalAmount -= perUnitAmount; + _isUpdated[j] = true; + rewardUnits[i].assetContract = _token[j].assetContract; + rewardUnits[i].tokenType = _token[j].tokenType; + rewardUnits[i].tokenId = _token[j].tokenId; + rewardUnits[i].totalAmount = perUnitAmount; + totalRewardUnits -= 1; + break; + } else { + step += totalRewardUnitsOfKind; + } + unchecked { + ++j; + } + } + unchecked { + ++i; + } + } + for (uint256 i; i < totalRewardKinds; ) { + if (_isUpdated[i]) { + _updateTokenInBundle(_token[i], _packId, i); + } + unchecked { + ++i; + } + } + } + + /*/////////////////////////////////////////////////////////////// + Getter functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the underlying contents of a pack. + function getPackContents( + uint256 _packId + ) public view returns (Token[] memory contents, uint256[] memory perUnitAmounts) { + PackInfo memory pack = packInfo[_packId]; + uint256 total = getTokenCountOfBundle(_packId); + contents = new Token[](total); + perUnitAmounts = new uint256[](total); + + for (uint256 i; i < total; ) { + contents[i] = getTokenOfBundle(_packId, i); + unchecked { + ++i; + } + } + perUnitAmounts = pack.perUnitAmounts; + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns whether owner can be set in the given execution context. + function _canSetOwner() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Returns whether royalty info can be set in the given execution context. + function _canSetRoyaltyInfo() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Returns whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function generateRandomValue() internal view returns (uint256 random) { + random = uint256(keccak256(abi.encodePacked(_msgSender(), blockhash(block.number - 1), block.difficulty))); + } + + /** + * @dev See {ERC1155-_beforeTokenTransfer}. + */ + function _beforeTokenTransfer( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal virtual override { + super._beforeTokenTransfer(operator, from, to, ids, amounts, data); + + // if transfer is restricted on the contract, we still want to allow burning and minting + if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { + require(hasRole(TRANSFER_ROLE, from) || hasRole(TRANSFER_ROLE, to), "!TRANSFER_ROLE"); + } + + if (from == address(0)) { + for (uint256 i = 0; i < ids.length; ++i) { + totalSupply[ids[i]] += amounts[i]; + } + } else { + for (uint256 i = 0; i < ids.length; ++i) { + // pack can no longer be updated after first transfer to non-zero address + if (canUpdatePack[ids[i]] && amounts[i] != 0) { + canUpdatePack[ids[i]] = false; + } + } + } + + if (to == address(0)) { + for (uint256 i = 0; i < ids.length; ++i) { + totalSupply[ids[i]] -= amounts[i]; + } + } + } + + /// @dev See EIP-2771 + function _msgSender() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } + + /// @dev See EIP-2771 + function _msgData() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable) + returns (bytes calldata) + { + return ERC2771ContextUpgradeable._msgData(); + } +} diff --git a/contracts/prebuilts/pack/PackVRFDirect.sol b/contracts/prebuilts/pack/PackVRFDirect.sol new file mode 100644 index 000000000..541ceb904 --- /dev/null +++ b/contracts/prebuilts/pack/PackVRFDirect.sol @@ -0,0 +1,516 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// ========== External imports ========== + +import "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/ERC1155PausableUpgradeable.sol"; + +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; +import "@openzeppelin/contracts/interfaces/IERC721Receiver.sol"; +import { IERC1155Receiver } from "@openzeppelin/contracts/interfaces/IERC1155Receiver.sol"; + +import "../../external-deps/chainlink/VRFV2WrapperConsumerBase.sol"; + +// ========== Internal imports ========== + +import "../interface/IPackVRFDirect.sol"; +import "../../extension/Multicall.sol"; +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; + +// ========== Features ========== + +import "../../extension/ContractMetadata.sol"; +import "../../extension/Royalty.sol"; +import "../../extension/Ownable.sol"; +import "../../extension/PermissionsEnumerable.sol"; +import { TokenStore, ERC1155Receiver } from "../../extension/TokenStore.sol"; + +/** + NOTE: This contract is a work in progress. + */ + +contract PackVRFDirect is + Initializable, + VRFV2WrapperConsumerBase, + ContractMetadata, + Ownable, + Royalty, + Permissions, + TokenStore, + ReentrancyGuardUpgradeable, + ERC2771ContextUpgradeable, + Multicall, + ERC1155Upgradeable, + IPackVRFDirect +{ + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + bytes32 private constant MODULE_TYPE = bytes32("PackVRFDirect"); + uint256 private constant VERSION = 2; + + address private immutable forwarder; + + // Token name + string public name; + + // Token symbol + string public symbol; + + /// @dev Only MINTER_ROLE holders can create packs. + bytes32 private minterRole; + + /// @dev The token Id of the next set of packs to be minted. + uint256 public nextTokenIdToMint; + + /*/////////////////////////////////////////////////////////////// + Mappings + //////////////////////////////////////////////////////////////*/ + + /// @dev Mapping from token ID => total circulating supply of token with that ID. + mapping(uint256 => uint256) public totalSupply; + + /// @dev Mapping from pack ID => The state of that set of packs. + mapping(uint256 => PackInfo) private packInfo; + + /*/////////////////////////////////////////////////////////////// + VRF state + //////////////////////////////////////////////////////////////*/ + + uint32 private constant CALLBACKGASLIMIT = 100_000; + uint16 private constant REQUEST_CONFIRMATIONS = 3; + uint32 private constant NUMWORDS = 1; + + struct RequestInfo { + uint256 packId; + address opener; + uint256 amountToOpen; + uint256[] randomWords; + bool openOnFulfillRandomness; + } + + mapping(uint256 => RequestInfo) private requestInfo; + mapping(address => uint256) private openerToReqId; + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + constructor( + address _nativeTokenWrapper, + address _trustedForwarder, + address _linkTokenAddress, + address _vrfV2Wrapper + ) VRFV2WrapperConsumerBase(_linkTokenAddress, _vrfV2Wrapper) TokenStore(_nativeTokenWrapper) initializer { + forwarder = _trustedForwarder; + } + + /// @dev Initializes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _royaltyRecipient, + uint256 _royaltyBps + ) external initializer { + bytes32 _minterRole = keccak256("MINTER_ROLE"); + + /** note: The immutable state-variable `forwarder` is an EOA-only forwarder, + * which guards against automated attacks. + * + * Use other forwarders only if there's a strong reason to bypass this check. + */ + address[] memory forwarders = new address[](_trustedForwarders.length + 1); + uint256 i; + for (; i < _trustedForwarders.length; i++) { + forwarders[i] = _trustedForwarders[i]; + } + forwarders[i] = forwarder; + __ERC2771Context_init(forwarders); + __ERC1155_init(_contractURI); + + name = _name; + symbol = _symbol; + + _setupContractURI(_contractURI); + _setupOwner(_defaultAdmin); + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + _setupRole(_minterRole, _defaultAdmin); + + _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + + minterRole = _minterRole; + } + + receive() external payable { + require(msg.sender == nativeTokenWrapper, "!nativeTokenWrapper."); + } + + /*/////////////////////////////////////////////////////////////// + Generic contract logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the type of the contract. + function contractType() external pure returns (bytes32) { + return MODULE_TYPE; + } + + /// @dev Returns the version of the contract. + function contractVersion() external pure returns (uint8) { + return uint8(VERSION); + } + + /*/////////////////////////////////////////////////////////////// + ERC 165 / 1155 / 2981 logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the URI for a given tokenId. + function uri(uint256 _tokenId) public view override returns (string memory) { + return getUriOfBundle(_tokenId); + } + + /// @dev See ERC 165 + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC1155Receiver, ERC1155Upgradeable, IERC165) returns (bool) { + return + super.supportsInterface(interfaceId) || + type(IERC2981Upgradeable).interfaceId == interfaceId || + type(IERC721Receiver).interfaceId == interfaceId || + type(IERC1155Receiver).interfaceId == interfaceId; + } + + /*/////////////////////////////////////////////////////////////// + Pack logic: create | open packs. + //////////////////////////////////////////////////////////////*/ + + /// @dev Creates a pack with the stated contents. + function createPack( + Token[] calldata _contents, + uint256[] calldata _numOfRewardUnits, + string memory _packUri, + uint128 _openStartTimestamp, + uint128 _amountDistributedPerOpen, + address _recipient + ) external payable onlyRole(minterRole) nonReentrant returns (uint256 packId, uint256 packTotalSupply) { + require(_contents.length > 0 && _contents.length == _numOfRewardUnits.length, "!Len"); + + packId = nextTokenIdToMint; + nextTokenIdToMint += 1; + + packTotalSupply = escrowPackContents( + _contents, + _numOfRewardUnits, + _packUri, + packId, + _amountDistributedPerOpen, + false + ); + + packInfo[packId].openStartTimestamp = _openStartTimestamp; + packInfo[packId].amountDistributedPerOpen = _amountDistributedPerOpen; + + // canUpdatePack[packId] = true; + + _mint(_recipient, packId, packTotalSupply, ""); + + emit PackCreated(packId, _recipient, packTotalSupply); + } + + /*/////////////////////////////////////////////////////////////// + VRF logic + //////////////////////////////////////////////////////////////*/ + + function openPackAndClaimRewards( + uint256 _packId, + uint256 _amountToOpen, + uint32 _callBackGasLimit + ) external returns (uint256) { + return _requestOpenPack(_packId, _amountToOpen, _callBackGasLimit, true); + } + + /// @notice Lets a pack owner open packs and receive the packs' reward units. + function openPack(uint256 _packId, uint256 _amountToOpen) external returns (uint256) { + return _requestOpenPack(_packId, _amountToOpen, CALLBACKGASLIMIT, false); + } + + function _requestOpenPack( + uint256 _packId, + uint256 _amountToOpen, + uint32 _callBackGasLimit, + bool _openOnFulfill + ) internal returns (uint256 requestId) { + address opener = _msgSender(); + + require(isTrustedForwarder(msg.sender) || opener == tx.origin, "!EOA"); + + require(openerToReqId[opener] == 0, "ReqInFlight"); + + require(_amountToOpen > 0 && balanceOf(opener, _packId) >= _amountToOpen, "!Bal"); + require(packInfo[_packId].openStartTimestamp <= block.timestamp, "!Open"); + + // Transfer packs into the contract. + _safeTransferFrom(opener, address(this), _packId, _amountToOpen, ""); + + // Request VRF for randomness. + requestId = requestRandomness(_callBackGasLimit, REQUEST_CONFIRMATIONS, NUMWORDS); + require(requestId > 0, "!VRF"); + + // Mark request as active; store request parameters. + requestInfo[requestId].packId = _packId; + requestInfo[requestId].opener = opener; + requestInfo[requestId].amountToOpen = _amountToOpen; + requestInfo[requestId].openOnFulfillRandomness = _openOnFulfill; + openerToReqId[opener] = requestId; + + emit PackOpenRequested(opener, _packId, _amountToOpen, requestId); + } + + /// @notice Called by Chainlink VRF to fulfill a random number request. + function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) internal override { + RequestInfo memory info = requestInfo[_requestId]; + + require(info.randomWords.length == 0, "!Req"); + requestInfo[_requestId].randomWords = _randomWords; + + emit PackRandomnessFulfilled(info.packId, _requestId); + + if (info.openOnFulfillRandomness) { + try PackVRFDirect(payable(address(this))).sendRewardsIndirect(info.opener) {} catch {} + } + } + + /// @notice Returns whether a pack opener is ready to call `claimRewards`. + function canClaimRewards(address _opener) public view returns (bool) { + uint256 requestId = openerToReqId[_opener]; + return requestId > 0 && requestInfo[requestId].randomWords.length > 0; + } + + /// @notice Lets a pack owner open packs and receive the packs' reward units. + function claimRewards() external returns (Token[] memory) { + return _claimRewards(_msgSender()); + } + + /// @notice Lets a pack owner open packs and receive the packs' reward units. + function sendRewardsIndirect(address _opener) external { + require(msg.sender == address(this)); + _claimRewards(_opener); + } + + function _claimRewards(address opener) internal returns (Token[] memory) { + require(isTrustedForwarder(msg.sender) || msg.sender == address(this) || opener == tx.origin, "!EOA"); + require(canClaimRewards(opener), "!ActiveReq"); + uint256 reqId = openerToReqId[opener]; + RequestInfo memory info = requestInfo[reqId]; + + delete openerToReqId[opener]; + delete requestInfo[reqId]; + + PackInfo memory pack = packInfo[info.packId]; + + Token[] memory rewardUnits = getRewardUnits( + info.randomWords[0], + info.packId, + info.amountToOpen, + pack.amountDistributedPerOpen, + pack + ); + + // Burn packs. + _burn(address(this), info.packId, info.amountToOpen); + + _transferTokenBatch(address(this), opener, rewardUnits); + + emit PackOpened(info.packId, opener, info.amountToOpen, rewardUnits); + + return rewardUnits; + } + + /// @dev Stores assets within the contract. + function escrowPackContents( + Token[] calldata _contents, + uint256[] calldata _numOfRewardUnits, + string memory _packUri, + uint256 packId, + uint256 amountPerOpen, + bool isUpdate + ) internal returns (uint256 supplyToMint) { + uint256 sumOfRewardUnits; + + for (uint256 i = 0; i < _contents.length; i += 1) { + require(_contents[i].totalAmount != 0, "0 amt"); + require(_contents[i].totalAmount % _numOfRewardUnits[i] == 0, "!R"); + require(_contents[i].tokenType != TokenType.ERC721 || _contents[i].totalAmount == 1, "!R"); + + sumOfRewardUnits += _numOfRewardUnits[i]; + + packInfo[packId].perUnitAmounts.push(_contents[i].totalAmount / _numOfRewardUnits[i]); + } + + require(sumOfRewardUnits % amountPerOpen == 0, "!Amt"); + supplyToMint = sumOfRewardUnits / amountPerOpen; + + if (isUpdate) { + for (uint256 i = 0; i < _contents.length; i += 1) { + _addTokenInBundle(_contents[i], packId); + } + _transferTokenBatch(_msgSender(), address(this), _contents); + } else { + _storeTokens(_msgSender(), _contents, _packUri, packId); + } + } + + /// @dev Returns the reward units to distribute. + function getRewardUnits( + uint256 _random, + uint256 _packId, + uint256 _numOfPacksToOpen, + uint256 _rewardUnitsPerOpen, + PackInfo memory pack + ) internal returns (Token[] memory rewardUnits) { + uint256 numOfRewardUnitsToDistribute = _numOfPacksToOpen * _rewardUnitsPerOpen; + rewardUnits = new Token[](numOfRewardUnitsToDistribute); + uint256 totalRewardUnits = totalSupply[_packId] * _rewardUnitsPerOpen; + uint256 totalRewardKinds = getTokenCountOfBundle(_packId); + + (Token[] memory _token, ) = getPackContents(_packId); + bool[] memory _isUpdated = new bool[](totalRewardKinds); + for (uint256 i = 0; i < numOfRewardUnitsToDistribute; i += 1) { + uint256 randomVal = uint256(keccak256(abi.encode(_random, i))); + uint256 target = randomVal % totalRewardUnits; + uint256 step; + + for (uint256 j = 0; j < totalRewardKinds; j += 1) { + uint256 totalRewardUnitsOfKind = _token[j].totalAmount / pack.perUnitAmounts[j]; + + if (target < step + totalRewardUnitsOfKind) { + _token[j].totalAmount -= pack.perUnitAmounts[j]; + _isUpdated[j] = true; + + rewardUnits[i].assetContract = _token[j].assetContract; + rewardUnits[i].tokenType = _token[j].tokenType; + rewardUnits[i].tokenId = _token[j].tokenId; + rewardUnits[i].totalAmount = pack.perUnitAmounts[j]; + + totalRewardUnits -= 1; + + break; + } else { + step += totalRewardUnitsOfKind; + } + } + } + + for (uint256 i = 0; i < totalRewardKinds; i += 1) { + if (_isUpdated[i]) { + _updateTokenInBundle(_token[i], _packId, i); + } + } + } + + /*/////////////////////////////////////////////////////////////// + Getter functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the underlying contents of a pack. + function getPackContents( + uint256 _packId + ) public view returns (Token[] memory contents, uint256[] memory perUnitAmounts) { + PackInfo memory pack = packInfo[_packId]; + uint256 total = getTokenCountOfBundle(_packId); + contents = new Token[](total); + perUnitAmounts = new uint256[](total); + + for (uint256 i = 0; i < total; i += 1) { + contents[i] = getTokenOfBundle(_packId, i); + } + perUnitAmounts = pack.perUnitAmounts; + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns whether owner can be set in the given execution context. + function _canSetOwner() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Returns whether royalty info can be set in the given execution context. + function _canSetRoyaltyInfo() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Returns whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + /** + * @dev See {ERC1155-_beforeTokenTransfer}. + */ + function _beforeTokenTransfer( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal virtual override { + super._beforeTokenTransfer(operator, from, to, ids, amounts, data); + + if (from == address(0)) { + for (uint256 i = 0; i < ids.length; ++i) { + totalSupply[ids[i]] += amounts[i]; + } + } + + if (to == address(0)) { + for (uint256 i = 0; i < ids.length; ++i) { + totalSupply[ids[i]] -= amounts[i]; + } + } + } + + /// @dev See EIP-2771 + function _msgSender() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } + + /// @dev See EIP-2771 + function _msgData() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable) + returns (bytes calldata) + { + return ERC2771ContextUpgradeable._msgData(); + } +} diff --git a/contracts/prebuilts/pack/pack.md b/contracts/prebuilts/pack/pack.md new file mode 100644 index 000000000..986805bf2 --- /dev/null +++ b/contracts/prebuilts/pack/pack.md @@ -0,0 +1,261 @@ +# Pack design document. + +This is a live document that explains what the [thirdweb](https://thirdweb.com/) `Pack` smart contract is, how it works and can be used, and why it is designed the way it is. + +The document is written for technical and non-technical readers. To ask further questions about thirdweb’s `Pack` contract, please join the [thirdweb discord](https://discord.gg/thirdweb) or create a github issue. + +# Background + +The thirdweb `Pack` contract is a lootbox mechanism. An account can bundle up arbitrary ERC20, ERC721 and ERC1155 tokens into a set of packs. A pack can then be opened in return for a selection of the tokens in the pack. The selection of tokens distributed on opening a pack depends on the relative supply of all tokens in the packs. + +> **IMPORTANT**: _Pack functions, such as opening of packs, can be costly in terms of gas usage due to random selection of rewards. Please check your gas estimates/usage, and do a trial on testnets before any mainnet deployment._ + +## Product: How packs _should_ work (without web3 terminology) + +Let's say we want to create a set of packs with three kinds of rewards - 80 **circles**, 15 **squares**, and 5 **stars** — and we want exactly 1 reward to be distributed when a pack is opened. + +In this case, with thirdweb’s `Pack` contract, each pack is guaranteed to yield exactly 1 reward. To deliver this guarantee, the number of packs created is equal to the sum of the supplies of each reward. So, we now have `80 + 15 + 5` i.e. `100` packs at hand. + +![pack-diag-1.png](/assets/pack-diag-1.png) + +On opening one of these 100 packs, the opener will receive one of the pack's rewards - either a **circle**, a **square**, or a **star**. The chances of receiving a particular reward is determined by how many of that reward exists across our set of packs. + +The percentage chance of receiving a particular kind of reward (e.g. a **star**) on opening a pack is calculated as:`(number_of_stars_packed) / (total number of packs)` + +In the beginning, 80 **circles**, 15 **squares**, and 5 **stars** exist across our set of 100 packs. That means the chances of receiving a **circle** upon opening a pack is `80/100` i.e. 80%. Similarly, a pack opener stands a 15% chance of receiving a **square**, and a 5% chance of receiving a **star** upon opening a pack. + +![pack-diag-2.png](/assets/pack-diag-2.png) + +The chances of receiving each kind of reward change as packs are opened. Let's say one of our 100 packs is opened, yielding a **circle**. We then have 99 packs remaining, with _79_ **circles**, 15 **squares**, and 5 **stars** packed. + +For the next pack that is opened, the opener will have a `79/99` i.e. around 79.8% chance of receiving a **circle**, around 15.2% chance of receiving a **square**, and around 5.1% chance of receiving a **star**. + +### Core parts of `Pack` as a product + +Given the above illustration of ‘how packs _should_ work’, we can now note down certain core parts of the `Pack` product, that any implementation of `Pack` should maintain: + +- A creator can pack arbitrary ERC20, ERC721 and ERC1155 tokens into a set of packs. +- The % chance of receiving a particular reward on opening a pack should be a function of the relative supplies of the rewards within a pack. That is, opening a pack _should not_ be like a lottery, where there’s an unchanging % chance of being distributed, assigned to rewards in a set of packs. +- A pack opener _should not_ be able to tell beforehand what reward they’ll receive on opening a pack. +- Each pack in a set of packs can be opened whenever the respective pack owner chooses to open the pack. +- Packs must be capable of being transferred and sold on a marketplace. + +## Why we’re building `Pack` + +Packs are designed to work as generic packs that contain rewards in them, where a pack can be opened to retrieve the rewards in that pack. + +Packs like these already exist as e.g. regular [Pokemon card packs](https://www.pokemoncenter.com/category/booster-packs), or in other forms that use blockchain technology, like [NBA Topshot](https://nbatopshot.com/) packs. This concept is ubiquitous across various cultures, sectors and products. + +As tokens continue to get legitimized as assets / items, we’re bringing ‘packs’ — a long-standing way of gamifying distribution of items — on-chain, as a primitive with a robust implementation that can be used across all chains, and for all kinds of use cases. + +# Technical details + +We’ll now go over the technical details of the `Pack` contract, with references to the example given in the previous section — ‘How packs work (without web3 terminology)’. + +## What can be packed in packs? + +You can create a set of packs with any combination of any number of ERC20, ERC721 and ERC1155 tokens. For example, you can create a set of packs with 10,000 [USDC](https://www.circle.com/en/usdc) (ERC20), 1 [Bored Ape Yacht Club](https://opensea.io/collection/boredapeyachtclub) NFT (ERC721), and 50 of [adidas originals’ first NFT](https://opensea.io/assets/0x28472a58a490c5e09a238847f66a68a47cc76f0f/0) (ERC1155). + +With strictly non-fungible tokens i.e. ERC721 NFTs, each NFT has a supply of 1. This means if a pack is opened and an ERC721 NFT is selected by the `Pack` contract to be distributed to the opener, that 1 NFT will be distributed to the opener. + +With fungible (ERC20) and semi-fungible (ERC1155) tokens, you must specify how many of those tokens must be distributed on opening a pack, as a unit. For example, if adding 10,000 USDC to a pack, you may specify that 20 USDC, as a unit, are meant to be distributed on opening a pack. This means you’re adding 500 units of 20 USDC to the set of packs you’re creating. + +And so, what can be packed in packs are _n_ number of configurations like ‘500 units of 20 USDC’. These configurations are interpreted by the `Pack` contract as `PackContent`: + +```solidity +enum TokenType { ERC20, ERC721, ERC1155 } + +struct Token { + address assetContract; + TokenType tokenType; + uint256 tokenId; + uint256 totalAmount; +} + +uint256 perUnitAmount; +``` + +| Value | Description | +| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| assetContract | The contract address of the token. | +| tokenType | The type of the token -- ERC20 / ERC721 / ERC1155 | +| tokenId | The tokenId of the token. (Not applicable for ERC20 tokens. The contract will ignore this value for ERC20 tokens.) | +| totalAmount | The total amount of this token packed in the pack. (Not applicable for ERC721 tokens. The contract will always consider this as 1 for ERC721 tokens.) | +| perUnitAmount | The amount of this token to distribute as a unit, on opening a pack. (Not applicable for ERC721 tokens. The contract will always consider this as 1 for ERC721 tokens.) | + +**Note:** A pack can contain different configurations for the same token. For example, the same set of packs can contain ‘500 units of 20 USDC’ and ‘10 units of 1000 USDC’ as two independent types of underlying rewards. + +## Creating packs + +You can create packs with any ERC20, ERC721 or ERC1155 tokens that you own. To create packs, you must specify the following: + +```solidity +/// @dev Creates a pack with the stated contents. +function createPack( + Token[] calldata contents, + uint256[] calldata numOfRewardUnits, + string calldata packUri, + uint128 openStartTimestamp, + uint128 amountDistributedPerOpen, + address recipient +) external +``` + +| Parameter | Description | +| ------------------------ | -------------------------------------------------------------------------------------------------------------- | +| contents | Tokens/assets packed in the set of pack. | +| numOfRewardUnits | Number of reward units for each asset, where each reward unit contains per unit amount of corresponding asset. | +| packUri | The (metadata) URI assigned to the packs created. | +| openStartTimestamp | The timestamp after which packs can be opened. | +| amountDistributedPerOpen | The number of reward units distributed per open. | +| recipient | The recipient of the packs created. | + +### Packs are ERC1155 tokens i.e. NFTs + +Packs themselves are ERC1155 tokens. And so, a set of packs created with your tokens is itself identified by a unique tokenId, has an associated metadata URI and a variable supply. + +In the example given in the previous section — ‘Non technical overview’, there is a set of 100 packs created, where that entire set of packs is identified by a unique tokenId. + +Since packs are ERC1155 tokens, you can publish multiple sets of packs using the same `Pack` contract. + +### Supply of packs + +When creating packs, you can specify the number of reward units to distribute to the opener on opening a pack. And so, when creating a set of packs, the total number of packs in that set is calculated as: + +`total_supply_of_packs = (total_reward_units) / (reward_units_to_distribute_per_open)` + +This guarantees that each pack can be opened to retrieve the intended _n_ reward units from inside the set of packs. + +## Updating packs + +You can add more contents to a created pack, up till the first transfer of packs. No addition can be made post that. + +```solidity +/// @dev Add contents to an existing packId. +function addPackContents( + uint256 packId, + Token[] calldata contents, + uint256[] calldata numOfRewardUnits, + address recipient +) external +``` + +| Parameter | Description | +| ---------------- | -------------------------------------------------------------------------------------------------------------- | +| PackId | The identifier of the pack to add contents to. | +| contents | Tokens/assets packed in the set of pack. | +| numOfRewardUnits | Number of reward units for each asset, where each reward unit contains per unit amount of corresponding asset. | +| recipient | The recipient of the new supply added. Should be the same address used during creation of packs. | + +## Opening packs + +Packs can be opened by owners of packs. A pack owner can open multiple packs at once. ‘Opening a pack’ essentially means burning the pack and receiving the intended _n_ number of reward units from inside the set of packs, in exchange. + +```solidity +function openPack(uint256 packId, uint256 amountToOpen) external; + +``` + +| Parameter | Description | +| ------------ | ------------------------------------ | +| packId | The identifier of the pack to open. | +| amountToOpen | The number of packs to open at once. | + +### How reward units are selected to distribute on opening packs + +We build on the example in the previous section — ‘Non-technical overview’. + +Each single **square**, **circle** or **star** is considered as a ‘reward unit’. For example, the 5 **stars** in the packs may be “5 units of 1000 USDC”, which is represented in the `Pack` contract by the following information + +```solidity +struct Token { + address assetContract; // USDC address + TokenType tokenType; // TokenType.ERC20 + uint256 tokenId; // Not applicable + uint256 totalAmount; // 5000 +} + +uint256 perUnitAmount; // 1000 +``` + +The percentage chance of receiving a particular kind of reward (e.g. a **star**) on opening a pack is calculated as:`(number_of_stars_packed) / (total number of packs)`. Here, `number_of_stars_packed` refers to the total number of reward units of the **star** kind inside the set of packs e.g. a total of 5 units of 1000 USDC. + +Going back to the example in the previous section — ‘Non-technical overview’. — the supply of the reward units in the relevant set of packs - 80 **circles**, 15 **squares**, and 5 **stars -** can be represented on a number line, from zero to the total supply of packs - in this case, 100. + +![pack-diag-2.png](/assets/pack-diag-2.png) + +Whenever a pack is opened, the `Pack` contract uses a new _random_ number in the range of the total supply of packs to determine what reward unit will be distributed to the pack opener. + +In our example case, the `Pack` contract uses a random number less than 100 to determine whether the pack opener will receive a **circle**, **square** or a **star**. + +So e.g. if the random number `num` is such that `0 <= num < 5`, the pack opener will receive a **star**. Similarly, if `5 <= num < 20`, the opener will receive a **square**, and if `20 <= num < 100`, the opener will receive a **circle**. + +Note that given this design, the opener truly has a 5% chance of receiving a **star**, a 15% chance of receiving a **square**, and an 80% chance of receiving a **circle**, as long as the random number used in the selection of the reward unit(s) to distribute is truly random. + +## The problem with random numbers + +From the previous section — ‘How reward units are selected to distribute on opening packs’: + +> Note that given this design, the opener truly has a 5% chance of receiving a **star**, a 15% chance of receiving a **square**, and an 80% chance of receiving a **circle**, as long as the random number used in the selection of the reward unit(s) to distribute is truly random. + +In the event of a pack opening, the random number used in the process affects what unit of reward is selected by the `Pack` contract to be distributed to the pack owner. + +If a pack owner can predict, at any moment, what random number will be used in this process of the contract selecting what unit of reward to distribute on opening a pack at that moment, the pack owner can selectively open their pack at a moment where they’ll receive the reward they want from the pack. + +This is a **possible** **critical vulnerability** since a core feature of the `Pack` product offering is the guarantee that each reward unit in a pack has a % probability of being distributed on opening a pack, and that this probability has some integrity (in the common sense way). Being able to predict the random numbers, as described above, overturns this guarantee. + +### Sourcing random numbers — solution + +The `Pack` contract requires a design where a pack owner _cannot possibly_ predict the random number that will be used in the process of their pack opening. + +To ensure the above, we make a simple check in the `openPack` function: + +```solidity +require(isTrustedForwarder(msg.sender) || _msgSender() == tx.origin, "opener cannot be smart contract"); +``` + +`tx.origin` returns the address of the external account that initiated the transaction, of which the `openPack` function call is a part of. + +The above check essentially means that only an external account i.e. an end user wallet, and no smart contract, can open packs. This lets us generate a pseudo random number using block variables, for the purpose of `openPack`: + +```solidity +uint256 random = uint256(keccak256(abi.encodePacked(_msgSender(), blockhash(block.number - 1), block.difficulty))); +``` + +Since only end user wallets can open packs, a pack owner _cannot possibly_ predict the random number that will be used in the process of their pack opening. That is because a pack opener cannot query the result of the random number calculation during a given block, and call `openPack` within that same block. + +We now list the single most important advantage, and consequent trade-off of using this solution: + +| Advantage | Trade-off | +| -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | +| A pack owner cannot possibly predict the random number that will be used in the process of their pack opening. | Only external accounts / EOAs can open packs. Smart contracts cannot open packs. | + +### Sourcing random numbers — discarded solutions + +We’ll now discuss some possible solutions for this design problem along with their trade-offs / why we do not use these solutions: + +- **Using an oracle (e.g. Chainlink VRF)** + + Using an oracle like Chainlink VRF enables the original design for the `Pack` contract: a pack owner can open _n_ number of packs, whenever they want, independent of when the other pack owners choose to open their own packs. All in all — opening _n_ packs becomes a closed isolated event performed by a single pack owner. + + ![pack-diag-3.png](/assets/pack-diag-3.png) + + **Why we’re not using this solution:** + + - Chainlink VRF v1 is only on Ethereum and Polygon, and Chainlink VRF v2 (current version) is only on Ethereum and Binance. As a result, this solution cannot be used by itself across all the chains thirdweb supports (and wants to support). + - Each random number request costs an end user Chainlink’s LINK token — it is costly, and seems like a random requirement for using a thirdweb offering. + +- **Delayed-reveal randomness: rewards for all packs in a set of packs visible all at once** + By ‘delayed-reveal’ randomness, we mean the following — + - When creating a set of packs, the creator provides (1) an encrypted seed i.e. integer (see the [encryption pattern used in thirdweb’s delayed-reveal NFTs](https://blog.thirdweb.com/delayed-reveal-nfts#step-1-encryption)), and (2) a future block number. + - The created packs are _non-transferrable_ by any address except the (1) pack creator, or (2) addresses manually approved by the pack creator. This is to let the creator distribute packs as they desire, _and_ is essential for the next step. + - After the specified future block number passes, the creator submits the unencrypted seed to the `Pack` contract. Whenever a pack owner now opens a pack, we calculate the random number to be used in the opening process as follows: + ```solidity + uint256 random = uint(keccak256(seed, msg.sender, blockhash(storedBlockNumber))); + ``` + - No one can predict the block hash of the stored future block unless the pack creator is the miner of the block with that block number (highly unlikely). + - The seed is controlled by the creator, submitted at the time of pack creation, and cannot be changed after submission. + - Since packs are non-transferrable in the way described above, as long as the pack opener is not approved to transfer packs, the opener cannot manipulate the value of `random` by transferring packs to a desirable address and then opening the pack from that address. + **Why we’re not using this solution:** + - Active involvement from the pack creator. They’re trusted to reveal the unencrypted seed once packs are eligible to be opened. + - Packs _must_ be non-transferrable in the way described above, which means they can’t be purchased on a marketplace, etc. Lack of a built-in distribution mechanism for the packs. diff --git a/contracts/prebuilts/signature-drop/SignatureDrop.sol b/contracts/prebuilts/signature-drop/SignatureDrop.sol new file mode 100644 index 000000000..b505940bb --- /dev/null +++ b/contracts/prebuilts/signature-drop/SignatureDrop.sol @@ -0,0 +1,371 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// ========== External imports ========== + +import "../../extension/Multicall.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; + +import "erc721a-upgradeable/contracts/ERC721AUpgradeable.sol"; + +// ========== Internal imports ========== + +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; +import "../../lib/CurrencyTransferLib.sol"; + +// ========== Features ========== + +import "../../extension/ContractMetadata.sol"; +import "../../legacy-contracts/extension/PlatformFee_V1.sol"; +import "../../extension/Royalty.sol"; +import "../../legacy-contracts/extension/PrimarySale_V1.sol"; +import "../../extension/Ownable.sol"; +import "../../extension/DelayedReveal.sol"; +import "../../extension/LazyMint.sol"; +import "../../extension/PermissionsEnumerable.sol"; +import "../../extension/DropSinglePhase.sol"; +import "../../extension/SignatureMintERC721Upgradeable.sol"; + +contract SignatureDrop is + Initializable, + ContractMetadata, + PlatformFee, + Royalty, + PrimarySale, + Ownable, + DelayedReveal, + LazyMint, + PermissionsEnumerable, + DropSinglePhase, + SignatureMintERC721Upgradeable, + ERC2771ContextUpgradeable, + Multicall, + ERC721AUpgradeable +{ + using StringsUpgradeable for uint256; + + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. + bytes32 private transferRole; + /// @dev Only MINTER_ROLE holders can sign off on `MintRequest`s and lazy mint tokens. + bytes32 private minterRole; + + /// @dev Max bps in the thirdweb system. + uint256 private constant MAX_BPS = 10_000; + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Initializes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _royaltyRecipient, + uint128 _royaltyBps, + uint128 _platformFeeBps, + address _platformFeeRecipient + ) external initializer { + bytes32 _transferRole = keccak256("TRANSFER_ROLE"); + bytes32 _minterRole = keccak256("MINTER_ROLE"); + + // Initialize inherited contracts, most base-like -> most derived. + __ERC2771Context_init(_trustedForwarders); + __ERC721A_init(_name, _symbol); + __SignatureMintERC721_init(); + + _setupContractURI(_contractURI); + _setupOwner(_defaultAdmin); + + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + _setupRole(_minterRole, _defaultAdmin); + _setupRole(_transferRole, _defaultAdmin); + _setupRole(_transferRole, address(0)); + + _setupPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + _setupPrimarySaleRecipient(_saleRecipient); + + transferRole = _transferRole; + minterRole = _minterRole; + } + + /*/////////////////////////////////////////////////////////////// + ERC 165 / 721 / 2981 logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the URI for a given tokenId. + function tokenURI(uint256 _tokenId) public view override returns (string memory) { + (uint256 batchId, ) = _getBatchId(_tokenId); + string memory batchUri = _getBaseURI(_tokenId); + + if (isEncryptedBatch(batchId)) { + return string(abi.encodePacked(batchUri, "0")); + } else { + return string(abi.encodePacked(batchUri, _tokenId.toString())); + } + } + + /// @dev See ERC 165 + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC721AUpgradeable, IERC165) returns (bool) { + return super.supportsInterface(interfaceId) || type(IERC2981Upgradeable).interfaceId == interfaceId; + } + + function contractType() external pure returns (bytes32) { + return bytes32("SignatureDrop"); + } + + function contractVersion() external pure returns (uint8) { + return uint8(5); + } + + /*/////////////////////////////////////////////////////////////// + Lazy minting + delayed-reveal logic + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Lets an account with `MINTER_ROLE` lazy mint 'n' NFTs. + * The URIs for each token is the provided `_baseURIForTokens` + `{tokenId}`. + */ + function lazyMint( + uint256 _amount, + string calldata _baseURIForTokens, + bytes calldata _data + ) public override returns (uint256 batchId) { + if (_data.length > 0) { + (bytes memory encryptedURI, bytes32 provenanceHash) = abi.decode(_data, (bytes, bytes32)); + if (encryptedURI.length != 0 && provenanceHash != "") { + _setEncryptedData(nextTokenIdToLazyMint + _amount, _data); + } + } + + return super.lazyMint(_amount, _baseURIForTokens, _data); + } + + /// @dev Lets an account with `MINTER_ROLE` reveal the URI for a batch of 'delayed-reveal' NFTs. + function reveal( + uint256 _index, + bytes calldata _key + ) external onlyRole(minterRole) returns (string memory revealedURI) { + uint256 batchId = getBatchIdAtIndex(_index); + revealedURI = getRevealURI(batchId, _key); + + _setEncryptedData(batchId, ""); + _setBaseURI(batchId, revealedURI); + + emit TokenURIRevealed(_index, revealedURI); + } + + /*/////////////////////////////////////////////////////////////// + Claiming lazy minted tokens logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Claim lazy minted tokens via signature. + function mintWithSignature( + MintRequest calldata _req, + bytes calldata _signature + ) external payable returns (address signer) { + uint256 tokenIdToMint = _currentIndex; + if (tokenIdToMint + _req.quantity > nextTokenIdToLazyMint) { + revert("!Tokens"); + } + + // Verify and process payload. + signer = _processRequest(_req, _signature); + + address receiver = _req.to; + + // Collect price + _collectPriceOnClaim(_req.primarySaleRecipient, _req.quantity, _req.currency, _req.pricePerToken); + + // Set royalties, if applicable. + if (_req.royaltyRecipient != address(0) && _req.royaltyBps != 0) { + _setupRoyaltyInfoForToken(tokenIdToMint, _req.royaltyRecipient, _req.royaltyBps); + } + + // Mint tokens. + _safeMint(receiver, _req.quantity); + + emit TokensMintedWithSignature(signer, receiver, tokenIdToMint, _req); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Runs before every `claim` function call. + function _beforeClaim( + address, + uint256 _quantity, + address, + uint256, + AllowlistProof calldata, + bytes memory + ) internal view override { + require(_currentIndex + _quantity <= nextTokenIdToLazyMint, "!Tokens"); + } + + /// @dev Collects and distributes the primary sale value of NFTs being claimed. + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override { + if (_pricePerToken == 0) { + require(msg.value == 0, "!Value"); + return; + } + + (address platformFeeRecipient, uint16 platformFeeBps) = getPlatformFeeInfo(); + + address saleRecipient = _primarySaleRecipient == address(0) ? primarySaleRecipient() : _primarySaleRecipient; + + uint256 totalPrice = _quantityToClaim * _pricePerToken; + uint256 platformFees = (totalPrice * platformFeeBps) / MAX_BPS; + + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { + require(msg.value == totalPrice, "!Price"); + } else { + require(msg.value == 0, "!Value"); + } + + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), platformFeeRecipient, platformFees); + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), saleRecipient, totalPrice - platformFees); + } + + /// @dev Transfers the NFTs being claimed. + function _transferTokensOnClaim( + address _to, + uint256 _quantityBeingClaimed + ) internal override returns (uint256 startTokenId) { + startTokenId = _currentIndex; + _safeMint(_to, _quantityBeingClaimed); + } + + /// @dev Returns whether a given address is authorized to sign mint requests. + function _isAuthorizedSigner(address _signer) internal view override returns (bool) { + return hasRole(minterRole, _signer); + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function _canSetPlatformFeeInfo() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether primary sale recipient can be set in the given execution context. + function _canSetPrimarySaleRecipient() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether owner can be set in the given execution context. + function _canSetOwner() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether royalty info can be set in the given execution context. + function _canSetRoyaltyInfo() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function _canSetClaimConditions() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Returns whether lazy minting can be done in the given execution context. + function _canLazyMint() internal view virtual override returns (bool) { + return hasRole(minterRole, _msgSender()); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + /** + * Returns the total amount of tokens minted in the contract. + */ + function totalMinted() external view returns (uint256) { + unchecked { + return _currentIndex - _startTokenId(); + } + } + + /// @dev The tokenId of the next NFT that will be minted / lazy minted. + function nextTokenIdToMint() external view returns (uint256) { + return nextTokenIdToLazyMint; + } + + /// @dev Burns `tokenId`. See {ERC721-_burn}. + function burn(uint256 tokenId) external virtual { + // note: ERC721AUpgradeable's `_burn(uint256,bool)` internally checks for token approvals. + _burn(tokenId, true); + } + + /// @dev See {ERC721-_beforeTokenTransfer}. + function _beforeTokenTransfers( + address from, + address to, + uint256 startTokenId, + uint256 quantity + ) internal virtual override { + super._beforeTokenTransfers(from, to, startTokenId, quantity); + + // if transfer is restricted on the contract, we still want to allow burning and minting + if (!hasRole(transferRole, address(0)) && from != address(0) && to != address(0)) { + if (!hasRole(transferRole, from) && !hasRole(transferRole, to)) { + revert("!Transfer-Role"); + } + } + } + + function _dropMsgSender() internal view virtual override returns (address) { + return _msgSender(); + } + + function _msgSender() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } + + function _msgData() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable) + returns (bytes calldata) + { + return ERC2771ContextUpgradeable._msgData(); + } +} diff --git a/contracts/signature-drop/signatureDrop.md b/contracts/prebuilts/signature-drop/signatureDrop.md similarity index 100% rename from contracts/signature-drop/signatureDrop.md rename to contracts/prebuilts/signature-drop/signatureDrop.md diff --git a/contracts/prebuilts/split/Split.sol b/contracts/prebuilts/split/Split.sol new file mode 100644 index 000000000..028f5a0ee --- /dev/null +++ b/contracts/prebuilts/split/Split.sol @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// Base +import "../../external-deps/openzeppelin/finance/PaymentSplitterUpgradeable.sol"; +import "../../infra/interface/IThirdwebContract.sol"; + +// Meta-tx +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; + +// Access +import "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; + +// Utils +import "../../extension/Multicall.sol"; +import "../../lib/FeeType.sol"; +import "../../extension/upgradeable/ReentrancyGuard.sol"; + +contract Split is + IThirdwebContract, + Initializable, + Multicall, + ERC2771ContextUpgradeable, + AccessControlEnumerableUpgradeable, + PaymentSplitterUpgradeable, + ReentrancyGuard +{ + bytes32 private constant MODULE_TYPE = bytes32("Split"); + uint128 private constant VERSION = 1; + + /// @dev Max bps in the thirdweb system + uint128 private constant MAX_BPS = 10_000; + + /// @dev Contract level metadata. + string public contractURI; + + constructor() initializer {} + + /// @dev Performs the job of the constructor. + /// @dev shares_ are scaled by 10,000 to prevent precision loss when including fees + function initialize( + address _defaultAdmin, + string memory _contractURI, + address[] memory _trustedForwarders, + address[] memory _payees, + uint256[] memory _shares + ) external initializer { + // Initialize inherited contracts: most base -> most derived + __ERC2771Context_init(_trustedForwarders); + __PaymentSplitter_init(_payees, _shares); + + contractURI = _contractURI; + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + } + + /// @dev Returns the module type of the contract. + function contractType() external pure returns (bytes32) { + return MODULE_TYPE; + } + + /// @dev Returns the version of the contract. + function contractVersion() external pure returns (uint8) { + return uint8(VERSION); + } + + /** + * @dev Triggers a transfer to `account` of the amount of Ether they are owed, according to their percentage of the + * total shares and their previous withdrawals. + */ + function release(address payable account) public virtual override nonReentrant { + uint256 payment = _release(account); + require(payment != 0, "PaymentSplitter: account is not due payment"); + } + + /** + * @dev Triggers a transfer to `account` of the amount of `token` tokens they are owed, according to their + * percentage of the total shares and their previous withdrawals. `token` must be the address of an IERC20 + * contract. + */ + function release(IERC20Upgradeable token, address account) public virtual override nonReentrant { + uint256 payment = _release(token, account); + require(payment != 0, "PaymentSplitter: account is not due payment"); + } + + /// @dev Returns the amount of Ether that `account` is owed, according to their percentage of the total shares and returns the payment + function _release(address payable account) internal returns (uint256) { + require(shares(account) > 0, "PaymentSplitter: account has no shares"); + + uint256 totalReceived = address(this).balance + totalReleased(); + uint256 payment = _pendingPayment(account, totalReceived, released(account)); + + if (payment == 0) { + return 0; + } + + _released[account] += payment; + _totalReleased += payment; + + AddressUpgradeable.sendValue(account, payment); + emit PaymentReleased(account, payment); + + return payment; + } + + /// @dev Returns the amount of `token` that `account` is owed, according to their percentage of the total shares and returns the payment + function _release(IERC20Upgradeable token, address account) internal returns (uint256) { + require(shares(account) > 0, "PaymentSplitter: account has no shares"); + + uint256 totalReceived = token.balanceOf(address(this)) + totalReleased(token); + uint256 payment = _pendingPayment(account, totalReceived, released(token, account)); + + if (payment == 0) { + return 0; + } + + _erc20Released[token][account] += payment; + _erc20TotalReleased[token] += payment; + + SafeERC20Upgradeable.safeTransfer(token, account, payment); + emit ERC20PaymentReleased(token, account, payment); + + return payment; + } + + /** + * @dev Release the owed amount of token to all of the payees. + */ + function distribute() public virtual nonReentrant { + uint256 count = payeeCount(); + for (uint256 i = 0; i < count; i++) { + _release(payable(payee(i))); + } + } + + /** + * @dev Release owed amount of the `token` to all of the payees. + */ + function distribute(IERC20Upgradeable token) public virtual nonReentrant { + uint256 count = payeeCount(); + for (uint256 i = 0; i < count; i++) { + _release(token, payee(i)); + } + } + + /// @dev See ERC2771 + function _msgSender() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } + + /// @dev See ERC2771 + function _msgData() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable) + returns (bytes calldata) + { + return ERC2771ContextUpgradeable._msgData(); + } + + /// @dev Sets contract URI for the contract-level metadata of the contract. + function setContractURI(string calldata _uri) external onlyRole(DEFAULT_ADMIN_ROLE) { + contractURI = _uri; + } +} diff --git a/contracts/prebuilts/staking/EditionStake.sol b/contracts/prebuilts/staking/EditionStake.sol new file mode 100644 index 000000000..9f5375bbc --- /dev/null +++ b/contracts/prebuilts/staking/EditionStake.sol @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// Token +import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155ReceiverUpgradeable.sol"; + +// Meta transactions +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; + +// Utils +import "../../extension/Multicall.sol"; +import "../../lib/CurrencyTransferLib.sol"; + +// ========== Features ========== + +import "../../extension/ContractMetadata.sol"; +import "../../extension/PermissionsEnumerable.sol"; +import { Staking1155Upgradeable } from "../../extension/Staking1155Upgradeable.sol"; +import "../interface/staking/IEditionStake.sol"; + +contract EditionStake is + Initializable, + ContractMetadata, + PermissionsEnumerable, + ERC2771ContextUpgradeable, + Multicall, + Staking1155Upgradeable, + ERC165Upgradeable, + IERC1155ReceiverUpgradeable, + IEditionStake +{ + bytes32 private constant MODULE_TYPE = bytes32("EditionStake"); + uint256 private constant VERSION = 1; + + /// @dev The address of the native token wrapper contract. + address internal immutable nativeTokenWrapper; + + /// @dev ERC20 Reward Token address. See {_mintRewards} below. + address public rewardToken; + + /// @dev Total amount of reward tokens in the contract. + uint256 private rewardTokenBalance; + + constructor(address _nativeTokenWrapper) initializer { + nativeTokenWrapper = _nativeTokenWrapper; + } + + /// @dev Initializes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _contractURI, + address[] memory _trustedForwarders, + address _rewardToken, + address _stakingToken, + uint80 _defaultTimeUnit, + uint256 _defaultRewardsPerUnitTime + ) external initializer { + __ERC2771Context_init_unchained(_trustedForwarders); + + rewardToken = _rewardToken; + __Staking1155_init(_stakingToken); + _setDefaultStakingCondition(_defaultTimeUnit, _defaultRewardsPerUnitTime); + + _setupContractURI(_contractURI); + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + } + + /// @dev Returns the module type of the contract. + function contractType() external pure virtual returns (bytes32) { + return MODULE_TYPE; + } + + /// @dev Returns the version of the contract. + function contractVersion() external pure virtual returns (uint8) { + return uint8(VERSION); + } + + /// @dev Lets the contract receive ether to unwrap native tokens. + receive() external payable { + require(msg.sender == nativeTokenWrapper, "caller not native token wrapper."); + } + + /// @dev Admin deposits reward tokens. + function depositRewardTokens(uint256 _amount) external payable nonReentrant { + require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Not authorized"); + + address _rewardToken = rewardToken == CurrencyTransferLib.NATIVE_TOKEN ? nativeTokenWrapper : rewardToken; + + uint256 balanceBefore = IERC20(_rewardToken).balanceOf(address(this)); + CurrencyTransferLib.transferCurrencyWithWrapper( + rewardToken, + _msgSender(), + address(this), + _amount, + nativeTokenWrapper + ); + uint256 actualAmount = IERC20(_rewardToken).balanceOf(address(this)) - balanceBefore; + + rewardTokenBalance += actualAmount; + + emit RewardTokensDepositedByAdmin(actualAmount); + } + + /// @dev Admin can withdraw excess reward tokens. + function withdrawRewardTokens(uint256 _amount) external nonReentrant { + require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Not authorized"); + + // to prevent locking of direct-transferred tokens + rewardTokenBalance = _amount > rewardTokenBalance ? 0 : rewardTokenBalance - _amount; + + CurrencyTransferLib.transferCurrencyWithWrapper( + rewardToken, + address(this), + _msgSender(), + _amount, + nativeTokenWrapper + ); + + emit RewardTokensWithdrawnByAdmin(_amount); + } + + /// @notice View total rewards available in the staking contract. + function getRewardTokenBalance() external view override returns (uint256 _rewardsAvailableInContract) { + return rewardTokenBalance; + } + + /*/////////////////////////////////////////////////////////////// + ERC 165 / 721 logic + //////////////////////////////////////////////////////////////*/ + + function onERC1155Received(address, address, uint256, uint256, bytes calldata) external view returns (bytes4) { + require(isStaking == 2, "Direct transfer"); + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external returns (bytes4) {} + + function supportsInterface( + bytes4 interfaceId + ) public view override(ERC165Upgradeable, IERC165Upgradeable) returns (bool) { + return interfaceId == type(IERC1155ReceiverUpgradeable).interfaceId || super.supportsInterface(interfaceId); + } + + /*/////////////////////////////////////////////////////////////// + Transfer Staking Rewards + //////////////////////////////////////////////////////////////*/ + + /// @dev Mint/Transfer ERC20 rewards to the staker. + function _mintRewards(address _staker, uint256 _rewards) internal override { + require(_rewards <= rewardTokenBalance, "Not enough reward tokens"); + rewardTokenBalance -= _rewards; + CurrencyTransferLib.transferCurrencyWithWrapper( + rewardToken, + address(this), + _staker, + _rewards, + nativeTokenWrapper + ); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns whether staking related restrictions can be set in the given execution context. + function _canSetStakeConditions() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function _stakeMsgSender() internal view virtual override returns (address) { + return _msgSender(); + } + + function _msgSender() + internal + view + virtual + override(ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } +} diff --git a/contracts/prebuilts/staking/NFTStake.sol b/contracts/prebuilts/staking/NFTStake.sol new file mode 100644 index 000000000..c50794ffc --- /dev/null +++ b/contracts/prebuilts/staking/NFTStake.sol @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// Token +import "../../eip/interface/IERC721.sol"; +import "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721ReceiverUpgradeable.sol"; + +// Meta transactions +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; + +// Utils +import "../../extension/Multicall.sol"; +import "../../lib/CurrencyTransferLib.sol"; + +// ========== Features ========== + +import "../../extension/ContractMetadata.sol"; +import "../../extension/PermissionsEnumerable.sol"; +import { Staking721Upgradeable } from "../../extension/Staking721Upgradeable.sol"; +import "../interface/staking/INFTStake.sol"; + +contract NFTStake is + Initializable, + ContractMetadata, + PermissionsEnumerable, + ERC2771ContextUpgradeable, + Multicall, + Staking721Upgradeable, + ERC165Upgradeable, + IERC721ReceiverUpgradeable, + INFTStake +{ + bytes32 private constant MODULE_TYPE = bytes32("NFTStake"); + uint256 private constant VERSION = 1; + + /// @dev The address of the native token wrapper contract. + address internal immutable nativeTokenWrapper; + + /// @dev ERC20 Reward Token address. See {_mintRewards} below. + address public rewardToken; + + /// @dev Total amount of reward tokens in the contract. + uint256 private rewardTokenBalance; + + constructor(address _nativeTokenWrapper) initializer { + nativeTokenWrapper = _nativeTokenWrapper; + } + + /// @dev Initializes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _contractURI, + address[] memory _trustedForwarders, + address _rewardToken, + address _stakingToken, + uint256 _timeUnit, + uint256 _rewardsPerUnitTime + ) external initializer { + __ERC2771Context_init_unchained(_trustedForwarders); + + rewardToken = _rewardToken; + __Staking721_init(_stakingToken); + _setStakingCondition(_timeUnit, _rewardsPerUnitTime); + + _setupContractURI(_contractURI); + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + } + + /// @dev Returns the module type of the contract. + function contractType() external pure virtual returns (bytes32) { + return MODULE_TYPE; + } + + /// @dev Returns the version of the contract. + function contractVersion() external pure virtual returns (uint8) { + return uint8(VERSION); + } + + /// @dev Lets the contract receive ether to unwrap native tokens. + receive() external payable { + require(msg.sender == nativeTokenWrapper, "caller not native token wrapper."); + } + + /// @dev Admin deposits reward tokens. + function depositRewardTokens(uint256 _amount) external payable nonReentrant { + require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Not authorized"); + + address _rewardToken = rewardToken == CurrencyTransferLib.NATIVE_TOKEN ? nativeTokenWrapper : rewardToken; + + uint256 balanceBefore = IERC20(_rewardToken).balanceOf(address(this)); + CurrencyTransferLib.transferCurrencyWithWrapper( + rewardToken, + _msgSender(), + address(this), + _amount, + nativeTokenWrapper + ); + uint256 actualAmount = IERC20(_rewardToken).balanceOf(address(this)) - balanceBefore; + + rewardTokenBalance += actualAmount; + + emit RewardTokensDepositedByAdmin(actualAmount); + } + + /// @dev Admin can withdraw excess reward tokens. + function withdrawRewardTokens(uint256 _amount) external nonReentrant { + require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Not authorized"); + + // to prevent locking of direct-transferred tokens + rewardTokenBalance = _amount > rewardTokenBalance ? 0 : rewardTokenBalance - _amount; + + CurrencyTransferLib.transferCurrencyWithWrapper( + rewardToken, + address(this), + _msgSender(), + _amount, + nativeTokenWrapper + ); + + emit RewardTokensWithdrawnByAdmin(_amount); + } + + /// @notice View total rewards available in the staking contract. + function getRewardTokenBalance() external view override returns (uint256) { + return rewardTokenBalance; + } + + /*/////////////////////////////////////////////////////////////// + ERC 165 / 721 logic + //////////////////////////////////////////////////////////////*/ + + function onERC721Received(address, address, uint256, bytes calldata) external view override returns (bytes4) { + require(isStaking == 2, "Direct transfer"); + return this.onERC721Received.selector; + } + + function supportsInterface(bytes4 interfaceId) public view override returns (bool) { + return interfaceId == type(IERC721ReceiverUpgradeable).interfaceId || super.supportsInterface(interfaceId); + } + + /*/////////////////////////////////////////////////////////////// + Transfer Staking Rewards + //////////////////////////////////////////////////////////////*/ + + /// @dev Mint/Transfer ERC20 rewards to the staker. + function _mintRewards(address _staker, uint256 _rewards) internal override { + require(_rewards <= rewardTokenBalance, "Not enough reward tokens"); + rewardTokenBalance -= _rewards; + CurrencyTransferLib.transferCurrencyWithWrapper( + rewardToken, + address(this), + _staker, + _rewards, + nativeTokenWrapper + ); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns whether staking related restrictions can be set in the given execution context. + function _canSetStakeConditions() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function _stakeMsgSender() internal view virtual override returns (address) { + return _msgSender(); + } + + function _msgSender() + internal + view + virtual + override(ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } +} diff --git a/contracts/prebuilts/staking/TokenStake.sol b/contracts/prebuilts/staking/TokenStake.sol new file mode 100644 index 000000000..d908db791 --- /dev/null +++ b/contracts/prebuilts/staking/TokenStake.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// Token +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +// Meta transactions +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; + +// Utils +import "../../extension/Multicall.sol"; +import { CurrencyTransferLib } from "../../lib/CurrencyTransferLib.sol"; +import "../../eip/interface/IERC20Metadata.sol"; + +// ========== Features ========== + +import "../../extension/ContractMetadata.sol"; +import "../../extension/PermissionsEnumerable.sol"; +import { Staking20Upgradeable } from "../../extension/Staking20Upgradeable.sol"; +import "../interface/staking/ITokenStake.sol"; + +contract TokenStake is + Initializable, + ContractMetadata, + PermissionsEnumerable, + ERC2771ContextUpgradeable, + Multicall, + Staking20Upgradeable, + ITokenStake +{ + bytes32 private constant MODULE_TYPE = bytes32("TokenStake"); + uint256 private constant VERSION = 1; + + /// @dev ERC20 Reward Token address. See {_mintRewards} below. + address public rewardToken; + + /// @dev Total amount of reward tokens in the contract. + uint256 private rewardTokenBalance; + + constructor(address _nativeTokenWrapper) initializer Staking20Upgradeable(_nativeTokenWrapper) {} + + /// @dev Initializes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _contractURI, + address[] memory _trustedForwarders, + address _rewardToken, + address _stakingToken, + uint80 _timeUnit, + uint256 _rewardRatioNumerator, + uint256 _rewardRatioDenominator + ) external initializer { + __ERC2771Context_init_unchained(_trustedForwarders); + + require(_rewardToken != _stakingToken, "Reward Token and Staking Token can't be same."); + rewardToken = _rewardToken; + + uint16 _stakingTokenDecimals = _stakingToken == CurrencyTransferLib.NATIVE_TOKEN + ? 18 + : IERC20Metadata(_stakingToken).decimals(); + uint16 _rewardTokenDecimals = _rewardToken == CurrencyTransferLib.NATIVE_TOKEN + ? 18 + : IERC20Metadata(_rewardToken).decimals(); + + __Staking20_init(_stakingToken, _stakingTokenDecimals, _rewardTokenDecimals); + _setStakingCondition(_timeUnit, _rewardRatioNumerator, _rewardRatioDenominator); + + _setupContractURI(_contractURI); + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + } + + /// @dev Returns the module type of the contract. + function contractType() external pure virtual returns (bytes32) { + return MODULE_TYPE; + } + + /// @dev Returns the version of the contract. + function contractVersion() external pure virtual returns (uint8) { + return uint8(VERSION); + } + + /// @dev Lets the contract receive ether to unwrap native tokens. + receive() external payable { + require(msg.sender == nativeTokenWrapper, "caller not native token wrapper."); + } + + /// @dev Admin deposits reward tokens. + function depositRewardTokens(uint256 _amount) external payable nonReentrant { + require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Not authorized"); + + address _rewardToken = rewardToken == CurrencyTransferLib.NATIVE_TOKEN ? nativeTokenWrapper : rewardToken; + + uint256 balanceBefore = IERC20(_rewardToken).balanceOf(address(this)); + CurrencyTransferLib.transferCurrencyWithWrapper( + rewardToken, + _msgSender(), + address(this), + _amount, + nativeTokenWrapper + ); + uint256 actualAmount = IERC20(_rewardToken).balanceOf(address(this)) - balanceBefore; + + rewardTokenBalance += actualAmount; + + emit RewardTokensDepositedByAdmin(actualAmount); + } + + /// @dev Admin can withdraw excess reward tokens. + function withdrawRewardTokens(uint256 _amount) external nonReentrant { + require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Not authorized"); + + // to prevent locking of direct-transferred tokens + rewardTokenBalance = _amount > rewardTokenBalance ? 0 : rewardTokenBalance - _amount; + + CurrencyTransferLib.transferCurrencyWithWrapper( + rewardToken, + address(this), + _msgSender(), + _amount, + nativeTokenWrapper + ); + + // The withdrawal shouldn't reduce staking token balance. `>=` accounts for any accidental transfers. + address _stakingToken = stakingToken == CurrencyTransferLib.NATIVE_TOKEN ? nativeTokenWrapper : stakingToken; + require( + IERC20(_stakingToken).balanceOf(address(this)) >= stakingTokenBalance, + "Staking token balance reduced." + ); + + emit RewardTokensWithdrawnByAdmin(_amount); + } + + /// @notice View total rewards available in the staking contract. + function getRewardTokenBalance() external view override returns (uint256) { + return rewardTokenBalance; + } + + /*/////////////////////////////////////////////////////////////// + Transfer Staking Rewards + //////////////////////////////////////////////////////////////*/ + + /// @dev Mint/Transfer ERC20 rewards to the staker. + function _mintRewards(address _staker, uint256 _rewards) internal override { + require(_rewards <= rewardTokenBalance, "Not enough reward tokens"); + rewardTokenBalance -= _rewards; + CurrencyTransferLib.transferCurrencyWithWrapper( + rewardToken, + address(this), + _staker, + _rewards, + nativeTokenWrapper + ); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns whether staking related restrictions can be set in the given execution context. + function _canSetStakeConditions() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function _stakeMsgSender() internal view virtual override returns (address) { + return _msgSender(); + } + + function _msgSender() + internal + view + virtual + override(ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } +} diff --git a/contracts/prebuilts/tiered-drop/TieredDrop.sol b/contracts/prebuilts/tiered-drop/TieredDrop.sol new file mode 100644 index 000000000..f52c82b17 --- /dev/null +++ b/contracts/prebuilts/tiered-drop/TieredDrop.sol @@ -0,0 +1,607 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// ========== External imports ========== + +import "../../extension/Multicall.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; + +import "erc721a-upgradeable/contracts/ERC721AUpgradeable.sol"; + +// ========== Internal imports ========== + +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; +import "../../lib/CurrencyTransferLib.sol"; + +// ========== Features ========== + +import "../../extension/ContractMetadata.sol"; +import "../../extension/PlatformFee.sol"; +import "../../extension/Royalty.sol"; +import "../../extension/PrimarySale.sol"; +import "../../extension/Ownable.sol"; +import "../../extension/DelayedReveal.sol"; +import "../../extension/PermissionsEnumerable.sol"; + +// ========== New Features ========== + +import "../../extension/LazyMintWithTier.sol"; +import "../../extension/SignatureActionUpgradeable.sol"; + +contract TieredDrop is + Initializable, + ContractMetadata, + Royalty, + PrimarySale, + Ownable, + DelayedReveal, + LazyMintWithTier, + PermissionsEnumerable, + SignatureActionUpgradeable, + ERC2771ContextUpgradeable, + Multicall, + ERC721AUpgradeable +{ + using StringsUpgradeable for uint256; + + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. + bytes32 private transferRole; + /// @dev Only MINTER_ROLE holders can sign off on `MintRequest`s and lazy mint tokens. + bytes32 private minterRole; + + /** + * @dev Conceptually, tokens are minted on this contract one-batch-of-a-tier at a time. Each batch is comprised of + * a given range of tokenIds [startId, endId). + * + * This array stores each such endId, in chronological order of minting. + */ + uint256 private lengthEndIdsAtMint; + mapping(uint256 => uint256) private endIdsAtMint; + + /** + * @dev Conceptually, tokens are minted on this contract one-batch-of-a-tier at a time. Each batch is comprised of + * a given range of tokenIds [startId, endId). + * + * This is a mapping from such an `endId` -> the tier that tokenIds [startId, endId) belong to. + * Together with `endIdsAtMint`, this mapping is used to return the tokenIds that belong to a given tier. + */ + mapping(uint256 => string) private tierAtEndId; + + /** + * @dev This contract lets an admin lazy mint batches of metadata at once, for a given tier. E.g. an admin may lazy mint + * the metadata of 5000 tokens that will actually be minted in the future. + * + * Lazy minting of NFT metafata happens from a start metadata ID (inclusive) to an end metadata ID (non-inclusive), + * where the lazy minted metadata lives at `providedBaseURI/${metadataId}` for each unit metadata. + * + * At the time of actual minting, the minter specifies the tier of NFTs they're minting. So, the order in which lazy minted + * metadata for a tier is assigned integer IDs may differ from the actual tokenIds minted for a tier. + * + * This is a mapping from an actually minted end tokenId -> the range of lazy minted metadata that now belongs + * to NFTs of [start tokenId, end tokenid). + */ + mapping(uint256 => TokenRange) private proxyTokenRange; + + /// @dev Mapping from tier -> the metadata ID up till which metadata IDs have been mapped to minted NFTs' tokenIds. + mapping(string => uint256) private nextMetadataIdToMapFromTier; + + /// @dev Mapping from tier -> how many units of lazy minted metadata have not yet been mapped to minted NFTs' tokenIds. + mapping(string => uint256) private totalRemainingInTier; + + /// @dev Mapping from batchId => tokenId offset for that batchId. + mapping(uint256 => bytes32) private tokenIdOffset; + + /// @dev Mapping from hash(tier, "minted") -> total minted in tier. + mapping(bytes32 => uint256) private totalsForTier; + + /*/////////////////////////////////////////////////////////////// + Events + //////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when tokens are claimed via `claimWithSignature`. + event TokensClaimed( + address indexed claimer, + address indexed receiver, + uint256 startTokenId, + uint256 quantityClaimed, + string[] tiersInPriority + ); + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Initializes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _royaltyRecipient, + uint16 _royaltyBps + ) external initializer { + bytes32 _transferRole = keccak256("TRANSFER_ROLE"); + bytes32 _minterRole = keccak256("MINTER_ROLE"); + + // Initialize inherited contracts, most base-like -> most derived. + __ERC2771Context_init(_trustedForwarders); + __ERC721A_init(_name, _symbol); + __SignatureAction_init(); + + _setupContractURI(_contractURI); + _setupOwner(_defaultAdmin); + + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + _setupRole(_minterRole, _defaultAdmin); + _setupRole(_transferRole, _defaultAdmin); + _setupRole(_transferRole, address(0)); + + _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + _setupPrimarySaleRecipient(_saleRecipient); + + transferRole = _transferRole; + minterRole = _minterRole; + } + + /*/////////////////////////////////////////////////////////////// + ERC 165 / 721 / 2981 logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the URI for a given tokenId. + function tokenURI(uint256 _tokenId) public view override returns (string memory) { + // Retrieve metadata ID for token. + uint256 metadataId = _getMetadataId(_tokenId); + + // Use metadata ID to return token metadata. + (uint256 batchId, uint256 index) = _getBatchId(metadataId); + string memory batchUri = _getBaseURI(metadataId); + + if (isEncryptedBatch(batchId)) { + return string(abi.encodePacked(batchUri, "0")); + } else { + uint256 fairMetadataId = _getFairMetadataId(metadataId, batchId, index); + return string(abi.encodePacked(batchUri, fairMetadataId.toString())); + } + } + + /// @dev See ERC 165 + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC721AUpgradeable, IERC165) returns (bool) { + return super.supportsInterface(interfaceId) || type(IERC2981Upgradeable).interfaceId == interfaceId; + } + + /*/////////////////////////////////////////////////////////////// + Lazy minting + delayed-reveal logic + //////////////////////////////////////////////////////////////* + + /** + * @dev Lets an account with `MINTER_ROLE` lazy mint 'n' NFTs. + * The URIs for each token is the provided `_baseURIForTokens` + `{tokenId}`. + */ + function lazyMint( + uint256 _amount, + string calldata _baseURIForTokens, + string calldata _tier, + bytes calldata _data + ) public override returns (uint256 batchId) { + if (_data.length > 0) { + (bytes memory encryptedURI, bytes32 provenanceHash) = abi.decode(_data, (bytes, bytes32)); + if (encryptedURI.length != 0 && provenanceHash != "") { + _setEncryptedData(nextTokenIdToLazyMint + _amount, _data); + } + } + + totalRemainingInTier[_tier] += _amount; + + uint256 startId = nextTokenIdToLazyMint; + if (isTierEmpty(_tier) || nextMetadataIdToMapFromTier[_tier] == type(uint256).max) { + nextMetadataIdToMapFromTier[_tier] = startId; + } + + return super.lazyMint(_amount, _baseURIForTokens, _tier, _data); + } + + /// @dev Lets an account with `MINTER_ROLE` reveal the URI for a batch of 'delayed-reveal' NFTs. + function reveal( + uint256 _index, + bytes calldata _key + ) external onlyRole(minterRole) returns (string memory revealedURI) { + uint256 batchId = getBatchIdAtIndex(_index); + revealedURI = getRevealURI(batchId, _key); + + _setEncryptedData(batchId, ""); + _setBaseURI(batchId, revealedURI); + + _scrambleOffset(batchId, _key); + + emit TokenURIRevealed(_index, revealedURI); + } + + /*/////////////////////////////////////////////////////////////// + Claiming lazy minted tokens logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Claim lazy minted tokens via signature. + function claimWithSignature( + GenericRequest calldata _req, + bytes calldata _signature + ) external payable returns (address signer) { + ( + string[] memory tiersInPriority, + address to, + address royaltyRecipient, + uint256 royaltyBps, + address primarySaleRecipient, + uint256 quantity, + uint256 totalPrice, + address currency + ) = abi.decode(_req.data, (string[], address, address, uint256, address, uint256, uint256, address)); + + if (quantity == 0) { + revert("0 qty"); + } + + uint256 tokenIdToMint = _currentIndex; + if (tokenIdToMint + quantity > nextTokenIdToLazyMint) { + revert("!Tokens"); + } + + // Verify and process payload. + signer = _processRequest(_req, _signature); + + // Collect price + collectPriceOnClaim(primarySaleRecipient, currency, totalPrice); + + // Set royalties, if applicable. + if (royaltyRecipient != address(0) && royaltyBps != 0) { + _setupRoyaltyInfoForToken(tokenIdToMint, royaltyRecipient, royaltyBps); + } + + // Mint tokens. + transferTokensOnClaim(to, quantity, tiersInPriority); + + emit TokensClaimed(_msgSender(), to, tokenIdToMint, quantity, tiersInPriority); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + /// @dev Collects and distributes the primary sale value of NFTs being claimed. + function collectPriceOnClaim(address _primarySaleRecipient, address _currency, uint256 _totalPrice) internal { + if (_totalPrice == 0) { + require(msg.value == 0, "!Value"); + return; + } + + address saleRecipient = _primarySaleRecipient == address(0) ? primarySaleRecipient() : _primarySaleRecipient; + + bool validMsgValue; + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { + validMsgValue = msg.value == _totalPrice; + } else { + validMsgValue = msg.value == 0; + } + require(validMsgValue, "Invalid msg value"); + + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), saleRecipient, _totalPrice); + } + + /// @dev Transfers the NFTs being claimed. + function transferTokensOnClaim(address _to, uint256 _totalQuantityBeingClaimed, string[] memory _tiers) internal { + uint256 startTokenIdToMint = _currentIndex; + + uint256 startIdToMap = startTokenIdToMint; + uint256 remaningToDistribute = _totalQuantityBeingClaimed; + + for (uint256 i = 0; i < _tiers.length; i += 1) { + string memory tier = _tiers[i]; + + uint256 qtyFulfilled = _getQuantityFulfilledByTier(tier, remaningToDistribute); + + if (qtyFulfilled == 0) { + continue; + } + + remaningToDistribute -= qtyFulfilled; + + _mapTokensToTier(tier, startIdToMap, qtyFulfilled); + + totalRemainingInTier[tier] -= qtyFulfilled; + totalsForTier[keccak256(abi.encodePacked(tier, "minted"))] += qtyFulfilled; + + if (remaningToDistribute > 0) { + startIdToMap += qtyFulfilled; + } else { + break; + } + } + + require(remaningToDistribute == 0, "Insufficient tokens in tiers."); + + _safeMint(_to, _totalQuantityBeingClaimed); + } + + /// @dev Maps lazy minted metadata to NFT tokenIds. + function _mapTokensToTier(string memory _tier, uint256 _startIdToMap, uint256 _quantity) private { + uint256 nextIdFromTier = nextMetadataIdToMapFromTier[_tier]; + uint256 startTokenId = _startIdToMap; + + TokenRange[] memory tokensInTier = tokensInTier[_tier]; + uint256 len = tokensInTier.length; + + uint256 qtyRemaining = _quantity; + + for (uint256 i = 0; i < len; i += 1) { + TokenRange memory range = tokensInTier[i]; + uint256 gap = 0; + + if (range.startIdInclusive <= nextIdFromTier && nextIdFromTier < range.endIdNonInclusive) { + uint256 proxyStartId = nextIdFromTier; + uint256 proxyEndId = proxyStartId + qtyRemaining <= range.endIdNonInclusive + ? proxyStartId + qtyRemaining + : range.endIdNonInclusive; + + gap = proxyEndId - proxyStartId; + + uint256 endTokenId = startTokenId + gap; + + endIdsAtMint[lengthEndIdsAtMint] = endTokenId; + lengthEndIdsAtMint += 1; + + tierAtEndId[endTokenId] = _tier; + proxyTokenRange[endTokenId] = TokenRange(proxyStartId, proxyEndId); + + startTokenId += gap; + qtyRemaining -= gap; + + if (nextIdFromTier + gap < range.endIdNonInclusive) { + nextIdFromTier += gap; + } else if (i < (len - 1)) { + nextIdFromTier = tokensInTier[i + 1].startIdInclusive; + } else { + nextIdFromTier = type(uint256).max; + } + } + + if (qtyRemaining == 0) { + nextMetadataIdToMapFromTier[_tier] = nextIdFromTier; + break; + } + } + } + + /// @dev Returns how much of the total-quantity-to-distribute can come from the given tier. + function _getQuantityFulfilledByTier( + string memory _tier, + uint256 _quantity + ) private view returns (uint256 fulfilled) { + uint256 total = totalRemainingInTier[_tier]; + + if (total >= _quantity) { + fulfilled = _quantity; + } else { + fulfilled = total; + } + } + + /// @dev Returns the tier that the given token is associated with. + function getTierForToken(uint256 _tokenId) external view returns (string memory) { + uint256 len = lengthEndIdsAtMint; + + for (uint256 i = 0; i < len; i += 1) { + uint256 endId = endIdsAtMint[i]; + + if (_tokenId < endId) { + return tierAtEndId[endId]; + } + } + + revert("!Tier"); + } + + /// @dev Returns the max `endIndex` that can be used with getTokensInTier. + function getTokensInTierLen() external view returns (uint256) { + return lengthEndIdsAtMint; + } + + /// @dev Returns all tokenIds that belong to the given tier. + function getTokensInTier( + string memory _tier, + uint256 _startIdx, + uint256 _endIdx + ) external view returns (TokenRange[] memory ranges) { + uint256 len = lengthEndIdsAtMint; + + require(_startIdx < _endIdx && _endIdx <= len, "TieredDrop: invalid indices."); + + uint256 numOfRangesForTier = 0; + bytes32 hashOfTier = keccak256(abi.encodePacked(_tier)); + + for (uint256 i = _startIdx; i < _endIdx; i += 1) { + bytes32 hashOfStoredTier = keccak256(abi.encodePacked(tierAtEndId[endIdsAtMint[i]])); + + if (hashOfStoredTier == hashOfTier) { + numOfRangesForTier += 1; + } + } + + ranges = new TokenRange[](numOfRangesForTier); + uint256 idx = 0; + + for (uint256 i = _startIdx; i < _endIdx; i += 1) { + bytes32 hashOfStoredTier = keccak256(abi.encodePacked(tierAtEndId[endIdsAtMint[i]])); + + if (hashOfStoredTier == hashOfTier) { + uint256 end = endIdsAtMint[i]; + + uint256 start = 0; + if (i > 0) { + start = endIdsAtMint[i - 1]; + } + + ranges[idx] = TokenRange(start, end); + idx += 1; + } + } + } + + /// @dev Returns the metadata ID for the given tokenID. + function _getMetadataId(uint256 _tokenId) private view returns (uint256) { + uint256 len = lengthEndIdsAtMint; + + for (uint256 i = 0; i < len; i += 1) { + if (_tokenId < endIdsAtMint[i]) { + uint256 targetEndId = endIdsAtMint[i]; + uint256 diff = targetEndId - _tokenId; + + TokenRange memory range = proxyTokenRange[targetEndId]; + + return range.endIdNonInclusive - diff; + } + } + + revert("!Metadata-ID"); + } + + /// @dev Returns the fair metadata ID for a given tokenId. + function _getFairMetadataId( + uint256 _metadataId, + uint256 _batchId, + uint256 _indexOfBatchId + ) private view returns (uint256 fairMetadataId) { + bytes32 bytesRandom = tokenIdOffset[_batchId]; + if (bytesRandom == bytes32(0)) { + return _metadataId; + } + + uint256 randomness = uint256(bytesRandom); + uint256 prevBatchId; + if (_indexOfBatchId > 0) { + prevBatchId = getBatchIdAtIndex(_indexOfBatchId - 1); + } + + uint256 batchSize = _batchId - prevBatchId; + uint256 offset = randomness % batchSize; + fairMetadataId = prevBatchId + ((_metadataId + offset) % batchSize); + } + + /// @dev Scrambles tokenId offset for a given batchId. + function _scrambleOffset(uint256 _batchId, bytes calldata _seed) private { + tokenIdOffset[_batchId] = keccak256(abi.encodePacked(_seed, block.timestamp, blockhash(block.number - 1))); + } + + /// @dev Returns whether a given address is authorized to sign mint requests. + function _isAuthorizedSigner(address _signer) internal view override returns (bool) { + return hasRole(minterRole, _signer); + } + + /// @dev Checks whether primary sale recipient can be set in the given execution context. + function _canSetPrimarySaleRecipient() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether owner can be set in the given execution context. + function _canSetOwner() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether royalty info can be set in the given execution context. + function _canSetRoyaltyInfo() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Returns whether lazy minting can be done in the given execution context. + function _canLazyMint() internal view virtual override returns (bool) { + return hasRole(minterRole, _msgSender()); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Returns the total amount of tokens minted in the contract. + */ + function totalMinted() external view returns (uint256) { + unchecked { + return _currentIndex - _startTokenId(); + } + } + + /// @dev Returns the total number of tokens minted from the given tier. + function totalMintedInTier(string memory _tier) external view returns (uint256) { + return totalsForTier[keccak256(abi.encodePacked(_tier, "minted"))]; + } + + /// @dev The tokenId of the next NFT that will be minted / lazy minted. + function nextTokenIdToMint() external view returns (uint256) { + return nextTokenIdToLazyMint; + } + + /// @dev Burns `tokenId`. See {ERC721-_burn}. + function burn(uint256 tokenId) external virtual { + // note: ERC721AUpgradeable's `_burn(uint256,bool)` internally checks for token approvals. + _burn(tokenId, true); + } + + /// @dev See {ERC721-_beforeTokenTransfer}. + function _beforeTokenTransfers( + address from, + address to, + uint256 startTokenId, + uint256 quantity + ) internal virtual override { + super._beforeTokenTransfers(from, to, startTokenId, quantity); + + // if transfer is restricted on the contract, we still want to allow burning and minting + if (!hasRole(transferRole, address(0)) && from != address(0) && to != address(0)) { + if (!hasRole(transferRole, from) && !hasRole(transferRole, to)) { + revert("!TRANSFER"); + } + } + } + + function _msgSender() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } + + function _msgData() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable) + returns (bytes calldata) + { + return ERC2771ContextUpgradeable._msgData(); + } +} diff --git a/contracts/prebuilts/token/TokenERC1155.sol b/contracts/prebuilts/token/TokenERC1155.sol new file mode 100644 index 000000000..857e0e7d2 --- /dev/null +++ b/contracts/prebuilts/token/TokenERC1155.sol @@ -0,0 +1,576 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// Interface +import { ITokenERC1155 } from "../interface/token/ITokenERC1155.sol"; + +import "../../infra/interface/IThirdwebContract.sol"; +import "../../extension/interface/IPlatformFee.sol"; +import "../../extension/interface/IPrimarySale.sol"; +import "../../extension/interface/IRoyalty.sol"; +import "../../extension/interface/IOwnable.sol"; + +import "../../extension/NFTMetadata.sol"; + +// Token +import "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol"; + +// Signature utils +import "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol"; + +// Access Control + security +import "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; + +// Utils +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; +import "../../extension/Multicall.sol"; +import "../../lib/CurrencyTransferLib.sol"; +import "../../lib/FeeType.sol"; +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; + +// Helper interfaces +import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; + +contract TokenERC1155 is + Initializable, + IThirdwebContract, + IOwnable, + IRoyalty, + IPrimarySale, + IPlatformFee, + EIP712Upgradeable, + ReentrancyGuardUpgradeable, + ERC2771ContextUpgradeable, + Multicall, + AccessControlEnumerableUpgradeable, + ERC1155Upgradeable, + ITokenERC1155, + NFTMetadata +{ + using ECDSAUpgradeable for bytes32; + using StringsUpgradeable for uint256; + + bytes32 private constant MODULE_TYPE = bytes32("TokenERC1155"); + uint256 private constant VERSION = 1; + + address public constant DEFAULT_FEE_RECIPIENT = 0x1Af20C6B23373350aD464700B5965CE4B0D2aD94; + uint16 private constant DEFAULT_FEE_BPS = 100; + + // Token name + string public name; + + // Token symbol + string public symbol; + + bytes32 private constant TYPEHASH = + keccak256( + "MintRequest(address to,address royaltyRecipient,uint256 royaltyBps,address primarySaleRecipient,uint256 tokenId,string uri,uint256 quantity,uint256 pricePerToken,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + + /// @dev Only TRANSFER_ROLE holders can have tokens transferred from or to them, during restricted transfers. + bytes32 private constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE"); + /// @dev Only MINTER_ROLE holders can sign off on `MintRequest`s. + bytes32 private constant MINTER_ROLE = keccak256("MINTER_ROLE"); + /// @dev Only METADATA_ROLE holders can update NFT metadata. + bytes32 private constant METADATA_ROLE = keccak256("METADATA_ROLE"); + + /// @dev Max bps in the thirdweb system + uint256 private constant MAX_BPS = 10_000; + + /// @dev Owner of the contract (purpose: OpenSea compatibility, etc.) + address private _owner; + + /// @dev The next token ID of the NFT to mint. + uint256 public nextTokenIdToMint; + + /// @dev The adress that receives all primary sales value. + address public primarySaleRecipient; + + /// @dev The adress that receives all primary sales value. + address public platformFeeRecipient; + + /// @dev The recipient of who gets the royalty. + address private royaltyRecipient; + + /// @dev The percentage of royalty how much royalty in basis points. + uint128 private royaltyBps; + + /// @dev The % of primary sales collected by the contract as fees. + uint128 private platformFeeBps; + + /// @dev The flat amount collected by the contract as fees on primary sales. + uint256 private flatPlatformFee; + + /// @dev Fee type variants: percentage fee and flat fee + PlatformFeeType private platformFeeType; + + /// @dev Contract level metadata. + string public contractURI; + + /// @dev Mapping from mint request UID => whether the mint request is processed. + mapping(bytes32 => bool) private minted; + + /// @dev Token ID => total circulating supply of tokens with that ID. + mapping(uint256 => uint256) public totalSupply; + + /// @dev Token ID => the address of the recipient of primary sales. + mapping(uint256 => address) public saleRecipientForToken; + + /// @dev Token ID => royalty recipient and bps for token + mapping(uint256 => RoyaltyInfo) private royaltyInfoForToken; + + constructor() initializer {} + + /// @dev Initializes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _primarySaleRecipient, + address _royaltyRecipient, + uint128 _royaltyBps, + uint128 _platformFeeBps, + address _platformFeeRecipient + ) external initializer { + // Initialize inherited contracts, most base-like -> most derived. + __ReentrancyGuard_init(); + __EIP712_init("TokenERC1155", "1"); + __ERC2771Context_init(_trustedForwarders); + __ERC1155_init(""); + + // Initialize this contract's state. + name = _name; + symbol = _symbol; + royaltyRecipient = _royaltyRecipient; + royaltyBps = _royaltyBps; + platformFeeRecipient = _platformFeeRecipient; + primarySaleRecipient = _primarySaleRecipient; + contractURI = _contractURI; + + require(_platformFeeBps <= MAX_BPS, "exceeds MAX_BPS"); + platformFeeBps = _platformFeeBps; + + // Fee type Bps by default + platformFeeType = PlatformFeeType.Bps; + + _owner = _defaultAdmin; + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + _setupRole(MINTER_ROLE, _defaultAdmin); + _setupRole(TRANSFER_ROLE, _defaultAdmin); + _setupRole(TRANSFER_ROLE, address(0)); + + _setupRole(METADATA_ROLE, _defaultAdmin); + _setRoleAdmin(METADATA_ROLE, METADATA_ROLE); + + emit PrimarySaleRecipientUpdated(_primarySaleRecipient); + emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); + emit DefaultRoyalty(_royaltyRecipient, _royaltyBps); + } + + /// ===== Public functions ===== + + /// @dev Returns the module type of the contract. + function contractType() external pure returns (bytes32) { + return MODULE_TYPE; + } + + /// @dev Returns the version of the contract. + function contractVersion() external pure returns (uint8) { + return uint8(VERSION); + } + + /** + * @dev Returns the address of the current owner. + */ + function owner() public view returns (address) { + return hasRole(DEFAULT_ADMIN_ROLE, _owner) ? _owner : address(0); + } + + /// @dev Verifies that a mint request is signed by an account holding MINTER_ROLE (at the time of the function call). + function verify(MintRequest calldata _req, bytes calldata _signature) public view returns (bool, address) { + address signer = recoverAddress(_req, _signature); + return (!minted[_req.uid] && hasRole(MINTER_ROLE, signer), signer); + } + + /// @dev Returns the URI for a tokenId + function uri(uint256 _tokenId) public view override returns (string memory) { + return _tokenURI[_tokenId]; + } + + /// @dev Lets an account with MINTER_ROLE mint an NFT. + function mintTo( + address _to, + uint256 _tokenId, + string calldata _uri, + uint256 _amount + ) external nonReentrant onlyRole(MINTER_ROLE) { + uint256 tokenIdToMint; + if (_tokenId == type(uint256).max) { + tokenIdToMint = nextTokenIdToMint; + nextTokenIdToMint += 1; + } else { + require(_tokenId < nextTokenIdToMint, "invalid id"); + tokenIdToMint = _tokenId; + } + + // `_mintTo` is re-used. `mintTo` just adds a minter role check. + _mintTo(_to, _uri, tokenIdToMint, _amount); + } + + /// ===== External functions ===== + + /// @dev See EIP-2981 + function royaltyInfo( + uint256 tokenId, + uint256 salePrice + ) external view virtual returns (address receiver, uint256 royaltyAmount) { + (address recipient, uint256 bps) = getRoyaltyInfoForToken(tokenId); + receiver = recipient; + royaltyAmount = (salePrice * bps) / MAX_BPS; + } + + /// @dev Mints an NFT according to the provided mint request. + function mintWithSignature(MintRequest calldata _req, bytes calldata _signature) external payable nonReentrant { + address signer = verifyRequest(_req, _signature); + address receiver = _req.to; + + uint256 tokenIdToMint; + if (_req.tokenId == type(uint256).max) { + tokenIdToMint = nextTokenIdToMint; + nextTokenIdToMint += 1; + } else { + require(_req.tokenId < nextTokenIdToMint, "invalid id"); + tokenIdToMint = _req.tokenId; + } + + if (_req.royaltyRecipient != address(0)) { + royaltyInfoForToken[tokenIdToMint] = RoyaltyInfo({ + recipient: _req.royaltyRecipient, + bps: _req.royaltyBps + }); + } + + _mintTo(receiver, _req.uri, tokenIdToMint, _req.quantity); + + collectPrice(_req); + + emit TokensMintedWithSignature(signer, receiver, tokenIdToMint, _req); + } + + // ===== Setter functions ===== + + /// @dev Lets a module admin set the default recipient of all primary sales. + function setPrimarySaleRecipient(address _saleRecipient) external onlyRole(DEFAULT_ADMIN_ROLE) { + primarySaleRecipient = _saleRecipient; + emit PrimarySaleRecipientUpdated(_saleRecipient); + } + + /// @dev Lets a module admin update the royalty bps and recipient. + function setDefaultRoyaltyInfo( + address _royaltyRecipient, + uint256 _royaltyBps + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(_royaltyBps <= MAX_BPS, "exceed royalty bps"); + + royaltyRecipient = _royaltyRecipient; + royaltyBps = uint128(_royaltyBps); + + emit DefaultRoyalty(_royaltyRecipient, _royaltyBps); + } + + /// @dev Lets a module admin set the royalty recipient for a particular token Id. + function setRoyaltyInfoForToken( + uint256 _tokenId, + address _recipient, + uint256 _bps + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(_bps <= MAX_BPS, "exceed royalty bps"); + + royaltyInfoForToken[_tokenId] = RoyaltyInfo({ recipient: _recipient, bps: _bps }); + + emit RoyaltyForToken(_tokenId, _recipient, _bps); + } + + /// @dev Lets a module admin update the fees on primary sales. + function setPlatformFeeInfo( + address _platformFeeRecipient, + uint256 _platformFeeBps + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(_platformFeeBps <= MAX_BPS, "exceeds MAX_BPS"); + + platformFeeBps = uint64(_platformFeeBps); + platformFeeRecipient = _platformFeeRecipient; + + emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); + } + + /// @dev Lets a module admin set a flat fee on primary sales. + function setFlatPlatformFeeInfo( + address _platformFeeRecipient, + uint256 _flatFee + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + flatPlatformFee = _flatFee; + platformFeeRecipient = _platformFeeRecipient; + + emit FlatPlatformFeeUpdated(_platformFeeRecipient, _flatFee); + } + + /// @dev Lets a module admin set a flat fee on primary sales. + function setPlatformFeeType(PlatformFeeType _feeType) external onlyRole(DEFAULT_ADMIN_ROLE) { + platformFeeType = _feeType; + + emit PlatformFeeTypeUpdated(_feeType); + } + + /// @dev Lets a module admin set a new owner for the contract. The new owner must be a module admin. + function setOwner(address _newOwner) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(hasRole(DEFAULT_ADMIN_ROLE, _newOwner), "new owner not module admin."); + address _prevOwner = _owner; + _owner = _newOwner; + + emit OwnerUpdated(_prevOwner, _newOwner); + } + + /// @dev Lets a module admin set the URI for contract-level metadata. + function setContractURI(string calldata _uri) external onlyRole(DEFAULT_ADMIN_ROLE) { + contractURI = _uri; + } + + /// ===== Getter functions ===== + + /// @dev Returns the platform fee bps and recipient. + function getPlatformFeeInfo() external view returns (address, uint16) { + return (platformFeeRecipient, uint16(platformFeeBps)); + } + + /// @dev Returns the flat platform fee and recipient. + function getFlatPlatformFeeInfo() external view returns (address, uint256) { + return (platformFeeRecipient, flatPlatformFee); + } + + /// @dev Returns the platform fee type. + function getPlatformFeeType() external view returns (PlatformFeeType) { + return platformFeeType; + } + + /// @dev Returns default royalty info. + function getDefaultRoyaltyInfo() external view returns (address, uint16) { + return (royaltyRecipient, uint16(royaltyBps)); + } + + /// @dev Returns the royalty recipient for a particular token Id. + function getRoyaltyInfoForToken(uint256 _tokenId) public view returns (address, uint16) { + RoyaltyInfo memory royaltyForToken = royaltyInfoForToken[_tokenId]; + + return + royaltyForToken.recipient == address(0) + ? (royaltyRecipient, uint16(royaltyBps)) + : (royaltyForToken.recipient, uint16(royaltyForToken.bps)); + } + + /// ===== Internal functions ===== + + /// @dev Mints an NFT to `to` + function _mintTo(address _to, string calldata _uri, uint256 _tokenId, uint256 _amount) internal { + if (bytes(_tokenURI[_tokenId]).length == 0) { + _setTokenURI(_tokenId, _uri); + } + + _mint(_to, _tokenId, _amount, ""); + + emit TokensMinted(_to, _tokenId, _tokenURI[_tokenId], _amount); + } + + /// @dev Returns the address of the signer of the mint request. + function recoverAddress(MintRequest calldata _req, bytes calldata _signature) internal view returns (address) { + return _hashTypedDataV4(keccak256(_encodeRequest(_req))).recover(_signature); + } + + /// @dev Resolves 'stack too deep' error in `recoverAddress`. + function _encodeRequest(MintRequest calldata _req) internal pure returns (bytes memory) { + return + bytes.concat( + abi.encode( + TYPEHASH, + _req.to, + _req.royaltyRecipient, + _req.royaltyBps, + _req.primarySaleRecipient, + _req.tokenId, + keccak256(bytes(_req.uri)) + ), + abi.encode( + _req.quantity, + _req.pricePerToken, + _req.currency, + _req.validityStartTimestamp, + _req.validityEndTimestamp, + _req.uid + ) + ); + } + + /// @dev Verifies that a mint request is valid. + function verifyRequest(MintRequest calldata _req, bytes calldata _signature) internal returns (address) { + (bool success, address signer) = verify(_req, _signature); + require(success, "invalid signature"); + + require( + _req.validityStartTimestamp <= block.timestamp && _req.validityEndTimestamp >= block.timestamp, + "request expired" + ); + require(_req.to != address(0), "recipient undefined"); + require(_req.quantity > 0, "zero quantity"); + + minted[_req.uid] = true; + + return signer; + } + + /// @dev Collects and distributes the primary sale value of tokens being claimed. + function collectPrice(MintRequest calldata _req) internal { + if (_req.pricePerToken == 0) { + require(msg.value == 0, "!Value"); + return; + } + + uint256 totalPrice = _req.pricePerToken * _req.quantity; + uint256 platformFeesTw = (totalPrice * DEFAULT_FEE_BPS) / MAX_BPS; + uint256 platformFees = platformFeeType == PlatformFeeType.Flat + ? flatPlatformFee + : ((totalPrice * platformFeeBps) / MAX_BPS); + require(totalPrice >= platformFees + platformFeesTw, "price less than platform fee"); + + if (_req.currency == CurrencyTransferLib.NATIVE_TOKEN) { + require(msg.value == totalPrice, "must send total price."); + } else { + require(msg.value == 0, "msg value not zero"); + } + + address saleRecipient = _req.primarySaleRecipient == address(0) + ? primarySaleRecipient + : _req.primarySaleRecipient; + + CurrencyTransferLib.transferCurrency(_req.currency, _msgSender(), DEFAULT_FEE_RECIPIENT, platformFeesTw); + CurrencyTransferLib.transferCurrency(_req.currency, _msgSender(), platformFeeRecipient, platformFees); + CurrencyTransferLib.transferCurrency( + _req.currency, + _msgSender(), + saleRecipient, + totalPrice - platformFees - platformFeesTw + ); + } + + /// ===== Low-level overrides ===== + + /// @dev Lets a token owner burn the tokens they own (i.e. destroy for good) + function burn(address account, uint256 id, uint256 value) public virtual { + require( + account == _msgSender() || isApprovedForAll(account, _msgSender()), + "ERC1155: caller is not owner nor approved." + ); + + _burn(account, id, value); + } + + /// @dev Lets a token owner burn multiple tokens they own at once (i.e. destroy for good) + function burnBatch(address account, uint256[] memory ids, uint256[] memory values) public virtual { + require( + account == _msgSender() || isApprovedForAll(account, _msgSender()), + "ERC1155: caller is not owner nor approved." + ); + + _burnBatch(account, ids, values); + } + + /** + * @dev See {ERC1155-_beforeTokenTransfer}. + */ + function _beforeTokenTransfer( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal virtual override { + super._beforeTokenTransfer(operator, from, to, ids, amounts, data); + + // if transfer is restricted on the contract, we still want to allow burning and minting + if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { + require(hasRole(TRANSFER_ROLE, from) || hasRole(TRANSFER_ROLE, to), "restricted to TRANSFER_ROLE holders."); + } + + if (from == address(0)) { + for (uint256 i = 0; i < ids.length; ++i) { + totalSupply[ids[i]] += amounts[i]; + } + } + + if (to == address(0)) { + for (uint256 i = 0; i < ids.length; ++i) { + totalSupply[ids[i]] -= amounts[i]; + } + } + } + + function supportsInterface( + bytes4 interfaceId + ) + public + view + virtual + override(AccessControlEnumerableUpgradeable, ERC1155Upgradeable, IERC165Upgradeable, IERC165) + returns (bool) + { + return + super.supportsInterface(interfaceId) || + interfaceId == type(IERC1155Upgradeable).interfaceId || + interfaceId == type(IERC2981Upgradeable).interfaceId; + } + + /// @dev Returns whether metadata can be set in the given execution context. + function _canSetMetadata() internal view virtual override returns (bool) { + return hasRole(METADATA_ROLE, _msgSender()); + } + + /// @dev Returns whether metadata can be frozen in the given execution context. + function _canFreezeMetadata() internal view virtual override returns (bool) { + return hasRole(METADATA_ROLE, _msgSender()); + } + + function _msgSender() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } + + function _msgData() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable) + returns (bytes calldata) + { + return ERC2771ContextUpgradeable._msgData(); + } +} diff --git a/contracts/prebuilts/token/TokenERC20.sol b/contracts/prebuilts/token/TokenERC20.sol new file mode 100644 index 000000000..7ef591045 --- /dev/null +++ b/contracts/prebuilts/token/TokenERC20.sol @@ -0,0 +1,312 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +//Interface +import { ITokenERC20 } from "../interface/token/ITokenERC20.sol"; + +import "../../infra/interface/IThirdwebContract.sol"; +import "../../extension/interface/IPlatformFee.sol"; +import "../../extension/interface/IPrimarySale.sol"; + +// Token +import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20VotesUpgradeable.sol"; + +// Security +import "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; + +// Signature utils +import "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol"; + +// Meta transactions +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; + +// Utils +import "../../extension/Multicall.sol"; +import "../../lib/CurrencyTransferLib.sol"; +import "../../lib/FeeType.sol"; + +contract TokenERC20 is + Initializable, + IThirdwebContract, + IPrimarySale, + IPlatformFee, + ReentrancyGuardUpgradeable, + ERC2771ContextUpgradeable, + Multicall, + ERC20BurnableUpgradeable, + ERC20VotesUpgradeable, + ITokenERC20, + AccessControlEnumerableUpgradeable +{ + using ECDSAUpgradeable for bytes32; + + bytes32 private constant MODULE_TYPE = bytes32("TokenERC20"); + uint256 private constant VERSION = 1; + + address public constant DEFAULT_FEE_RECIPIENT = 0x1Af20C6B23373350aD464700B5965CE4B0D2aD94; + uint16 private constant DEFAULT_FEE_BPS = 100; + + bytes32 private constant TYPEHASH = + keccak256( + "MintRequest(address to,address primarySaleRecipient,uint256 quantity,uint256 price,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + + bytes32 internal constant MINTER_ROLE = keccak256("MINTER_ROLE"); + bytes32 internal constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE"); + + /// @dev Returns the URI for the storefront-level metadata of the contract. + string public contractURI; + + /// @dev Max bps in the thirdweb system + uint128 internal constant MAX_BPS = 10_000; + + /// @dev The % of primary sales collected by the contract as fees. + uint128 private platformFeeBps; + + /// @dev The adress that receives all primary sales value. + address internal platformFeeRecipient; + + /// @dev The adress that receives all primary sales value. + address public primarySaleRecipient; + + /// @dev Mapping from mint request UID => whether the mint request is processed. + mapping(bytes32 => bool) private minted; + + constructor() initializer {} + + /// @dev Initializes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _primarySaleRecipient, + address _platformFeeRecipient, + uint256 _platformFeeBps + ) external initializer { + __ReentrancyGuard_init(); + __ERC2771Context_init_unchained(_trustedForwarders); + __ERC20Permit_init(_name); + __ERC20_init_unchained(_name, _symbol); + + contractURI = _contractURI; + primarySaleRecipient = _primarySaleRecipient; + platformFeeRecipient = _platformFeeRecipient; + + require(_platformFeeBps <= MAX_BPS, "exceeds MAX_BPS"); + platformFeeBps = uint128(_platformFeeBps); + + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + _setupRole(TRANSFER_ROLE, _defaultAdmin); + _setupRole(MINTER_ROLE, _defaultAdmin); + _setupRole(TRANSFER_ROLE, address(0)); + + emit PrimarySaleRecipientUpdated(_primarySaleRecipient); + emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); + } + + /// @dev Returns the module type of the contract. + function contractType() external pure virtual returns (bytes32) { + return MODULE_TYPE; + } + + /// @dev Returns the version of the contract. + function contractVersion() external pure virtual returns (uint8) { + return uint8(VERSION); + } + + function _afterTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual override(ERC20Upgradeable, ERC20VotesUpgradeable) { + super._afterTokenTransfer(from, to, amount); + } + + /// @dev Runs on every transfer. + function _beforeTokenTransfer(address from, address to, uint256 amount) internal override { + super._beforeTokenTransfer(from, to, amount); + + if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { + require(hasRole(TRANSFER_ROLE, from) || hasRole(TRANSFER_ROLE, to), "transfers restricted."); + } + } + + function _mint(address account, uint256 amount) internal virtual override(ERC20Upgradeable, ERC20VotesUpgradeable) { + super._mint(account, amount); + } + + function _burn(address account, uint256 amount) internal virtual override(ERC20Upgradeable, ERC20VotesUpgradeable) { + super._burn(account, amount); + } + + /** + * @dev Creates `amount` new tokens for `to`. + * + * See {ERC20-_mint}. + * + * Requirements: + * + * - the caller must have the `MINTER_ROLE`. + */ + function mintTo(address to, uint256 amount) public virtual nonReentrant { + require(hasRole(MINTER_ROLE, _msgSender()), "not minter."); + _mintTo(to, amount); + } + + /// @dev Verifies that a mint request is signed by an account holding MINTER_ROLE (at the time of the function call). + function verify(MintRequest calldata _req, bytes calldata _signature) public view returns (bool, address) { + address signer = recoverAddress(_req, _signature); + return (!minted[_req.uid] && hasRole(MINTER_ROLE, signer), signer); + } + + /// @dev Mints tokens according to the provided mint request. + function mintWithSignature(MintRequest calldata _req, bytes calldata _signature) external payable nonReentrant { + address signer = verifyRequest(_req, _signature); + address receiver = _req.to; + + collectPrice(_req); + + _mintTo(receiver, _req.quantity); + + emit TokensMintedWithSignature(signer, receiver, _req); + } + + /// @dev Lets a module admin set the default recipient of all primary sales. + function setPrimarySaleRecipient(address _saleRecipient) external onlyRole(DEFAULT_ADMIN_ROLE) { + primarySaleRecipient = _saleRecipient; + emit PrimarySaleRecipientUpdated(_saleRecipient); + } + + /// @dev Lets a module admin update the fees on primary sales. + function setPlatformFeeInfo( + address _platformFeeRecipient, + uint256 _platformFeeBps + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(_platformFeeBps <= MAX_BPS, "exceeds MAX_BPS"); + + platformFeeBps = uint64(_platformFeeBps); + platformFeeRecipient = _platformFeeRecipient; + + emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); + } + + /// @dev Returns the platform fee bps and recipient. + function getPlatformFeeInfo() external view returns (address, uint16) { + return (platformFeeRecipient, uint16(platformFeeBps)); + } + + /// @dev Collects and distributes the primary sale value of tokens being claimed. + function collectPrice(MintRequest calldata _req) internal { + if (_req.price == 0) { + require(msg.value == 0, "!Value"); + return; + } + + uint256 platformFeesTw = (_req.price * DEFAULT_FEE_BPS) / MAX_BPS; + uint256 platformFees = (_req.price * platformFeeBps) / MAX_BPS; + + if (_req.currency == CurrencyTransferLib.NATIVE_TOKEN) { + require(msg.value == _req.price, "must send total price."); + } else { + require(msg.value == 0, "msg value not zero"); + } + + address saleRecipient = _req.primarySaleRecipient == address(0) + ? primarySaleRecipient + : _req.primarySaleRecipient; + + CurrencyTransferLib.transferCurrency(_req.currency, _msgSender(), DEFAULT_FEE_RECIPIENT, platformFeesTw); + CurrencyTransferLib.transferCurrency(_req.currency, _msgSender(), platformFeeRecipient, platformFees); + CurrencyTransferLib.transferCurrency( + _req.currency, + _msgSender(), + saleRecipient, + _req.price - platformFees - platformFeesTw + ); + } + + /// @dev Mints `amount` of tokens to `to` + function _mintTo(address _to, uint256 _amount) internal { + _mint(_to, _amount); + emit TokensMinted(_to, _amount); + } + + /// @dev Verifies that a mint request is valid. + function verifyRequest(MintRequest calldata _req, bytes calldata _signature) internal returns (address) { + (bool success, address signer) = verify(_req, _signature); + require(success, "invalid signature"); + + require( + _req.validityStartTimestamp <= block.timestamp && _req.validityEndTimestamp >= block.timestamp, + "request expired" + ); + require(_req.to != address(0), "recipient undefined"); + require(_req.quantity > 0, "zero quantity"); + + minted[_req.uid] = true; + + return signer; + } + + /// @dev Returns the address of the signer of the mint request. + function recoverAddress(MintRequest calldata _req, bytes calldata _signature) internal view returns (address) { + return _hashTypedDataV4(keccak256(_encodeRequest(_req))).recover(_signature); + } + + /// @dev Resolves 'stack too deep' error in `recoverAddress`. + function _encodeRequest(MintRequest calldata _req) internal pure returns (bytes memory) { + return + abi.encode( + TYPEHASH, + _req.to, + _req.primarySaleRecipient, + _req.quantity, + _req.price, + _req.currency, + _req.validityStartTimestamp, + _req.validityEndTimestamp, + _req.uid + ); + } + + /// @dev Sets contract URI for the storefront-level metadata of the contract. + function setContractURI(string calldata _uri) external onlyRole(DEFAULT_ADMIN_ROLE) { + contractURI = _uri; + } + + function _msgSender() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } + + function _msgData() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable) + returns (bytes calldata) + { + return ERC2771ContextUpgradeable._msgData(); + } +} diff --git a/contracts/token/TokenERC721.sol b/contracts/prebuilts/token/TokenERC721.sol similarity index 75% rename from contracts/token/TokenERC721.sol rename to contracts/prebuilts/token/TokenERC721.sol index 4c0e117b4..0765e7104 100644 --- a/contracts/token/TokenERC721.sol +++ b/contracts/prebuilts/token/TokenERC721.sol @@ -1,14 +1,28 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.11; +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + // Interface -import { ITokenERC721 } from "../interfaces/token/ITokenERC721.sol"; +import { ITokenERC721 } from "../interface/token/ITokenERC721.sol"; -import "../interfaces/IThirdwebContract.sol"; -import "../extension/interface/IPlatformFee.sol"; -import "../extension/interface/IPrimarySale.sol"; -import "../extension/interface/IRoyalty.sol"; -import "../extension/interface/IOwnable.sol"; +import "../../infra/interface/IThirdwebContract.sol"; +import "../../extension/interface/IPlatformFee.sol"; +import "../../extension/interface/IPrimarySale.sol"; +import "../../extension/interface/IRoyalty.sol"; +import "../../extension/interface/IOwnable.sol"; + +//Extensions +import "../../extension/NFTMetadata.sol"; // Token import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; @@ -22,20 +36,17 @@ import "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgrad import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; // Meta transactions -import "../openzeppelin-presets/metatx/ERC2771ContextUpgradeable.sol"; +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; // Utils import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; -import "../lib/CurrencyTransferLib.sol"; -import "../lib/FeeType.sol"; +import "../../extension/Multicall.sol"; +import "../../lib/CurrencyTransferLib.sol"; +import "../../lib/FeeType.sol"; // Helper interfaces import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; -// Thirdweb top-level -import "../interfaces/ITWFee.sol"; - contract TokenERC721 is Initializable, IThirdwebContract, @@ -46,10 +57,11 @@ contract TokenERC721 is ReentrancyGuardUpgradeable, EIP712Upgradeable, ERC2771ContextUpgradeable, - MulticallUpgradeable, + Multicall, AccessControlEnumerableUpgradeable, ERC721EnumerableUpgradeable, - ITokenERC721 + ITokenERC721, + NFTMetadata { using ECDSAUpgradeable for bytes32; using StringsUpgradeable for uint256; @@ -57,6 +69,9 @@ contract TokenERC721 is bytes32 private constant MODULE_TYPE = bytes32("TokenERC721"); uint256 private constant VERSION = 1; + address public constant DEFAULT_FEE_RECIPIENT = 0x1Af20C6B23373350aD464700B5965CE4B0D2aD94; + uint16 private constant DEFAULT_FEE_BPS = 100; + bytes32 private constant TYPEHASH = keccak256( "MintRequest(address to,address royaltyRecipient,uint256 royaltyBps,address primarySaleRecipient,string uri,uint256 price,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" @@ -66,16 +81,12 @@ contract TokenERC721 is bytes32 private constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE"); /// @dev Only MINTER_ROLE holders can sign off on `MintRequest`s. bytes32 private constant MINTER_ROLE = keccak256("MINTER_ROLE"); + /// @dev Only METADATA_ROLE holders can update NFT metadata. + bytes32 private constant METADATA_ROLE = keccak256("METADATA_ROLE"); /// @dev Max bps in the thirdweb system uint256 private constant MAX_BPS = 10_000; - /// @dev The address interpreted as native token of the chain. - address private constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; - - /// @dev The thirdweb contract with fee related information. - ITWFee public immutable thirdwebFee; - /// @dev Owner of the contract (purpose: OpenSea compatibility, etc.) address private _owner; @@ -95,7 +106,7 @@ contract TokenERC721 is uint128 private royaltyBps; /// @dev The % of primary sales collected by the contract as fees. - uint128 public platformFeeBps; + uint128 private platformFeeBps; /// @dev Contract level metadata. string public contractURI; @@ -103,17 +114,12 @@ contract TokenERC721 is /// @dev Mapping from mint request UID => whether the mint request is processed. mapping(bytes32 => bool) private minted; - /// @dev Mapping from tokenId => URI - mapping(uint256 => string) private uri; - /// @dev Token ID => royalty recipient and bps for token mapping(uint256 => RoyaltyInfo) private royaltyInfoForToken; - constructor(address _thirdwebFee) initializer { - thirdwebFee = ITWFee(_thirdwebFee); - } + constructor() initializer {} - /// @dev Initiliazes the contract, like a constructor. + /// @dev Initializes the contract, like a constructor. function initialize( address _defaultAdmin, string memory _name, @@ -138,13 +144,23 @@ contract TokenERC721 is platformFeeRecipient = _platformFeeRecipient; primarySaleRecipient = _saleRecipient; contractURI = _contractURI; + + require(_platformFeeBps <= MAX_BPS, "exceeds MAX_BPS"); platformFeeBps = _platformFeeBps; _owner = _defaultAdmin; _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); _setupRole(MINTER_ROLE, _defaultAdmin); + + _setupRole(METADATA_ROLE, _defaultAdmin); + _setRoleAdmin(METADATA_ROLE, METADATA_ROLE); + _setupRole(TRANSFER_ROLE, _defaultAdmin); _setupRole(TRANSFER_ROLE, address(0)); + + emit PrimarySaleRecipientUpdated(_saleRecipient); + emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); + emit DefaultRoyalty(_royaltyRecipient, _royaltyBps); } /// ===== Public functions ===== @@ -174,11 +190,11 @@ contract TokenERC721 is /// @dev Returns the URI for a tokenId function tokenURI(uint256 _tokenId) public view override returns (string memory) { - return uri[_tokenId]; + return _tokenURI[_tokenId]; } /// @dev Lets an account with MINTER_ROLE mint an NFT. - function mintTo(address _to, string calldata _uri) external onlyRole(MINTER_ROLE) returns (uint256) { + function mintTo(address _to, string calldata _uri) external nonReentrant onlyRole(MINTER_ROLE) returns (uint256) { // `_mintTo` is re-used. `mintTo` just adds a minter role check. return _mintTo(_to, _uri); } @@ -186,26 +202,22 @@ contract TokenERC721 is /// ===== External functions ===== /// @dev See EIP-2981 - function royaltyInfo(uint256 tokenId, uint256 salePrice) - external - view - virtual - returns (address receiver, uint256 royaltyAmount) - { + function royaltyInfo( + uint256 tokenId, + uint256 salePrice + ) external view virtual returns (address receiver, uint256 royaltyAmount) { (address recipient, uint256 bps) = getRoyaltyInfoForToken(tokenId); receiver = recipient; royaltyAmount = (salePrice * bps) / MAX_BPS; } /// @dev Mints an NFT according to the provided mint request. - function mintWithSignature(MintRequest calldata _req, bytes calldata _signature) - external - payable - nonReentrant - returns (uint256 tokenIdMinted) - { + function mintWithSignature( + MintRequest calldata _req, + bytes calldata _signature + ) external payable nonReentrant returns (uint256 tokenIdMinted) { address signer = verifyRequest(_req, _signature); - address receiver = _req.to == address(0) ? _msgSender() : _req.to; + address receiver = _req.to; tokenIdMinted = _mintTo(receiver, _req.uri); @@ -230,10 +242,10 @@ contract TokenERC721 is } /// @dev Lets a module admin update the royalty bps and recipient. - function setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) - external - onlyRole(DEFAULT_ADMIN_ROLE) - { + function setDefaultRoyaltyInfo( + address _royaltyRecipient, + uint256 _royaltyBps + ) external onlyRole(DEFAULT_ADMIN_ROLE) { require(_royaltyBps <= MAX_BPS, "exceed royalty bps"); royaltyRecipient = _royaltyRecipient; @@ -256,11 +268,11 @@ contract TokenERC721 is } /// @dev Lets a module admin update the fees on primary sales. - function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) - external - onlyRole(DEFAULT_ADMIN_ROLE) - { - require(_platformFeeBps <= MAX_BPS, "bps <= 10000."); + function setPlatformFeeInfo( + address _platformFeeRecipient, + uint256 _platformFeeBps + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(_platformFeeBps <= MAX_BPS, "exceeds MAX_BPS"); platformFeeBps = uint64(_platformFeeBps); platformFeeRecipient = _platformFeeRecipient; @@ -311,9 +323,10 @@ contract TokenERC721 is tokenIdToMint = nextTokenIdToMint; nextTokenIdToMint += 1; - uri[tokenIdToMint] = _uri; + require(bytes(_uri).length > 0, "empty uri."); + _setTokenURI(tokenIdToMint, _uri); - _mint(_to, tokenIdToMint); + _safeMint(_to, tokenIdToMint); emit TokensMinted(_to, tokenIdToMint, _uri); } @@ -350,6 +363,7 @@ contract TokenERC721 is _req.validityStartTimestamp <= block.timestamp && _req.validityEndTimestamp >= block.timestamp, "request expired" ); + require(_req.to != address(0), "recipient undefined"); minted[_req.uid] = true; @@ -357,31 +371,33 @@ contract TokenERC721 is } /// @dev Collects and distributes the primary sale value of tokens being claimed. - function collectPrice(MintRequest memory _req) internal { + function collectPrice(MintRequest calldata _req) internal { if (_req.price == 0) { + require(msg.value == 0, "!Value"); return; } uint256 totalPrice = _req.price; + uint256 platformFeesTw = (totalPrice * DEFAULT_FEE_BPS) / MAX_BPS; uint256 platformFees = (totalPrice * platformFeeBps) / MAX_BPS; - (address twFeeRecipient, uint256 twFeeBps) = thirdwebFee.getFeeInfo(address(this), FeeType.PRIMARY_SALE); - uint256 twFee = (totalPrice * twFeeBps) / MAX_BPS; - if (_req.currency == NATIVE_TOKEN) { + if (_req.currency == CurrencyTransferLib.NATIVE_TOKEN) { require(msg.value == totalPrice, "must send total price."); + } else { + require(msg.value == 0, "msg value not zero"); } address saleRecipient = _req.primarySaleRecipient == address(0) ? primarySaleRecipient : _req.primarySaleRecipient; + CurrencyTransferLib.transferCurrency(_req.currency, _msgSender(), DEFAULT_FEE_RECIPIENT, platformFeesTw); CurrencyTransferLib.transferCurrency(_req.currency, _msgSender(), platformFeeRecipient, platformFees); - CurrencyTransferLib.transferCurrency(_req.currency, _msgSender(), twFeeRecipient, twFee); CurrencyTransferLib.transferCurrency( _req.currency, _msgSender(), saleRecipient, - totalPrice - platformFees - twFee + totalPrice - platformFees - platformFeesTw ); } @@ -398,9 +414,10 @@ contract TokenERC721 is function _beforeTokenTransfer( address from, address to, - uint256 tokenId + uint256 tokenId, + uint256 batchSize ) internal virtual override(ERC721EnumerableUpgradeable) { - super._beforeTokenTransfer(from, to, tokenId); + super._beforeTokenTransfer(from, to, tokenId, batchSize); // if transfer is restricted on the contract, we still want to allow burning and minting if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { @@ -408,7 +425,19 @@ contract TokenERC721 is } } - function supportsInterface(bytes4 interfaceId) + /// @dev Returns whether metadata can be set in the given execution context. + function _canSetMetadata() internal view virtual override returns (bool) { + return hasRole(METADATA_ROLE, _msgSender()); + } + + /// @dev Returns whether metadata can be frozen in the given execution context. + function _canFreezeMetadata() internal view virtual override returns (bool) { + return hasRole(METADATA_ROLE, _msgSender()); + } + + function supportsInterface( + bytes4 interfaceId + ) public view virtual @@ -422,7 +451,7 @@ contract TokenERC721 is internal view virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) + override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) returns (address sender) { return ERC2771ContextUpgradeable._msgSender(); diff --git a/contracts/token/signatureMint.md b/contracts/prebuilts/token/signatureMint.md similarity index 98% rename from contracts/token/signatureMint.md rename to contracts/prebuilts/token/signatureMint.md index e1bd9d755..e30ba9d0f 100644 --- a/contracts/token/signatureMint.md +++ b/contracts/prebuilts/token/signatureMint.md @@ -66,7 +66,7 @@ struct MintRequest { | tokenId | The tokenId of the token to mint. (Only applicable for ERC1155 tokens)| | uri | The metadata URI of the token to mint. (Not applicable for ERC20 tokens)| | quantity | The quantity of tokens to mint.| -| pricePerToken | The price to pay per quantity of tokens minted.| +| pricePerToken | The price to pay per quantity of tokens minted. (For TokenERC20, this parameter is `price`, indicating the total price of all tokens)| | currency | The currency in which to pay the price per token minted.| | validityStartTimestamp | The unix timestamp after which the payload is valid.| | validityEndTimestamp | The unix timestamp at which the payload expires.| diff --git a/contracts/prebuilts/unaudited/airdrop/AirdropERC1155.sol b/contracts/prebuilts/unaudited/airdrop/AirdropERC1155.sol new file mode 100644 index 000000000..b27a0afcc --- /dev/null +++ b/contracts/prebuilts/unaudited/airdrop/AirdropERC1155.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// ========== External imports ========== +import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; + +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "../../../extension/Multicall.sol"; + +// ========== Internal imports ========== + +import "../../interface/airdrop/IAirdropERC1155.sol"; +import "../../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; + +// ========== Features ========== +import "../../../extension/PermissionsEnumerable.sol"; +import "../../../extension/ContractMetadata.sol"; + +contract AirdropERC1155 is + Initializable, + ContractMetadata, + PermissionsEnumerable, + ReentrancyGuardUpgradeable, + ERC2771ContextUpgradeable, + Multicall, + IAirdropERC1155 +{ + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + bytes32 private constant MODULE_TYPE = bytes32("AirdropERC1155"); + uint256 private constant VERSION = 2; + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + constructor() { + _disableInitializers(); + } + + /// @dev Initializes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _contractURI, + address[] memory _trustedForwarders + ) external initializer { + __ERC2771Context_init_unchained(_trustedForwarders); + + _setupContractURI(_contractURI); + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + __ReentrancyGuard_init(); + } + + /*/////////////////////////////////////////////////////////////// + Generic contract logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the type of the contract. + function contractType() external pure returns (bytes32) { + return MODULE_TYPE; + } + + /// @dev Returns the version of the contract. + function contractVersion() external pure returns (uint8) { + return uint8(VERSION); + } + + /*/////////////////////////////////////////////////////////////// + Airdrop logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Lets contract-owner send ERC1155 tokens to a list of addresses. + * @dev The token-owner should approve target tokens to Airdrop contract, + * which acts as operator for the tokens. + * + * @param _tokenAddress The contract address of the tokens to transfer. + * @param _tokenOwner The owner of the tokens to transfer. + * @param _contents List containing recipient, tokenId and amounts to airdrop. + */ + function airdropERC1155( + address _tokenAddress, + address _tokenOwner, + AirdropContent[] calldata _contents + ) external nonReentrant { + require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Not authorized."); + + uint256 len = _contents.length; + + for (uint256 i = 0; i < len; ) { + try + IERC1155(_tokenAddress).safeTransferFrom( + _tokenOwner, + _contents[i].recipient, + _contents[i].tokenId, + _contents[i].amount, + "" + ) + {} catch { + // revert if failure is due to unapproved tokens + require( + IERC1155(_tokenAddress).balanceOf(_tokenOwner, _contents[i].tokenId) >= _contents[i].amount && + IERC1155(_tokenAddress).isApprovedForAll(_tokenOwner, address(this)), + "Not balance or approved" + ); + + emit AirdropFailed( + _tokenAddress, + _tokenOwner, + _contents[i].recipient, + _contents[i].tokenId, + _contents[i].amount + ); + } + + unchecked { + i += 1; + } + } + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + /// @dev Checks whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev See ERC2771 + function _msgSender() + internal + view + virtual + override(ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } +} diff --git a/contracts/prebuilts/unaudited/airdrop/AirdropERC1155Claimable.sol b/contracts/prebuilts/unaudited/airdrop/AirdropERC1155Claimable.sol new file mode 100644 index 000000000..7d42b469a --- /dev/null +++ b/contracts/prebuilts/unaudited/airdrop/AirdropERC1155Claimable.sol @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// ========== External imports ========== +import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; + +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import { Multicall } from "../../../extension/Multicall.sol"; + +// ========== Internal imports ========== + +import "../../interface/airdrop/IAirdropERC1155Claimable.sol"; + +// ========== Features ========== + +import "../../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; +import "../../../lib/MerkleProof.sol"; + +contract AirdropERC1155Claimable is + Initializable, + ReentrancyGuardUpgradeable, + ERC2771ContextUpgradeable, + Multicall, + IAirdropERC1155Claimable +{ + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + /// @dev address of token being airdropped. + address public airdropTokenAddress; + + /// @dev address of owner of tokens being airdropped. + address public tokenOwner; + + /// @dev list of tokens to airdrop. + uint256[] public tokenIds; + + /// @dev airdrop expiration timestamp. + uint256 public expirationTimestamp; + + /*/////////////////////////////////////////////////////////////// + Mappings + //////////////////////////////////////////////////////////////*/ + + /// @dev Mapping from tokenId and claimer address to total number of tokens claimed. + mapping(uint256 => mapping(address => uint256)) public supplyClaimedByWallet; + + /// @dev claim limit for open/public claiming without allowlist. + mapping(uint256 => uint256) public openClaimLimitPerWallet; + + /// @dev number tokens available to claim for a tokenId. + mapping(uint256 => uint256) public availableAmount; + + /// @dev mapping of tokenId to merkle root of the allowlist of addresses eligible to claim. + mapping(uint256 => bytes32) public merkleRoot; + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + constructor() { + _disableInitializers(); + } + + /// @dev Initializes the contract, like a constructor. + function initialize( + address[] memory _trustedForwarders, + address _tokenOwner, + address _airdropTokenAddress, + uint256[] memory _tokenIds, + uint256[] memory _availableAmounts, + uint256 _expirationTimestamp, + uint256[] memory _openClaimLimitPerWallet, + bytes32[] memory _merkleRoot + ) external initializer { + __ReentrancyGuard_init(); + __ERC2771Context_init(_trustedForwarders); + + tokenOwner = _tokenOwner; + airdropTokenAddress = _airdropTokenAddress; + tokenIds = _tokenIds; + expirationTimestamp = _expirationTimestamp; + + require( + _openClaimLimitPerWallet.length == _tokenIds.length && + _merkleRoot.length == _tokenIds.length && + _availableAmounts.length == _tokenIds.length, + "length mismatch." + ); + + for (uint256 i = 0; i < _tokenIds.length; i++) { + merkleRoot[_tokenIds[i]] = _merkleRoot[i]; + openClaimLimitPerWallet[_tokenIds[i]] = _openClaimLimitPerWallet[i]; + availableAmount[_tokenIds[i]] = _availableAmounts[i]; + } + } + + /*/////////////////////////////////////////////////////////////// + Claim logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Lets an account claim a given quantity of ERC1155 tokens. + * + * @param _receiver The receiver of the tokens to claim. + * @param _quantity The quantity of tokens to claim. + * @param _tokenId Token Id to claim. + * @param _proofs The proof of the claimer's inclusion in the merkle root allowlist + * of the claim conditions that apply. + * @param _proofMaxQuantityForWallet The maximum number of tokens an address included in an + * allowlist can claim. + */ + function claim( + address _receiver, + uint256 _quantity, + uint256 _tokenId, + bytes32[] calldata _proofs, + uint256 _proofMaxQuantityForWallet + ) external nonReentrant { + address claimer = _msgSender(); + + verifyClaim(claimer, _quantity, _tokenId, _proofs, _proofMaxQuantityForWallet); + + _transferClaimedTokens(_receiver, _quantity, _tokenId); + + emit TokensClaimed(_msgSender(), _receiver, _tokenId, _quantity); + } + + /// @dev Transfers the tokens being claimed. + function _transferClaimedTokens(address _to, uint256 _quantityBeingClaimed, uint256 _tokenId) internal { + // if transfer claimed tokens is called when `to != msg.sender`, it'd use msg.sender's limits. + // behavior would be similar to `msg.sender` mint for itself, then transfer to `_to`. + supplyClaimedByWallet[_tokenId][_msgSender()] += _quantityBeingClaimed; + availableAmount[_tokenId] -= _quantityBeingClaimed; + + IERC1155(airdropTokenAddress).safeTransferFrom(tokenOwner, _to, _tokenId, _quantityBeingClaimed, ""); + } + + /// @dev Checks a request to claim tokens against the active claim condition's criteria. + function verifyClaim( + address _claimer, + uint256 _quantity, + uint256 _tokenId, + bytes32[] calldata _proofs, + uint256 _proofMaxQuantityForWallet + ) public view { + bool isOverride; + + /* + * Here `isOverride` implies that if the merkle proof verification fails, + * the claimer would claim through open claim limit instead of allowlisted limit. + */ + bytes32 mroot = merkleRoot[_tokenId]; + if (mroot != bytes32(0)) { + (isOverride, ) = MerkleProof.verify( + _proofs, + mroot, + keccak256(abi.encodePacked(_claimer, _proofMaxQuantityForWallet)) + ); + } + + uint256 supplyClaimedAlready = supplyClaimedByWallet[_tokenId][_claimer]; + + require(_quantity > 0, "Claiming zero tokens"); + require(_quantity <= availableAmount[_tokenId], "exceeds available tokens."); + + uint256 expTimestamp = expirationTimestamp; + require(expTimestamp == 0 || block.timestamp < expTimestamp, "airdrop expired."); + + uint256 claimLimitForWallet = isOverride ? _proofMaxQuantityForWallet : openClaimLimitPerWallet[_tokenId]; + require(_quantity + supplyClaimedAlready <= claimLimitForWallet, "invalid quantity."); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function _msgSender() + internal + view + virtual + override(ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } +} diff --git a/contracts/prebuilts/unaudited/airdrop/AirdropERC20.sol b/contracts/prebuilts/unaudited/airdrop/AirdropERC20.sol new file mode 100644 index 000000000..b5829896f --- /dev/null +++ b/contracts/prebuilts/unaudited/airdrop/AirdropERC20.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// ========== External imports ========== + +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "../../../extension/Multicall.sol"; + +// ========== Internal imports ========== + +import "../../interface/airdrop/IAirdropERC20.sol"; +import { CurrencyTransferLib } from "../../../lib/CurrencyTransferLib.sol"; +import "../../../eip/interface/IERC20.sol"; +import "../../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; + +// ========== Features ========== +import "../../../extension/PermissionsEnumerable.sol"; +import "../../../extension/ContractMetadata.sol"; + +contract AirdropERC20 is + Initializable, + ContractMetadata, + PermissionsEnumerable, + ReentrancyGuardUpgradeable, + ERC2771ContextUpgradeable, + Multicall, + IAirdropERC20 +{ + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + bytes32 private constant MODULE_TYPE = bytes32("AirdropERC20"); + uint256 private constant VERSION = 2; + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + constructor() { + _disableInitializers(); + } + + /// @dev Initializes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _contractURI, + address[] memory _trustedForwarders + ) external initializer { + __ERC2771Context_init_unchained(_trustedForwarders); + + _setupContractURI(_contractURI); + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + __ReentrancyGuard_init(); + } + + /*/////////////////////////////////////////////////////////////// + Generic contract logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the type of the contract. + function contractType() external pure returns (bytes32) { + return MODULE_TYPE; + } + + /// @dev Returns the version of the contract. + function contractVersion() external pure returns (uint8) { + return uint8(VERSION); + } + + /*/////////////////////////////////////////////////////////////// + Airdrop logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Lets contract-owner send ERC20 tokens to a list of addresses. + * @dev The token-owner should approve target tokens to Airdrop contract, + * which acts as operator for the tokens. + * + * @param _tokenAddress The contract address of the tokens to transfer. + * @param _tokenOwner The owner of the tokens to transfer. + * @param _contents List containing recipient, tokenId and amounts to airdrop. + */ + function airdropERC20( + address _tokenAddress, + address _tokenOwner, + AirdropContent[] calldata _contents + ) external payable nonReentrant { + require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Not authorized."); + + uint256 len = _contents.length; + uint256 nativeTokenAmount; + uint256 refundAmount; + + for (uint256 i = 0; i < len; ) { + bool success = _transferCurrencyWithReturnVal( + _tokenAddress, + _tokenOwner, + _contents[i].recipient, + _contents[i].amount + ); + + if (!success) { + emit AirdropFailed(_tokenAddress, _tokenOwner, _contents[i].recipient, _contents[i].amount); + } + + if (_tokenAddress == CurrencyTransferLib.NATIVE_TOKEN) { + nativeTokenAmount += _contents[i].amount; + + require(nativeTokenAmount <= msg.value, "Insufficient native token amount"); + + if (!success) { + refundAmount += _contents[i].amount; + } + } + + unchecked { + i += 1; + } + } + + require(nativeTokenAmount == msg.value, "Incorrect native token amount"); + + if (refundAmount > 0) { + // refund failed payments' amount to contract admin address + CurrencyTransferLib.safeTransferNativeToken(msg.sender, refundAmount); + } + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + /// @dev Transfers ERC20 tokens and returns a boolean i.e. the status of the transfer. + function _transferCurrencyWithReturnVal( + address _currency, + address _from, + address _to, + uint256 _amount + ) internal returns (bool success) { + if (_amount == 0) { + success = true; + return success; + } + + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { + // solhint-disable avoid-low-level-calls + // slither-disable-next-line low-level-calls + (success, ) = _to.call{ value: _amount }(""); + } else { + (bool success_, bytes memory data_) = _currency.call( + abi.encodeWithSelector(IERC20.transferFrom.selector, _from, _to, _amount) + ); + + success = success_; + if (!success || (data_.length > 0 && !abi.decode(data_, (bool)))) { + success = false; + + require( + IERC20(_currency).balanceOf(_from) >= _amount && + IERC20(_currency).allowance(_from, address(this)) >= _amount, + "Not balance or allowance" + ); + } + } + } + + /// @dev Checks whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev See ERC2771 + function _msgSender() + internal + view + virtual + override(ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } +} diff --git a/contracts/prebuilts/unaudited/airdrop/AirdropERC20Claimable.sol b/contracts/prebuilts/unaudited/airdrop/AirdropERC20Claimable.sol new file mode 100644 index 000000000..22840d247 --- /dev/null +++ b/contracts/prebuilts/unaudited/airdrop/AirdropERC20Claimable.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// ========== External imports ========== +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "../../../extension/Multicall.sol"; + +// ========== Internal imports ========== + +import "../../interface/airdrop/IAirdropERC20Claimable.sol"; + +// ========== Features ========== + +import "../../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; +import "../../../lib/MerkleProof.sol"; + +contract AirdropERC20Claimable is + Initializable, + ReentrancyGuardUpgradeable, + ERC2771ContextUpgradeable, + Multicall, + IAirdropERC20Claimable +{ + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + /// @dev address of token being airdropped. + address public airdropTokenAddress; + + /// @dev address of owner of tokens being airdropped. + address public tokenOwner; + + /// @dev number tokens available to claim. + uint256 public availableAmount; + + /// @dev airdrop expiration timestamp. + uint256 public expirationTimestamp; + + /// @dev claim limit for open/public claiming without allowlist. + uint256 public openClaimLimitPerWallet; + + /// @dev merkle root of the allowlist of addresses eligible to claim. + bytes32 public merkleRoot; + + /*/////////////////////////////////////////////////////////////// + Mappings + //////////////////////////////////////////////////////////////*/ + + /// @dev Mapping from address => total number of tokens a wallet has claimed. + mapping(address => uint256) public supplyClaimedByWallet; + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + constructor() { + _disableInitializers(); + } + + /// @dev Initializes the contract, like a constructor. + function initialize( + address[] memory _trustedForwarders, + address _tokenOwner, + address _airdropTokenAddress, + uint256 _airdropAmount, + uint256 _expirationTimestamp, + uint256 _openClaimLimitPerWallet, + bytes32 _merkleRoot + ) external initializer { + __ReentrancyGuard_init(); + __ERC2771Context_init(_trustedForwarders); + + tokenOwner = _tokenOwner; + airdropTokenAddress = _airdropTokenAddress; + availableAmount = _airdropAmount; + expirationTimestamp = _expirationTimestamp; + openClaimLimitPerWallet = _openClaimLimitPerWallet; + merkleRoot = _merkleRoot; + } + + /*/////////////////////////////////////////////////////////////// + Claim logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Lets an account claim a given quantity of NFTs. + * + * @param _receiver The receiver of the NFTs to claim. + * @param _quantity The quantity of NFTs to claim. + * @param _proofs The proof of the claimer's inclusion in the merkle root allowlist + * of the claim conditions that apply. + * @param _proofMaxQuantityForWallet The maximum number of NFTs an address included in an + * allowlist can claim. + */ + function claim( + address _receiver, + uint256 _quantity, + bytes32[] calldata _proofs, + uint256 _proofMaxQuantityForWallet + ) external nonReentrant { + address claimer = _msgSender(); + + verifyClaim(claimer, _quantity, _proofs, _proofMaxQuantityForWallet); + + _transferClaimedTokens(_receiver, _quantity); + + emit TokensClaimed(_msgSender(), _receiver, _quantity); + } + + /// @dev Checks a request to claim tokens against the active claim condition's criteria. + function verifyClaim( + address _claimer, + uint256 _quantity, + bytes32[] calldata _proofs, + uint256 _proofMaxQuantityForWallet + ) public view { + bool isOverride; + + /* + * Here `isOverride` implies that if the merkle proof verification fails, + * the claimer would claim through open claim limit instead of allowlisted limit. + */ + if (merkleRoot != bytes32(0)) { + (isOverride, ) = MerkleProof.verify( + _proofs, + merkleRoot, + keccak256(abi.encodePacked(_claimer, _proofMaxQuantityForWallet)) + ); + } + + uint256 supplyClaimedAlready = supplyClaimedByWallet[_claimer]; + + require(_quantity > 0, "Claiming zero tokens"); + require(_quantity <= availableAmount, "exceeds available tokens."); + + uint256 expTimestamp = expirationTimestamp; + require(expTimestamp == 0 || block.timestamp < expTimestamp, "airdrop expired."); + + uint256 claimLimitForWallet = isOverride ? _proofMaxQuantityForWallet : openClaimLimitPerWallet; + require(_quantity + supplyClaimedAlready <= claimLimitForWallet, "invalid quantity."); + } + + /// @dev Transfers the tokens being claimed. + function _transferClaimedTokens(address _to, uint256 _quantityBeingClaimed) internal { + // if transfer claimed tokens is called when `to != msg.sender`, it'd use msg.sender's limits. + // behavior would be similar to `msg.sender` mint for itself, then transfer to `_to`. + supplyClaimedByWallet[_msgSender()] += _quantityBeingClaimed; + availableAmount -= _quantityBeingClaimed; + + require(IERC20(airdropTokenAddress).transferFrom(tokenOwner, _to, _quantityBeingClaimed), "transfer failed"); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function _msgSender() + internal + view + virtual + override(ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } +} diff --git a/contracts/prebuilts/unaudited/airdrop/AirdropERC721.sol b/contracts/prebuilts/unaudited/airdrop/AirdropERC721.sol new file mode 100644 index 000000000..97a99f63f --- /dev/null +++ b/contracts/prebuilts/unaudited/airdrop/AirdropERC721.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// ========== External imports ========== +import "../../../eip/interface/IERC721.sol"; + +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "../../../extension/Multicall.sol"; + +// ========== Internal imports ========== + +import "../../interface/airdrop/IAirdropERC721.sol"; +import "../../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; + +// ========== Features ========== +import "../../../extension/PermissionsEnumerable.sol"; +import "../../../extension/ContractMetadata.sol"; + +contract AirdropERC721 is + Initializable, + ContractMetadata, + PermissionsEnumerable, + ReentrancyGuardUpgradeable, + ERC2771ContextUpgradeable, + Multicall, + IAirdropERC721 +{ + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + bytes32 private constant MODULE_TYPE = bytes32("AirdropERC721"); + uint256 private constant VERSION = 2; + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + constructor() { + _disableInitializers(); + } + + /// @dev Initializes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _contractURI, + address[] memory _trustedForwarders + ) external initializer { + __ERC2771Context_init_unchained(_trustedForwarders); + + _setupContractURI(_contractURI); + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + __ReentrancyGuard_init(); + } + + /*/////////////////////////////////////////////////////////////// + Generic contract logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the type of the contract. + function contractType() external pure returns (bytes32) { + return MODULE_TYPE; + } + + /// @dev Returns the version of the contract. + function contractVersion() external pure returns (uint8) { + return uint8(VERSION); + } + + /*/////////////////////////////////////////////////////////////// + Airdrop logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Lets contract-owner send ERC721 tokens to a list of addresses. + * @dev The token-owner should approve target tokens to Airdrop contract, + * which acts as operator for the tokens. + * + * @param _tokenAddress The contract address of the tokens to transfer. + * @param _tokenOwner The owner of the tokens to transfer. + * @param _contents List containing recipient, tokenId to airdrop. + */ + function airdropERC721( + address _tokenAddress, + address _tokenOwner, + AirdropContent[] calldata _contents + ) external nonReentrant { + require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Not authorized."); + + uint256 len = _contents.length; + + for (uint256 i = 0; i < len; ) { + try + IERC721(_tokenAddress).safeTransferFrom(_tokenOwner, _contents[i].recipient, _contents[i].tokenId) + {} catch { + // revert if failure is due to unapproved tokens + require( + (IERC721(_tokenAddress).ownerOf(_contents[i].tokenId) == _tokenOwner && + address(this) == IERC721(_tokenAddress).getApproved(_contents[i].tokenId)) || + IERC721(_tokenAddress).isApprovedForAll(_tokenOwner, address(this)), + "Not owner or approved" + ); + + emit AirdropFailed(_tokenAddress, _tokenOwner, _contents[i].recipient, _contents[i].tokenId); + } + + unchecked { + i += 1; + } + } + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + /// @dev Checks whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev See ERC2771 + function _msgSender() + internal + view + virtual + override(ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } +} diff --git a/contracts/prebuilts/unaudited/airdrop/AirdropERC721Claimable.sol b/contracts/prebuilts/unaudited/airdrop/AirdropERC721Claimable.sol new file mode 100644 index 000000000..16f38d3b7 --- /dev/null +++ b/contracts/prebuilts/unaudited/airdrop/AirdropERC721Claimable.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// ========== External imports ========== +import "../../../eip/interface/IERC721.sol"; + +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "../../../extension/Multicall.sol"; + +// ========== Internal imports ========== + +import "../../interface/airdrop/IAirdropERC721Claimable.sol"; + +// ========== Features ========== + +import "../../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; +import "../../../lib/MerkleProof.sol"; + +contract AirdropERC721Claimable is + Initializable, + ReentrancyGuardUpgradeable, + ERC2771ContextUpgradeable, + Multicall, + IAirdropERC721Claimable +{ + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + /// @dev address of token being airdropped. + address public airdropTokenAddress; + + /// @dev address of owner of tokens being airdropped. + address public tokenOwner; + + /// @dev list of tokens to airdrop. + uint256[] public tokenIds; + + /// @dev next index in tokenIds[] to claim in the airdrop. + uint256 public nextIndex; + + /// @dev number tokens available to claim in tokenIds[]. + uint256 public availableAmount; + + /// @dev airdrop expiration timestamp. + uint256 public expirationTimestamp; + + /// @dev claim limit for open/public claiming without allowlist. + uint256 public openClaimLimitPerWallet; + + /// @dev merkle root of the allowlist of addresses eligible to claim. + bytes32 public merkleRoot; + + /*/////////////////////////////////////////////////////////////// + Mappings + //////////////////////////////////////////////////////////////*/ + + /// @dev Mapping from address => total number of tokens a wallet has claimed. + mapping(address => uint256) public supplyClaimedByWallet; + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + constructor() { + _disableInitializers(); + } + + /// @dev Initializes the contract, like a constructor. + function initialize( + address[] memory _trustedForwarders, + address _tokenOwner, + address _airdropTokenAddress, + uint256[] memory _tokenIds, + uint256 _expirationTimestamp, + uint256 _openClaimLimitPerWallet, + bytes32 _merkleRoot + ) external initializer { + __ReentrancyGuard_init(); + __ERC2771Context_init(_trustedForwarders); + + tokenOwner = _tokenOwner; + airdropTokenAddress = _airdropTokenAddress; + tokenIds = _tokenIds; + expirationTimestamp = _expirationTimestamp; + openClaimLimitPerWallet = _openClaimLimitPerWallet; + merkleRoot = _merkleRoot; + + availableAmount = _tokenIds.length; + } + + /*/////////////////////////////////////////////////////////////// + Claim logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Lets an account claim a given quantity of NFTs. + * + * @param _receiver The receiver of the NFTs to claim. + * @param _quantity The quantity of NFTs to claim. + * @param _proofs The proof of the claimer's inclusion in the merkle root allowlist + * of the claim conditions that apply. + * @param _proofMaxQuantityForWallet The maximum number of NFTs an address included in an + * allowlist can claim. + */ + function claim( + address _receiver, + uint256 _quantity, + bytes32[] calldata _proofs, + uint256 _proofMaxQuantityForWallet + ) external nonReentrant { + address claimer = _msgSender(); + + verifyClaim(claimer, _quantity, _proofs, _proofMaxQuantityForWallet); + + _transferClaimedTokens(_receiver, _quantity); + + emit TokensClaimed(_msgSender(), _receiver, _quantity); + } + + /// @dev Checks a request to claim tokens against the active claim condition's criteria. + function verifyClaim( + address _claimer, + uint256 _quantity, + bytes32[] calldata _proofs, + uint256 _proofMaxQuantityForWallet + ) public view { + bool isOverride; + + /* + * Here `isOverride` implies that if the merkle proof verification fails, + * the claimer would claim through open claim limit instead of allowlisted limit. + */ + if (merkleRoot != bytes32(0)) { + (isOverride, ) = MerkleProof.verify( + _proofs, + merkleRoot, + keccak256(abi.encodePacked(_claimer, _proofMaxQuantityForWallet)) + ); + } + + uint256 supplyClaimedAlready = supplyClaimedByWallet[_claimer]; + + require(_quantity > 0, "Claiming zero tokens"); + require(_quantity <= availableAmount, "exceeds available tokens."); + + uint256 expTimestamp = expirationTimestamp; + require(expTimestamp == 0 || block.timestamp < expTimestamp, "airdrop expired."); + + uint256 claimLimitForWallet = isOverride ? _proofMaxQuantityForWallet : openClaimLimitPerWallet; + require(_quantity + supplyClaimedAlready <= claimLimitForWallet, "invalid quantity."); + } + + /// @dev Transfers the tokens being claimed. + function _transferClaimedTokens(address _to, uint256 _quantityBeingClaimed) internal { + // if transfer claimed tokens is called when `to != msg.sender`, it'd use msg.sender's limits. + // behavior would be similar to `msg.sender` mint for itself, then transfer to `_to`. + supplyClaimedByWallet[_msgSender()] += _quantityBeingClaimed; + availableAmount -= _quantityBeingClaimed; + + uint256 index = nextIndex; + uint256[] memory _tokenIds = tokenIds; + address _tokenAddress = airdropTokenAddress; + address _tokenOwner = tokenOwner; + + for (uint256 i = 0; i < _quantityBeingClaimed; i += 1) { + IERC721(_tokenAddress).safeTransferFrom(_tokenOwner, _to, _tokenIds[index]); + index += 1; + } + nextIndex = index; + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function _msgSender() + internal + view + virtual + override(ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } +} diff --git a/contracts/prebuilts/unaudited/burn-to-claim-drop/BurnToClaimDropERC721.sol b/contracts/prebuilts/unaudited/burn-to-claim-drop/BurnToClaimDropERC721.sol new file mode 100644 index 000000000..9346c1ff3 --- /dev/null +++ b/contracts/prebuilts/unaudited/burn-to-claim-drop/BurnToClaimDropERC721.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +import "@thirdweb-dev/dynamic-contracts/src/presets/BaseRouter.sol"; + +import "../../../extension/Multicall.sol"; + +import "../../../extension/upgradeable/Initializable.sol"; +import "../../../extension/upgradeable/Permissions.sol"; +import "../../../extension/upgradeable/ERC2771ContextUpgradeable.sol"; + +import "../../../extension/upgradeable/init/ContractMetadataInit.sol"; +import "../../../extension/upgradeable/init/PlatformFeeInit.sol"; +import "../../../extension/upgradeable/init/RoyaltyInit.sol"; +import "../../../extension/upgradeable/init/PrimarySaleInit.sol"; +import "../../../extension/upgradeable/init/OwnableInit.sol"; +import "../../../extension/upgradeable/init/ERC721AInit.sol"; +import "../../../extension/upgradeable/init/PermissionsEnumerableInit.sol"; +import "../../../extension/upgradeable/init/ReentrancyGuardInit.sol"; + +contract BurnToClaimDropERC721 is + Initializable, + Multicall, + ERC2771ContextUpgradeable, + BaseRouter, + ContractMetadataInit, + PlatformFeeInit, + RoyaltyInit, + PrimarySaleInit, + OwnableInit, + PermissionsEnumerableInit, + ERC721AInit +{ + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + constructor(Extension[] memory _extensions) BaseRouter(_extensions) { + _disableInitializers(); + } + + /// @notice Initializes the contract. + function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _royaltyRecipient, + uint128 _royaltyBps, + uint128 _platformFeeBps, + address _platformFeeRecipient + ) external initializer { + // Initialize extensions + __BaseRouter_init(); + + // Initialize inherited contracts, most base-like -> most derived. + __ERC2771Context_init(_trustedForwarders); + __ERC721A_init(_name, _symbol); + + _setupContractURI(_contractURI); + _setupOwner(_defaultAdmin); + + _setupRoles(_defaultAdmin); + + _setupPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + _setupPrimarySaleRecipient(_saleRecipient); + } + + /// @dev Called in the initialize function. Sets up roles. + function _setupRoles(address _defaultAdmin) internal onlyInitializing { + bytes32 _transferRole = keccak256("TRANSFER_ROLE"); + bytes32 _minterRole = keccak256("MINTER_ROLE"); + bytes32 _extensionRole = keccak256("EXTENSION_ROLE"); + bytes32 _defaultAdminRole = 0x00; + + _setupRole(_defaultAdminRole, _defaultAdmin); + _setupRole(_minterRole, _defaultAdmin); + _setupRole(_transferRole, _defaultAdmin); + _setupRole(_transferRole, address(0)); + _setupRole(_extensionRole, _defaultAdmin); + _setRoleAdmin(_extensionRole, _extensionRole); + } + + /*/////////////////////////////////////////////////////////////// + Contract identifiers + //////////////////////////////////////////////////////////////*/ + + /// @notice Returns the type of contract. + function contractType() external pure returns (bytes32) { + return bytes32("BurnToClaimDropERC721"); + } + + /// @notice Returns the contract version. + function contractVersion() external pure returns (uint8) { + return uint8(5); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns whether all relevant permission and other checks are met before any upgrade. + function _isAuthorizedCallToUpgrade() internal view virtual override returns (bool) { + return _hasRole(keccak256("EXTENSION_ROLE"), msg.sender); + } + + /// @dev Checks whether an account holds the given role. + function _hasRole(bytes32 role, address addr) internal view returns (bool) { + PermissionsStorage.Data storage data = PermissionsStorage.data(); + return data._hasRole[role][addr]; + } + + /// @notice Returns the sender in the given execution context. + function _msgSender() + internal + view + virtual + override(ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } +} diff --git a/contracts/prebuilts/unaudited/burn-to-claim-drop/extension/BurnToClaimDrop721Logic.sol b/contracts/prebuilts/unaudited/burn-to-claim-drop/extension/BurnToClaimDrop721Logic.sol new file mode 100644 index 000000000..80d1411f6 --- /dev/null +++ b/contracts/prebuilts/unaudited/burn-to-claim-drop/extension/BurnToClaimDrop721Logic.sol @@ -0,0 +1,362 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +import { BurnToClaimDrop721Storage } from "./BurnToClaimDrop721Storage.sol"; + +import "../../../../lib/Strings.sol"; +import "../../../../lib/CurrencyTransferLib.sol"; + +import { IERC2981 } from "../../../../eip/interface/IERC2981.sol"; +import { Context, ERC721AUpgradeable, ERC721AStorage } from "../../../../eip/ERC721AUpgradeable.sol"; + +import { IERC2771Context } from "../../../../extension/interface/IERC2771Context.sol"; + +import { ERC2771ContextUpgradeable } from "../../../../extension/upgradeable/ERC2771ContextUpgradeable.sol"; +import { DelayedReveal } from "../../../../extension/upgradeable/DelayedReveal.sol"; +import { PrimarySale } from "../../../../extension/upgradeable/PrimarySale.sol"; +import { PlatformFee } from "../../../../extension/upgradeable/PlatformFee.sol"; +import { Royalty, IERC165 } from "../../../../extension/upgradeable/Royalty.sol"; +import { LazyMint } from "../../../../extension/upgradeable/LazyMint.sol"; +import { Drop } from "../../../../extension/upgradeable/Drop.sol"; +import { ContractMetadata } from "../../../../extension/upgradeable/ContractMetadata.sol"; +import { Ownable } from "../../../../extension/upgradeable/Ownable.sol"; +import { PermissionsStorage } from "../../../../extension/upgradeable/Permissions.sol"; +import { BurnToClaim, BurnToClaimStorage } from "../../../../extension/upgradeable/BurnToClaim.sol"; +import { ReentrancyGuard } from "../../../../extension/upgradeable/ReentrancyGuard.sol"; + +contract BurnToClaimDrop721Logic is + ContractMetadata, + PlatformFee, + Royalty, + PrimarySale, + Ownable, + BurnToClaim, + DelayedReveal, + LazyMint, + Drop, + ERC2771ContextUpgradeable, + ERC721AUpgradeable, + ReentrancyGuard +{ + using Strings for uint256; + + /*/////////////////////////////////////////////////////////////// + Constants + //////////////////////////////////////////////////////////////*/ + + /// @dev Default admin role for all roles. Only accounts with this role can grant/revoke other roles. + bytes32 private constant DEFAULT_ADMIN_ROLE = 0x00; + /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. + bytes32 private constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE"); + /// @dev Only MINTER_ROLE holders can sign off on `MintRequest`s and lazy mint tokens. + bytes32 private constant MINTER_ROLE = keccak256("MINTER_ROLE"); + + /// @dev Max bps in the thirdweb system. + uint256 private constant MAX_BPS = 10_000; + + /*/////////////////////////////////////////////////////////////// + Events + //////////////////////////////////////////////////////////////*/ + + /// @dev Emitted when the global max NFTs that can be minted is updated. + event MaxTotalMintedUpdated(uint256 maxTotalMinted); + + /*/////////////////////////////////////////////////////////////// + ERC 165 / 721 / 2981 logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the URI for a given tokenId. + * @dev The URI, for a given tokenId, is returned once it is lazy minted, even if it might not be actually minted. (See `LazyMint`) + */ + function tokenURI(uint256 _tokenId) public view override returns (string memory) { + (uint256 batchId, ) = _getBatchId(_tokenId); + string memory batchUri = _getBaseURI(_tokenId); + + if (isEncryptedBatch(batchId)) { + return string(abi.encodePacked(batchUri, "0")); + } else { + return string(abi.encodePacked(batchUri, _tokenId.toString())); + } + } + + /// @notice See ERC 165 + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC721AUpgradeable, IERC165) returns (bool) { + return super.supportsInterface(interfaceId) || type(IERC2981).interfaceId == interfaceId; + } + + /*/////////////////////////////////////////////////////////////// + Lazy minting + delayed-reveal logic + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Lets an account with `MINTER_ROLE` lazy mint 'n' NFTs. + * The URIs for each token is the provided `_baseURIForTokens` + `{tokenId}`. + */ + function lazyMint( + uint256 _amount, + string calldata _baseURIForTokens, + bytes calldata _data + ) public override returns (uint256) { + uint256 nextId = nextTokenIdToLazyMint(); + if (_data.length > 0) { + (bytes memory encryptedURI, bytes32 provenanceHash) = abi.decode(_data, (bytes, bytes32)); + if (encryptedURI.length != 0 && provenanceHash != "") { + _setEncryptedData(nextId + _amount, _data); + } + } + + return super.lazyMint(_amount, _baseURIForTokens, _data); + } + + /// @notice Lets an account with `MINTER_ROLE` reveal the URI for a batch of 'delayed-reveal' NFTs. + function reveal(uint256 _index, bytes calldata _key) external returns (string memory revealedURI) { + require(_hasRole(MINTER_ROLE, _msgSender()), "not minter."); + uint256 batchId = getBatchIdAtIndex(_index); + revealedURI = getRevealURI(batchId, _key); + + _setEncryptedData(batchId, ""); + _setBaseURI(batchId, revealedURI); + + emit TokenURIRevealed(_index, revealedURI); + } + + /*/////////////////////////////////////////////////////////////// + Claiming lazy minted tokens logic + //////////////////////////////////////////////////////////////*/ + + /// @notice Claim lazy minted tokens after burning required tokens from origin contract. + function burnAndClaim(uint256 _burnTokenId, uint256 _quantity) external payable nonReentrant { + _checkTokenSupply(_quantity); + + // Verify and burn tokens on origin contract + address _tokenOwner = _dropMsgSender(); + verifyBurnToClaim(_tokenOwner, _burnTokenId, _quantity); + _burnTokensOnOrigin(_tokenOwner, _burnTokenId, _quantity); + + // Collect price + _collectPriceOnClaim( + address(0), + _quantity, + _burnToClaimStorage().burnToClaimInfo.currency, + _burnToClaimStorage().burnToClaimInfo.mintPriceForNewToken + ); + + // Mint tokens. + _safeMint(_tokenOwner, _quantity); + + // emit event + emit TokensBurnedAndClaimed( + _burnToClaimStorage().burnToClaimInfo.originContractAddress, + _tokenOwner, + _burnTokenId, + _quantity + ); + } + + /*/////////////////////////////////////////////////////////////// + Setter functions + //////////////////////////////////////////////////////////////*/ + + /// @notice Lets a contract admin set the global maximum NFTs that can be minted. + function setMaxTotalMinted(uint256 _maxTotalMinted) external { + require(_hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "not admin."); + + BurnToClaimDrop721Storage.Data storage data = BurnToClaimDrop721Storage.burnToClaimDrop721Storage(); + data.maxTotalMinted = _maxTotalMinted; + emit MaxTotalMintedUpdated(_maxTotalMinted); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Check if given quantity is available for minting. + function _checkTokenSupply(uint256 _quantity) internal view { + uint256 _maxTotalMinted = maxTotalMinted(); + uint256 currentTotalMinted = totalMinted(); + + require(currentTotalMinted + _quantity <= nextTokenIdToLazyMint(), "!Tokens"); + require( + _maxTotalMinted == 0 || currentTotalMinted + _quantity <= _maxTotalMinted, + "exceed max total mint cap." + ); + } + + /// @dev Runs before every `claim` function call. + function _beforeClaim( + address, + uint256 _quantity, + address, + uint256, + AllowlistProof calldata, + bytes memory + ) internal view override { + _checkTokenSupply(_quantity); + } + + /// @dev Collects and distributes the primary sale value of NFTs being claimed. + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override { + if (_pricePerToken == 0) { + require(msg.value == 0, "!Value"); + return; + } + + (address platformFeeRecipient, uint16 platformFeeBps) = getPlatformFeeInfo(); + + address saleRecipient = _primarySaleRecipient == address(0) ? primarySaleRecipient() : _primarySaleRecipient; + + uint256 totalPrice = _quantityToClaim * _pricePerToken; + uint256 platformFees = (totalPrice * platformFeeBps) / MAX_BPS; + + bool validMsgValue; + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { + validMsgValue = msg.value == totalPrice; + } else { + validMsgValue = msg.value == 0; + } + require(validMsgValue, "Invalid msg value"); + + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), platformFeeRecipient, platformFees); + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), saleRecipient, totalPrice - platformFees); + } + + /// @dev Transfers the NFTs being claimed. + function _transferTokensOnClaim( + address _to, + uint256 _quantityBeingClaimed + ) internal override returns (uint256 startTokenId) { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + startTokenId = data._currentIndex; + _safeMint(_to, _quantityBeingClaimed); + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function _canSetPlatformFeeInfo() internal view override returns (bool) { + return _hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether primary sale recipient can be set in the given execution context. + function _canSetPrimarySaleRecipient() internal view override returns (bool) { + return _hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether owner can be set in the given execution context. + function _canSetOwner() internal view override returns (bool) { + return _hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether royalty info can be set in the given execution context. + function _canSetRoyaltyInfo() internal view override returns (bool) { + return _hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view override returns (bool) { + return _hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function _canSetClaimConditions() internal view override returns (bool) { + return _hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Returns whether lazy minting can be done in the given execution context. + function _canLazyMint() internal view virtual override returns (bool) { + return _hasRole(MINTER_ROLE, _msgSender()); + } + + /// @dev Returns whether burn-to-claim info can be set in the given execution context. + function _canSetBurnToClaim() internal view virtual override returns (bool) { + return _hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the total amount of tokens minted in the contract. + */ + function totalMinted() public view returns (uint256) { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + unchecked { + return data._currentIndex - _startTokenId(); + } + } + + /// @notice The tokenId of the next NFT that will be minted / lazy minted. + function nextTokenIdToMint() external view returns (uint256) { + return nextTokenIdToLazyMint(); + } + + /// @notice The next token ID of the NFT that can be claimed. + function nextTokenIdToClaim() external view returns (uint256) { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + return data._currentIndex; + } + + /// @notice Global max total NFTs that can be minted. + function maxTotalMinted() public view returns (uint256) { + BurnToClaimDrop721Storage.Data storage data = BurnToClaimDrop721Storage.burnToClaimDrop721Storage(); + return data.maxTotalMinted; + } + + /// @notice Burns `tokenId`. See {ERC721-_burn}. + function burn(uint256 tokenId) external virtual { + // note: ERC721AUpgradeable's `_burn(uint256,bool)` internally checks for token approvals. + _burn(tokenId, true); + } + + /// @dev See {ERC721-_beforeTokenTransfer}. + function _beforeTokenTransfers( + address from, + address to, + uint256 startTokenId, + uint256 quantity + ) internal virtual override { + super._beforeTokenTransfers(from, to, startTokenId, quantity); + + // if transfer is restricted on the contract, we still want to allow burning and minting + if (!_hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { + if (!_hasRole(TRANSFER_ROLE, from) && !_hasRole(TRANSFER_ROLE, to)) { + revert("!Transfer-Role"); + } + } + } + + function _hasRole(bytes32 role, address addr) internal view returns (bool) { + PermissionsStorage.Data storage data = PermissionsStorage.data(); + return data._hasRole[role][addr]; + } + + function _dropMsgSender() internal view virtual override returns (address) { + return _msgSender(); + } + + function _msgSender() internal view virtual override(Context, ERC2771ContextUpgradeable) returns (address) { + return ERC2771ContextUpgradeable._msgSender(); + } + + function _msgData() internal view virtual override(Context, ERC2771ContextUpgradeable) returns (bytes calldata) { + return ERC2771ContextUpgradeable._msgData(); + } +} diff --git a/contracts/prebuilts/unaudited/burn-to-claim-drop/extension/BurnToClaimDrop721Storage.sol b/contracts/prebuilts/unaudited/burn-to-claim-drop/extension/BurnToClaimDrop721Storage.sol new file mode 100644 index 000000000..05d8a350e --- /dev/null +++ b/contracts/prebuilts/unaudited/burn-to-claim-drop/extension/BurnToClaimDrop721Storage.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +library BurnToClaimDrop721Storage { + /// @custom:storage-location erc7201:burn.to.claim.drop.721.storage + /// @dev keccak256(abi.encode(uint256(keccak256("burn.to.claim.drop.721.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant BURN_TO_CLAIM_DROP_721_STORAGE_POSITION = + 0x3107fcf7768de14f3c3441e6960e7a1659b448f798b4e6665bf2dc61db3ea300; + + struct Data { + /// @dev Global max total NFTs that can be minted. + uint256 maxTotalMinted; + } + + function burnToClaimDrop721Storage() internal pure returns (Data storage burnToClaimDrop721Data) { + bytes32 position = BURN_TO_CLAIM_DROP_721_STORAGE_POSITION; + assembly { + burnToClaimDrop721Data.slot := position + } + } +} diff --git a/contracts/prebuilts/unaudited/contract-builder/CoreRouter.sol b/contracts/prebuilts/unaudited/contract-builder/CoreRouter.sol new file mode 100644 index 000000000..d504362f2 --- /dev/null +++ b/contracts/prebuilts/unaudited/contract-builder/CoreRouter.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +// @author: thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) + +pragma solidity ^0.8.0; + +// Interface +import "lib/dynamic-contracts/src/presets/BaseRouter.sol"; + +// Core +import "lib/dynamic-contracts/src/core/Router.sol"; + +// Utils +import "lib/dynamic-contracts/src/lib/StringSet.sol"; +import "./extension/PermissionOverride.sol"; + +// Fixed extensions +import "../../../extension/Ownable.sol"; +import "../../../extension/ContractMetadata.sol"; + +/** + * //////////// + * + * NOTE: This contract is a work in progress, and has not been audited. + * + * //////////// + */ + +contract CoreRouter is BaseRouter, ContractMetadata, Ownable { + using StringSet for StringSet.Set; + + /*/////////////////////////////////////////////////////////////// + Constructor + //////////////////////////////////////////////////////////////*/ + + constructor(address _owner, Extension[] memory _extensions) BaseRouter(_extensions) { + // Initialize extensions + __BaseRouter_init(); + + _setupOwner(_owner); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns whether all relevant permission and other checks are met before any upgrade. + function _isAuthorizedCallToUpgrade() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + + /// @dev Returns whether owner can be set in the given execution context. + function _canSetOwner() internal view virtual override returns (bool) { + return msg.sender == owner(); + } +} diff --git a/contracts/prebuilts/unaudited/contract-builder/extension/PermissionOverride.sol b/contracts/prebuilts/unaudited/contract-builder/extension/PermissionOverride.sol new file mode 100644 index 000000000..ba5ba5dcf --- /dev/null +++ b/contracts/prebuilts/unaudited/contract-builder/extension/PermissionOverride.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +/** + * //////////// + * + * NOTE: This contract is a work in progress, and has not been audited. + * + * //////////// + */ + +library PermissionsStorage { + bytes32 public constant PERMISSIONS_STORAGE_POSITION = keccak256("permissions.storage"); + + struct Data { + /// @dev Map from keccak256 hash of a role => a map from address => whether address has role. + mapping(bytes32 => mapping(address => bool)) _hasRole; + /// @dev Map from keccak256 hash of a role to role admin. See {getRoleAdmin}. + mapping(bytes32 => bytes32) _getRoleAdmin; + } + + function permissionsStorage() internal pure returns (Data storage permissionsData) { + bytes32 position = PERMISSIONS_STORAGE_POSITION; + assembly { + permissionsData.slot := position + } + } +} + +contract PermissionOverrideCoreRouter { + bytes32 private constant DEFAULT_ADMIN_ROLE = 0x00; + bytes32 private constant EXTENSION_ROLE = keccak256("EXTENSION_ROLE"); + + function canSetContractURI(address _caller) public view returns (bool) { + return _hasRole(DEFAULT_ADMIN_ROLE, _caller); + } + + function canSetOwner(address _caller) public view returns (bool) { + return _hasRole(DEFAULT_ADMIN_ROLE, _caller); + } + + function canSetExtension(address _caller) public view returns (bool) { + return _hasRole(DEFAULT_ADMIN_ROLE, _caller); + } + + function _hasRole(bytes32 role, address account) internal view returns (bool) { + PermissionsStorage.Data storage data = PermissionsStorage.permissionsStorage(); + return data._hasRole[role][account]; + } +} diff --git a/contracts/prebuilts/unaudited/loyalty/LoyaltyPoints.sol b/contracts/prebuilts/unaudited/loyalty/LoyaltyPoints.sol new file mode 100644 index 000000000..81e32da1a --- /dev/null +++ b/contracts/prebuilts/unaudited/loyalty/LoyaltyPoints.sol @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// Interface +import "../../interface/ILoyaltyPoints.sol"; + +// Base +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; + +// Lib +import "../../../lib/CurrencyTransferLib.sol"; + +// Extensions +import "../../../extension/SignatureMintERC20Upgradeable.sol"; +import "../../../extension/ContractMetadata.sol"; +import "../../../extension/PrimarySale.sol"; +import "../../../extension/PlatformFee.sol"; +import "../../../extension/PermissionsEnumerable.sol"; +import "../../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; + +/** + * @title LoyaltyPoints + * + * @custom:description This contract is a loyalty points contract. Each token represents a loyalty point. Loyalty points can + * be cancelled (i.e. 'burned') by its owner or an approved operator. Loyalty points can be revoked + * (i.e. 'burned') without its owner's approval, by an admin of the contract. + */ + +contract LoyaltyPoints is + ILoyaltyPoints, + ContractMetadata, + PrimarySale, + PlatformFee, + PermissionsEnumerable, + ReentrancyGuardUpgradeable, + ERC2771ContextUpgradeable, + SignatureMintERC20Upgradeable, + ERC20Upgradeable +{ + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + /// @dev Only TRANSFER_ROLE holders can have tokens transferred from or to them, during restricted transfers. + bytes32 private constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE"); + /// @dev Only MINTER_ROLE holders can sign off on `MintRequest`s. + bytes32 private constant MINTER_ROLE = keccak256("MINTER_ROLE"); + /// @dev Only REVOKE_ROLE holders can revoke a loyalty card. + bytes32 private constant REVOKE_ROLE = keccak256("REVOKE_ROLE"); + + /// @dev Max bps in the thirdweb system. + uint256 private constant MAX_BPS = 10_000; + + /// @dev Mapping from token owner => total tokens minted to them in the contract's lifetime. + mapping(address => uint256) private _mintedToInLifetime; + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer + //////////////////////////////////////////////////////////////*/ + + constructor() initializer {} + + /// @dev Initializes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + uint128 _platformFeeBps, + address _platformFeeRecipient + ) external initializer { + // Initialize inherited contracts, most base-like -> most derived. + __ERC2771Context_init(_trustedForwarders); + __ERC20_init_unchained(_name, _symbol); + __SignatureMintERC20_init(_name); + __ReentrancyGuard_init(); + + _setupContractURI(_contractURI); + + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + _setupRole(MINTER_ROLE, _defaultAdmin); + _setupRole(TRANSFER_ROLE, _defaultAdmin); + + _setupRole(REVOKE_ROLE, _defaultAdmin); + _setRoleAdmin(REVOKE_ROLE, REVOKE_ROLE); + + _setupPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + _setupPrimarySaleRecipient(_saleRecipient); + } + + /*/////////////////////////////////////////////////////////////// + View functions + //////////////////////////////////////////////////////////////*/ + + /// @notice Returns the total tokens minted to `owner` in the contract's lifetime. + function getTotalMintedInLifetime(address _owner) external view returns (uint256) { + return _mintedToInLifetime[_owner]; + } + + /*/////////////////////////////////////////////////////////////// + External functions + //////////////////////////////////////////////////////////////*/ + + /// @notice Mints tokens to a recipient using a signature from an authorized party. + function mintWithSignature( + MintRequest calldata _req, + bytes calldata _signature + ) external payable nonReentrant returns (address signer) { + signer = _processRequest(_req, _signature); + address receiver = _req.to; + + _collectPriceOnClaim(_req.primarySaleRecipient, _req.currency, _req.price); + _mintTo(receiver, _req.quantity); + + emit TokensMintedWithSignature(signer, receiver, _req); + } + + /// @notice Mints `amount` of tokens to the recipient `to`. + function mintTo(address to, uint256 amount) public virtual { + require(hasRole(MINTER_ROLE, _msgSender()), "not minter."); + _mintTo(to, amount); + } + + /// @notice Burns `amount` of tokens. See {ERC20-_burn}. + function cancel(address _owner, uint256 _amount) external virtual { + address caller = _msgSender(); + if (caller != _owner) { + _spendAllowance(_owner, caller, _amount); + } + _burn(_owner, _amount); + } + + /// @notice Burns `amount` of tokens from `owner`'s balance (without requiring approval from owner). See {ERC20-_burn}. + function revoke(address _owner, uint256 _amount) external virtual onlyRole(REVOKE_ROLE) { + _burn(_owner, _amount); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Mints `amount` of tokens to `to` + function _mintTo(address _to, uint256 _amount) internal { + _mint(_to, _amount); + emit TokensMinted(_to, _amount); + } + + /// @dev Collects and distributes the primary sale value of tokens being minted. + function _collectPriceOnClaim(address _primarySaleRecipient, address _currency, uint256 _price) internal { + if (_price == 0) { + require(msg.value == 0, "!Value"); + return; + } + + bool validMsgValue; + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { + validMsgValue = msg.value == _price; + } else { + validMsgValue = msg.value == 0; + } + require(validMsgValue, "Invalid msg value"); + + address saleRecipient = _primarySaleRecipient == address(0) ? primarySaleRecipient() : _primarySaleRecipient; + + uint256 fees; + address feeRecipient; + + PlatformFeeType feeType = getPlatformFeeType(); + if (feeType == PlatformFeeType.Flat) { + (feeRecipient, fees) = getFlatPlatformFeeInfo(); + } else { + uint16 platformFeeBps; + (feeRecipient, platformFeeBps) = getPlatformFeeInfo(); + fees = (_price * platformFeeBps) / MAX_BPS; + } + + require(_price >= fees, "!F"); + + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), feeRecipient, fees); + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), saleRecipient, _price - fees); + } + + /// @dev Runs on every transfer. + function _beforeTokenTransfer(address from, address to, uint256 amount) internal override { + super._beforeTokenTransfer(from, to, amount); + + if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { + require(hasRole(TRANSFER_ROLE, from) || hasRole(TRANSFER_ROLE, to), "transfers restricted."); + } + + if (from == address(0)) { + _mintedToInLifetime[to] += amount; + } + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function _canSetPlatformFeeInfo() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether primary sale recipient can be set in the given execution context. + function _canSetPrimarySaleRecipient() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Returns whether a given address is authorized to sign mint requests. + function _isAuthorizedSigner(address _signer) internal view override returns (bool) { + return hasRole(MINTER_ROLE, _signer); + } + + function _msgSender() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } + + function _msgData() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable) + returns (bytes calldata) + { + return ERC2771ContextUpgradeable._msgData(); + } +} diff --git a/contracts/vote/VoteERC20.sol b/contracts/prebuilts/vote/VoteERC20.sol similarity index 81% rename from contracts/vote/VoteERC20.sol rename to contracts/prebuilts/vote/VoteERC20.sol index 1ac923b43..31360b91f 100644 --- a/contracts/vote/VoteERC20.sol +++ b/contracts/prebuilts/vote/VoteERC20.sol @@ -1,8 +1,19 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.11; +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + // Base -import "../interfaces/IThirdwebContract.sol"; +import "../../infra/interface/IThirdwebContract.sol"; // Governance import "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol"; @@ -13,22 +24,12 @@ import "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesU import "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesQuorumFractionUpgradeable.sol"; // Meta transactions -import "../openzeppelin-presets/metatx/ERC2771ContextUpgradeable.sol"; - -// Utils -import "@openzeppelin/contracts-upgradeable/token/ERC721/utils/ERC721HolderUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/token/ERC1155/utils/ERC1155HolderUpgradeable.sol"; - -// Helper interfaces -import "@openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155ReceiverUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721ReceiverUpgradeable.sol"; +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; contract VoteERC20 is Initializable, IThirdwebContract, ERC2771ContextUpgradeable, - ERC721HolderUpgradeable, - ERC1155HolderUpgradeable, GovernorUpgradeable, GovernorSettingsUpgradeable, GovernorCountingSimpleUpgradeable, @@ -59,7 +60,7 @@ contract VoteERC20 is // solhint-disable-next-line no-empty-blocks constructor() initializer {} - /// @dev Initiliazes the contract, like a constructor. + /// @dev Initializes the contract, like a constructor. function initialize( string memory _name, string memory _contractURI, @@ -140,16 +141,8 @@ contract VoteERC20 is return GovernorSettingsUpgradeable.proposalThreshold(); } - function supportsInterface(bytes4 interfaceId) - public - view - override(ERC1155ReceiverUpgradeable, GovernorUpgradeable) - returns (bool) - { - return - interfaceId == type(IERC1155ReceiverUpgradeable).interfaceId || - interfaceId == type(IERC721ReceiverUpgradeable).interfaceId || - super.supportsInterface(interfaceId); + function supportsInterface(bytes4 interfaceId) public view override returns (bool) { + return interfaceId == type(IERC721ReceiverUpgradeable).interfaceId || super.supportsInterface(interfaceId); } function _msgSender() diff --git a/contracts/signature-drop/SignatureDrop.sol b/contracts/signature-drop/SignatureDrop.sol deleted file mode 100644 index 9b9b4296b..000000000 --- a/contracts/signature-drop/SignatureDrop.sol +++ /dev/null @@ -1,370 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -// ========== External imports ========== - -import "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; - -import "erc721a-upgradeable/contracts/ERC721AUpgradeable.sol"; - -// ========== Internal imports ========== - -import "../openzeppelin-presets/metatx/ERC2771ContextUpgradeable.sol"; -import "../lib/CurrencyTransferLib.sol"; - -// ========== Features ========== - -import "../extension/ContractMetadata.sol"; -import "../extension/PlatformFee.sol"; -import "../extension/Royalty.sol"; -import "../extension/PrimarySale.sol"; -import "../extension/Ownable.sol"; -import "../extension/DelayedReveal.sol"; -import "../extension/LazyMint.sol"; -import "../extension/PermissionsEnumerable.sol"; -import "../extension/DropSinglePhase.sol"; -import "../extension/SignatureMintERC721Upgradeable.sol"; - -contract SignatureDrop is - Initializable, - ContractMetadata, - PlatformFee, - Royalty, - PrimarySale, - Ownable, - DelayedReveal, - LazyMint, - PermissionsEnumerable, - DropSinglePhase, - SignatureMintERC721Upgradeable, - ERC2771ContextUpgradeable, - MulticallUpgradeable, - ERC721AUpgradeable -{ - using StringsUpgradeable for uint256; - - /*/////////////////////////////////////////////////////////////// - State variables - //////////////////////////////////////////////////////////////*/ - - /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. - bytes32 private transferRole; - /// @dev Only MINTER_ROLE holders can sign off on `MintRequest`s and lazy mint tokens. - bytes32 private minterRole; - - /// @dev Max bps in the thirdweb system. - uint256 private constant MAX_BPS = 10_000; - - /*/////////////////////////////////////////////////////////////// - Constructor + initializer logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Initiliazes the contract, like a constructor. - function initialize( - address _defaultAdmin, - string memory _name, - string memory _symbol, - string memory _contractURI, - address[] memory _trustedForwarders, - address _saleRecipient, - address _royaltyRecipient, - uint128 _royaltyBps, - uint128 _platformFeeBps, - address _platformFeeRecipient - ) external initializer { - transferRole = keccak256("TRANSFER_ROLE"); - minterRole = keccak256("MINTER_ROLE"); - - // Initialize inherited contracts, most base-like -> most derived. - __ERC2771Context_init(_trustedForwarders); - __ERC721A_init(_name, _symbol); - __SignatureMintERC721_init(); - - _setupContractURI(_contractURI); - _setupOwner(_defaultAdmin); - - _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); - _setupRole(minterRole, _defaultAdmin); - _setupRole(transferRole, _defaultAdmin); - _setupRole(transferRole, address(0)); - - _setupPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); - _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); - _setupPrimarySaleRecipient(_saleRecipient); - } - - /*/////////////////////////////////////////////////////////////// - ERC 165 / 721 / 2981 logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns the URI for a given tokenId. - function tokenURI(uint256 _tokenId) public view override returns (string memory) { - uint256 batchId = getBatchId(_tokenId); - string memory batchUri = getBaseURI(_tokenId); - - if (isEncryptedBatch(batchId)) { - return string(abi.encodePacked(batchUri, "0")); - } else { - return string(abi.encodePacked(batchUri, _tokenId.toString())); - } - } - - /// @dev See ERC 165 - function supportsInterface(bytes4 interfaceId) - public - view - virtual - override(ERC721AUpgradeable, IERC165) - returns (bool) - { - return super.supportsInterface(interfaceId) || type(IERC2981Upgradeable).interfaceId == interfaceId; - } - - function contractType() external pure returns (bytes32) { - return bytes32("SignatureDrop"); - } - - function contractVersion() external pure returns (uint256) { - return 2; - } - - /*/////////////////////////////////////////////////////////////// - Lazy minting + delayed-reveal logic - //////////////////////////////////////////////////////////////*/ - - /** - * @dev Lets an account with `MINTER_ROLE` lazy mint 'n' NFTs. - * The URIs for each token is the provided `_baseURIForTokens` + `{tokenId}`. - */ - function lazyMint( - uint256 _amount, - string calldata _baseURIForTokens, - bytes calldata _encryptedBaseURI - ) public override onlyRole(minterRole) returns (uint256 batchId) { - if (_encryptedBaseURI.length != 0) { - _setEncryptedBaseURI(nextTokenIdToLazyMint + _amount, _encryptedBaseURI); - } - - return super.lazyMint(_amount, _baseURIForTokens, _encryptedBaseURI); - } - - /// @dev Lets an account with `MINTER_ROLE` reveal the URI for a batch of 'delayed-reveal' NFTs. - function reveal(uint256 _index, bytes calldata _key) - external - onlyRole(minterRole) - returns (string memory revealedURI) - { - uint256 batchId = getBatchIdAtIndex(_index); - revealedURI = getRevealURI(batchId, _key); - - _setEncryptedBaseURI(batchId, ""); - _setBaseURI(batchId, revealedURI); - - emit TokenURIRevealed(_index, revealedURI); - } - - /*/////////////////////////////////////////////////////////////// - Claiming lazy minted tokens logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Claim lazy minted tokens via signature. - function mintWithSignature(MintRequest calldata _req, bytes calldata _signature) - external - payable - returns (address signer) - { - if (_req.quantity == 0) { - revert("0 qty"); - } - - uint256 tokenIdToMint = _currentIndex; - if (tokenIdToMint + _req.quantity > nextTokenIdToLazyMint) { - revert("Not enough tokens"); - } - - // Verify and process payload. - signer = _processRequest(_req, _signature); - - /** - * Get receiver of tokens. - * - * Note: If `_req.to == address(0)`, a `mintWithSignature` transaction sitting in the - * mempool can be frontrun by copying the input data, since the minted tokens - * will be sent to the `_msgSender()` in this case. - */ - address receiver = _req.to == address(0) ? _msgSender() : _req.to; - - // Collect price - collectPriceOnClaim(_req.quantity, _req.currency, _req.pricePerToken); - - // Mint tokens. - _safeMint(receiver, _req.quantity); - - emit TokensMintedWithSignature(signer, receiver, tokenIdToMint, _req); - } - - /*/////////////////////////////////////////////////////////////// - Internal functions - //////////////////////////////////////////////////////////////*/ - - /// @dev Runs before every `claim` function call. - function _beforeClaim( - address, - uint256 _quantity, - address, - uint256, - AllowlistProof calldata, - bytes memory - ) internal view override { - bool bot = isTrustedForwarder(msg.sender) || _msgSender() == tx.origin; - require(bot, "BOT"); - require(_currentIndex + _quantity <= nextTokenIdToLazyMint, "Not enough tokens"); - } - - /// @dev Collects and distributes the primary sale value of NFTs being claimed. - function collectPriceOnClaim( - uint256 _quantityToClaim, - address _currency, - uint256 _pricePerToken - ) internal override { - if (_pricePerToken == 0) { - return; - } - - (address platformFeeRecipient, uint16 platformFeeBps) = getPlatformFeeInfo(); - - uint256 totalPrice = _quantityToClaim * _pricePerToken; - uint256 platformFees = (totalPrice * platformFeeBps) / MAX_BPS; - - if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { - if (msg.value != totalPrice) { - revert("Must send total price"); - } - } - - CurrencyTransferLib.transferCurrency(_currency, _msgSender(), platformFeeRecipient, platformFees); - CurrencyTransferLib.transferCurrency( - _currency, - _msgSender(), - primarySaleRecipient(), - totalPrice - platformFees - ); - } - - /// @dev Transfers the NFTs being claimed. - function transferTokensOnClaim(address _to, uint256 _quantityBeingClaimed) - internal - override - returns (uint256 startTokenId) - { - startTokenId = _currentIndex; - _safeMint(_to, _quantityBeingClaimed); - } - - /// @dev Returns whether a given address is authorized to sign mint requests. - function _isAuthorizedSigner(address _signer) internal view override returns (bool) { - return hasRole(minterRole, _signer); - } - - /// @dev Checks whether platform fee info can be set in the given execution context. - function _canSetPlatformFeeInfo() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Checks whether primary sale recipient can be set in the given execution context. - function _canSetPrimarySaleRecipient() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Checks whether owner can be set in the given execution context. - function _canSetOwner() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Checks whether royalty info can be set in the given execution context. - function _canSetRoyaltyInfo() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Checks whether contract metadata can be set in the given execution context. - function _canSetContractURI() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Checks whether platform fee info can be set in the given execution context. - function _canSetClaimConditions() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Returns whether lazy minting can be done in the given execution context. - function _canLazyMint() internal view virtual override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /*/////////////////////////////////////////////////////////////// - Miscellaneous - //////////////////////////////////////////////////////////////*/ - - /** - * Returns the total amount of tokens minted in the contract. - */ - function totalMinted() external view returns (uint256) { - unchecked { - return _currentIndex - _startTokenId(); - } - } - - /// @dev The tokenId of the next NFT that will be minted / lazy minted. - function nextTokenIdToMint() external view returns (uint256) { - return nextTokenIdToLazyMint; - } - - /// @dev Burns `tokenId`. See {ERC721-_burn}. - function burn(uint256 tokenId) external virtual { - // note: ERC721AUpgradeable's `_burn(uint256,bool)` internally checks for token approvals. - _burn(tokenId, true); - } - - /// @dev See {ERC721-_beforeTokenTransfer}. - function _beforeTokenTransfers( - address from, - address to, - uint256 startTokenId, - uint256 quantity - ) internal virtual override { - super._beforeTokenTransfers(from, to, startTokenId, quantity); - - // if transfer is restricted on the contract, we still want to allow burning and minting - if (!hasRole(transferRole, address(0)) && from != address(0) && to != address(0)) { - if (!hasRole(transferRole, from) && !hasRole(transferRole, to)) { - revert("!Transfer-Role"); - } - } - } - - function _dropMsgSender() internal view virtual override returns (address) { - return _msgSender(); - } - - function _msgSender() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (address sender) - { - return ERC2771ContextUpgradeable._msgSender(); - } - - function _msgData() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (bytes calldata) - { - return ERC2771ContextUpgradeable._msgData(); - } -} diff --git a/contracts/token/TokenERC1155.sol b/contracts/token/TokenERC1155.sol deleted file mode 100644 index 71251eee4..000000000 --- a/contracts/token/TokenERC1155.sol +++ /dev/null @@ -1,517 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -// Interface -import { ITokenERC1155 } from "../interfaces/token/ITokenERC1155.sol"; - -import "../interfaces/IThirdwebContract.sol"; -import "../extension/interface/IPlatformFee.sol"; -import "../extension/interface/IPrimarySale.sol"; -import "../extension/interface/IRoyalty.sol"; -import "../extension/interface/IOwnable.sol"; - -// Token -import "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol"; - -// Signature utils -import "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol"; - -// Access Control + security -import "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; - -// Utils -import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; -import "../lib/CurrencyTransferLib.sol"; -import "../lib/FeeType.sol"; -import "../openzeppelin-presets/metatx/ERC2771ContextUpgradeable.sol"; - -// Helper interfaces -import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; - -// Thirdweb top-level -import "../interfaces/ITWFee.sol"; - -contract TokenERC1155 is - Initializable, - IThirdwebContract, - IOwnable, - IRoyalty, - IPrimarySale, - IPlatformFee, - EIP712Upgradeable, - ReentrancyGuardUpgradeable, - ERC2771ContextUpgradeable, - MulticallUpgradeable, - AccessControlEnumerableUpgradeable, - ERC1155Upgradeable, - ITokenERC1155 -{ - using ECDSAUpgradeable for bytes32; - using StringsUpgradeable for uint256; - - bytes32 private constant MODULE_TYPE = bytes32("TokenERC1155"); - uint256 private constant VERSION = 1; - - // Token name - string public name; - - // Token symbol - string public symbol; - - bytes32 private constant TYPEHASH = - keccak256( - "MintRequest(address to,address royaltyRecipient,uint256 royaltyBps,address primarySaleRecipient,uint256 tokenId,string uri,uint256 quantity,uint256 pricePerToken,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" - ); - - /// @dev Only TRANSFER_ROLE holders can have tokens transferred from or to them, during restricted transfers. - bytes32 private constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE"); - /// @dev Only MINTER_ROLE holders can sign off on `MintRequest`s. - bytes32 private constant MINTER_ROLE = keccak256("MINTER_ROLE"); - - /// @dev Max bps in the thirdweb system - uint256 private constant MAX_BPS = 10_000; - - /// @dev The address interpreted as native token of the chain. - address private constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; - - /// @dev The thirdweb contract with fee related information. - ITWFee public immutable thirdwebFee; - - /// @dev Owner of the contract (purpose: OpenSea compatibility, etc.) - address private _owner; - - /// @dev The next token ID of the NFT to mint. - uint256 public nextTokenIdToMint; - - /// @dev The adress that receives all primary sales value. - address public primarySaleRecipient; - - /// @dev The adress that receives all primary sales value. - address public platformFeeRecipient; - - /// @dev The recipient of who gets the royalty. - address private royaltyRecipient; - - /// @dev The percentage of royalty how much royalty in basis points. - uint128 private royaltyBps; - - /// @dev The % of primary sales collected by the contract as fees. - uint128 public platformFeeBps; - - /// @dev Contract level metadata. - string public contractURI; - - /// @dev Mapping from mint request UID => whether the mint request is processed. - mapping(bytes32 => bool) private minted; - - mapping(uint256 => string) private _tokenURI; - - /// @dev Token ID => total circulating supply of tokens with that ID. - mapping(uint256 => uint256) public totalSupply; - - /// @dev Token ID => the address of the recipient of primary sales. - mapping(uint256 => address) public saleRecipientForToken; - - /// @dev Token ID => royalty recipient and bps for token - mapping(uint256 => RoyaltyInfo) private royaltyInfoForToken; - - constructor(address _thirdwebFee) initializer { - thirdwebFee = ITWFee(_thirdwebFee); - } - - /// @dev Initiliazes the contract, like a constructor. - function initialize( - address _defaultAdmin, - string memory _name, - string memory _symbol, - string memory _contractURI, - address[] memory _trustedForwarders, - address _primarySaleRecipient, - address _royaltyRecipient, - uint128 _royaltyBps, - uint128 _platformFeeBps, - address _platformFeeRecipient - ) external initializer { - // Initialize inherited contracts, most base-like -> most derived. - __ReentrancyGuard_init(); - __EIP712_init("TokenERC1155", "1"); - __ERC2771Context_init(_trustedForwarders); - __ERC1155_init(""); - - // Initialize this contract's state. - name = _name; - symbol = _symbol; - royaltyRecipient = _royaltyRecipient; - royaltyBps = _royaltyBps; - platformFeeRecipient = _platformFeeRecipient; - primarySaleRecipient = _primarySaleRecipient; - contractURI = _contractURI; - platformFeeBps = _platformFeeBps; - - _owner = _defaultAdmin; - _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); - _setupRole(MINTER_ROLE, _defaultAdmin); - _setupRole(TRANSFER_ROLE, _defaultAdmin); - _setupRole(TRANSFER_ROLE, address(0)); - } - - /// ===== Public functions ===== - - /// @dev Returns the module type of the contract. - function contractType() external pure returns (bytes32) { - return MODULE_TYPE; - } - - /// @dev Returns the version of the contract. - function contractVersion() external pure returns (uint8) { - return uint8(VERSION); - } - - /** - * @dev Returns the address of the current owner. - */ - function owner() public view returns (address) { - return hasRole(DEFAULT_ADMIN_ROLE, _owner) ? _owner : address(0); - } - - /// @dev Verifies that a mint request is signed by an account holding MINTER_ROLE (at the time of the function call). - function verify(MintRequest calldata _req, bytes calldata _signature) public view returns (bool, address) { - address signer = recoverAddress(_req, _signature); - return (!minted[_req.uid] && hasRole(MINTER_ROLE, signer), signer); - } - - /// @dev Returns the URI for a tokenId - function uri(uint256 _tokenId) public view override returns (string memory) { - return _tokenURI[_tokenId]; - } - - /// @dev Lets an account with MINTER_ROLE mint an NFT. - function mintTo( - address _to, - uint256 _tokenId, - string calldata _uri, - uint256 _amount - ) external onlyRole(MINTER_ROLE) { - uint256 tokenIdToMint; - if (_tokenId == type(uint256).max) { - tokenIdToMint = nextTokenIdToMint; - nextTokenIdToMint += 1; - } else { - require(_tokenId < nextTokenIdToMint, "invalid id"); - tokenIdToMint = _tokenId; - } - - // `_mintTo` is re-used. `mintTo` just adds a minter role check. - _mintTo(_to, _uri, tokenIdToMint, _amount); - } - - /// ===== External functions ===== - - /// @dev See EIP-2981 - function royaltyInfo(uint256 tokenId, uint256 salePrice) - external - view - virtual - returns (address receiver, uint256 royaltyAmount) - { - (address recipient, uint256 bps) = getRoyaltyInfoForToken(tokenId); - receiver = recipient; - royaltyAmount = (salePrice * bps) / MAX_BPS; - } - - /// @dev Mints an NFT according to the provided mint request. - function mintWithSignature(MintRequest calldata _req, bytes calldata _signature) external payable nonReentrant { - address signer = verifyRequest(_req, _signature); - address receiver = _req.to == address(0) ? _msgSender() : _req.to; - - uint256 tokenIdToMint; - if (_req.tokenId == type(uint256).max) { - tokenIdToMint = nextTokenIdToMint; - nextTokenIdToMint += 1; - } else { - require(_req.tokenId < nextTokenIdToMint, "invalid id"); - tokenIdToMint = _req.tokenId; - } - - if (_req.royaltyRecipient != address(0)) { - royaltyInfoForToken[tokenIdToMint] = RoyaltyInfo({ - recipient: _req.royaltyRecipient, - bps: _req.royaltyBps - }); - } - - _mintTo(receiver, _req.uri, tokenIdToMint, _req.quantity); - - collectPrice(_req); - - emit TokensMintedWithSignature(signer, receiver, tokenIdToMint, _req); - } - - // ===== Setter functions ===== - - /// @dev Lets a module admin set the default recipient of all primary sales. - function setPrimarySaleRecipient(address _saleRecipient) external onlyRole(DEFAULT_ADMIN_ROLE) { - primarySaleRecipient = _saleRecipient; - emit PrimarySaleRecipientUpdated(_saleRecipient); - } - - /// @dev Lets a module admin update the royalty bps and recipient. - function setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) - external - onlyRole(DEFAULT_ADMIN_ROLE) - { - require(_royaltyBps <= MAX_BPS, "exceed royalty bps"); - - royaltyRecipient = _royaltyRecipient; - royaltyBps = uint128(_royaltyBps); - - emit DefaultRoyalty(_royaltyRecipient, _royaltyBps); - } - - /// @dev Lets a module admin set the royalty recipient for a particular token Id. - function setRoyaltyInfoForToken( - uint256 _tokenId, - address _recipient, - uint256 _bps - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(_bps <= MAX_BPS, "exceed royalty bps"); - - royaltyInfoForToken[_tokenId] = RoyaltyInfo({ recipient: _recipient, bps: _bps }); - - emit RoyaltyForToken(_tokenId, _recipient, _bps); - } - - /// @dev Lets a module admin update the fees on primary sales. - function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) - external - onlyRole(DEFAULT_ADMIN_ROLE) - { - require(_platformFeeBps <= MAX_BPS, "bps <= 10000."); - - platformFeeBps = uint64(_platformFeeBps); - platformFeeRecipient = _platformFeeRecipient; - - emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); - } - - /// @dev Lets a module admin set a new owner for the contract. The new owner must be a module admin. - function setOwner(address _newOwner) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(hasRole(DEFAULT_ADMIN_ROLE, _newOwner), "new owner not module admin."); - address _prevOwner = _owner; - _owner = _newOwner; - - emit OwnerUpdated(_prevOwner, _newOwner); - } - - /// @dev Lets a module admin set the URI for contract-level metadata. - function setContractURI(string calldata _uri) external onlyRole(DEFAULT_ADMIN_ROLE) { - contractURI = _uri; - } - - /// ===== Getter functions ===== - - /// @dev Returns the platform fee bps and recipient. - function getPlatformFeeInfo() external view returns (address, uint16) { - return (platformFeeRecipient, uint16(platformFeeBps)); - } - - /// @dev Returns the platform fee bps and recipient. - function getDefaultRoyaltyInfo() external view returns (address, uint16) { - return (royaltyRecipient, uint16(royaltyBps)); - } - - /// @dev Returns the royalty recipient for a particular token Id. - function getRoyaltyInfoForToken(uint256 _tokenId) public view returns (address, uint16) { - RoyaltyInfo memory royaltyForToken = royaltyInfoForToken[_tokenId]; - - return - royaltyForToken.recipient == address(0) - ? (royaltyRecipient, uint16(royaltyBps)) - : (royaltyForToken.recipient, uint16(royaltyForToken.bps)); - } - - /// ===== Internal functions ===== - - /// @dev Mints an NFT to `to` - function _mintTo( - address _to, - string calldata _uri, - uint256 _tokenId, - uint256 _amount - ) internal { - if (bytes(_tokenURI[_tokenId]).length == 0) { - require(bytes(_uri).length > 0, "empty uri."); - _tokenURI[_tokenId] = _uri; - } - - _mint(_to, _tokenId, _amount, ""); - - emit TokensMinted(_to, _tokenId, _tokenURI[_tokenId], _amount); - } - - /// @dev Returns the address of the signer of the mint request. - function recoverAddress(MintRequest calldata _req, bytes calldata _signature) internal view returns (address) { - return _hashTypedDataV4(keccak256(_encodeRequest(_req))).recover(_signature); - } - - /// @dev Resolves 'stack too deep' error in `recoverAddress`. - function _encodeRequest(MintRequest calldata _req) internal pure returns (bytes memory) { - return - abi.encode( - TYPEHASH, - _req.to, - _req.royaltyRecipient, - _req.royaltyBps, - _req.primarySaleRecipient, - _req.tokenId, - keccak256(bytes(_req.uri)), - _req.quantity, - _req.pricePerToken, - _req.currency, - _req.validityStartTimestamp, - _req.validityEndTimestamp, - _req.uid - ); - } - - /// @dev Verifies that a mint request is valid. - function verifyRequest(MintRequest calldata _req, bytes calldata _signature) internal returns (address) { - (bool success, address signer) = verify(_req, _signature); - require(success, "invalid signature"); - - require( - _req.validityStartTimestamp <= block.timestamp && _req.validityEndTimestamp >= block.timestamp, - "request expired" - ); - - minted[_req.uid] = true; - - return signer; - } - - /// @dev Collects and distributes the primary sale value of tokens being claimed. - function collectPrice(MintRequest memory _req) internal { - if (_req.pricePerToken == 0) { - return; - } - - uint256 totalPrice = _req.pricePerToken * _req.quantity; - uint256 platformFees = (totalPrice * platformFeeBps) / MAX_BPS; - (address twFeeRecipient, uint256 twFeeBps) = thirdwebFee.getFeeInfo(address(this), FeeType.PRIMARY_SALE); - uint256 twFee = (totalPrice * twFeeBps) / MAX_BPS; - - if (_req.currency == NATIVE_TOKEN) { - require(msg.value == totalPrice, "must send total price."); - } - - address saleRecipient = _req.primarySaleRecipient == address(0) - ? primarySaleRecipient - : _req.primarySaleRecipient; - - CurrencyTransferLib.transferCurrency(_req.currency, _msgSender(), platformFeeRecipient, platformFees); - CurrencyTransferLib.transferCurrency(_req.currency, _msgSender(), twFeeRecipient, twFee); - CurrencyTransferLib.transferCurrency( - _req.currency, - _msgSender(), - saleRecipient, - totalPrice - platformFees - twFee - ); - } - - /// ===== Low-level overrides ===== - - /// @dev Lets a token owner burn the tokens they own (i.e. destroy for good) - function burn( - address account, - uint256 id, - uint256 value - ) public virtual { - require( - account == _msgSender() || isApprovedForAll(account, _msgSender()), - "ERC1155: caller is not owner nor approved." - ); - - _burn(account, id, value); - } - - /// @dev Lets a token owner burn multiple tokens they own at once (i.e. destroy for good) - function burnBatch( - address account, - uint256[] memory ids, - uint256[] memory values - ) public virtual { - require( - account == _msgSender() || isApprovedForAll(account, _msgSender()), - "ERC1155: caller is not owner nor approved." - ); - - _burnBatch(account, ids, values); - } - - /** - * @dev See {ERC1155-_beforeTokenTransfer}. - */ - function _beforeTokenTransfer( - address operator, - address from, - address to, - uint256[] memory ids, - uint256[] memory amounts, - bytes memory data - ) internal virtual override { - super._beforeTokenTransfer(operator, from, to, ids, amounts, data); - - // if transfer is restricted on the contract, we still want to allow burning and minting - if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { - require(hasRole(TRANSFER_ROLE, from) || hasRole(TRANSFER_ROLE, to), "restricted to TRANSFER_ROLE holders."); - } - - if (from == address(0)) { - for (uint256 i = 0; i < ids.length; ++i) { - totalSupply[ids[i]] += amounts[i]; - } - } - - if (to == address(0)) { - for (uint256 i = 0; i < ids.length; ++i) { - totalSupply[ids[i]] -= amounts[i]; - } - } - } - - function supportsInterface(bytes4 interfaceId) - public - view - virtual - override(AccessControlEnumerableUpgradeable, ERC1155Upgradeable, IERC165Upgradeable, IERC165) - returns (bool) - { - return - super.supportsInterface(interfaceId) || - interfaceId == type(IERC1155Upgradeable).interfaceId || - interfaceId == type(IERC2981Upgradeable).interfaceId; - } - - function _msgSender() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (address sender) - { - return ERC2771ContextUpgradeable._msgSender(); - } - - function _msgData() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (bytes calldata) - { - return ERC2771ContextUpgradeable._msgData(); - } -} diff --git a/contracts/token/TokenERC20.sol b/contracts/token/TokenERC20.sol deleted file mode 100644 index b51322c03..000000000 --- a/contracts/token/TokenERC20.sol +++ /dev/null @@ -1,335 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -//Interface -import { ITokenERC20 } from "../interfaces/token/ITokenERC20.sol"; - -import "../interfaces/IThirdwebContract.sol"; -import "../extension/interface/IPlatformFee.sol"; -import "../extension/interface/IPrimarySale.sol"; - -// Token -import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20VotesUpgradeable.sol"; - -// Security -import "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; - -// Signature utils -import "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol"; - -// Meta transactions -import "../openzeppelin-presets/metatx/ERC2771ContextUpgradeable.sol"; - -// Utils -import "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; -import "../lib/CurrencyTransferLib.sol"; -import "../lib/FeeType.sol"; - -// Thirdweb top-level -import "../interfaces/ITWFee.sol"; - -contract TokenERC20 is - Initializable, - IThirdwebContract, - IPrimarySale, - IPlatformFee, - ReentrancyGuardUpgradeable, - ERC2771ContextUpgradeable, - MulticallUpgradeable, - ERC20BurnableUpgradeable, - ERC20PausableUpgradeable, - ERC20VotesUpgradeable, - ITokenERC20, - AccessControlEnumerableUpgradeable -{ - using ECDSAUpgradeable for bytes32; - - bytes32 private constant MODULE_TYPE = bytes32("TokenERC20"); - uint256 private constant VERSION = 1; - - bytes32 private constant TYPEHASH = - keccak256( - "MintRequest(address to,address primarySaleRecipient,uint256 quantity,uint256 price,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" - ); - - bytes32 internal constant MINTER_ROLE = keccak256("MINTER_ROLE"); - bytes32 internal constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); - bytes32 internal constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE"); - - /// @dev The thirdweb contract with fee related information. - ITWFee internal immutable thirdwebFee; - - /// @dev Returns the URI for the storefront-level metadata of the contract. - string public contractURI; - - /// @dev Max bps in the thirdweb system - uint128 internal constant MAX_BPS = 10_000; - - /// @dev The % of primary sales collected by the contract as fees. - uint128 internal platformFeeBps; - - /// @dev The adress that receives all primary sales value. - address internal platformFeeRecipient; - - /// @dev The adress that receives all primary sales value. - address public primarySaleRecipient; - - /// @dev Mapping from mint request UID => whether the mint request is processed. - mapping(bytes32 => bool) private minted; - - constructor(address _thirdwebFee) initializer { - thirdwebFee = ITWFee(_thirdwebFee); - } - - /// @dev Initiliazes the contract, like a constructor. - function initialize( - address _defaultAdmin, - string memory _name, - string memory _symbol, - string memory _contractURI, - address[] memory _trustedForwarders, - address _primarySaleRecipient, - address _platformFeeRecipient, - uint256 _platformFeeBps - ) external initializer { - __ERC2771Context_init_unchained(_trustedForwarders); - __ERC20Permit_init(_name); - __ERC20_init_unchained(_name, _symbol); - - contractURI = _contractURI; - primarySaleRecipient = _primarySaleRecipient; - platformFeeRecipient = _platformFeeRecipient; - platformFeeBps = uint128(_platformFeeBps); - - _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); - _setupRole(TRANSFER_ROLE, _defaultAdmin); - _setupRole(MINTER_ROLE, _defaultAdmin); - _setupRole(PAUSER_ROLE, _defaultAdmin); - _setupRole(TRANSFER_ROLE, address(0)); - } - - /// @dev Returns the module type of the contract. - function contractType() external pure virtual returns (bytes32) { - return MODULE_TYPE; - } - - /// @dev Returns the version of the contract. - function contractVersion() external pure virtual returns (uint8) { - return uint8(VERSION); - } - - function _afterTokenTransfer( - address from, - address to, - uint256 amount - ) internal virtual override(ERC20Upgradeable, ERC20VotesUpgradeable) { - super._afterTokenTransfer(from, to, amount); - } - - /// @dev Runs on every transfer. - function _beforeTokenTransfer( - address from, - address to, - uint256 amount - ) internal override(ERC20Upgradeable, ERC20PausableUpgradeable) { - super._beforeTokenTransfer(from, to, amount); - - if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { - require(hasRole(TRANSFER_ROLE, from) || hasRole(TRANSFER_ROLE, to), "transfers restricted."); - } - } - - function _mint(address account, uint256 amount) internal virtual override(ERC20Upgradeable, ERC20VotesUpgradeable) { - super._mint(account, amount); - } - - function _burn(address account, uint256 amount) internal virtual override(ERC20Upgradeable, ERC20VotesUpgradeable) { - super._burn(account, amount); - } - - /** - * @dev Creates `amount` new tokens for `to`. - * - * See {ERC20-_mint}. - * - * Requirements: - * - * - the caller must have the `MINTER_ROLE`. - */ - function mintTo(address to, uint256 amount) public virtual { - require(hasRole(MINTER_ROLE, _msgSender()), "not minter."); - _mintTo(to, amount); - } - - /// @dev Verifies that a mint request is signed by an account holding MINTER_ROLE (at the time of the function call). - function verify(MintRequest calldata _req, bytes calldata _signature) public view returns (bool, address) { - address signer = recoverAddress(_req, _signature); - return (!minted[_req.uid] && hasRole(MINTER_ROLE, signer), signer); - } - - /// @dev Mints tokens according to the provided mint request. - function mintWithSignature(MintRequest calldata _req, bytes calldata _signature) external payable nonReentrant { - address signer = verifyRequest(_req, _signature); - address receiver = _req.to == address(0) ? _msgSender() : _req.to; - address saleRecipient = _req.primarySaleRecipient == address(0) - ? primarySaleRecipient - : _req.primarySaleRecipient; - - collectPrice(saleRecipient, _req.currency, _req.price); - - _mintTo(receiver, _req.quantity); - - emit TokensMintedWithSignature(signer, receiver, _req); - } - - /// @dev Lets a module admin set the default recipient of all primary sales. - function setPrimarySaleRecipient(address _saleRecipient) external onlyRole(DEFAULT_ADMIN_ROLE) { - primarySaleRecipient = _saleRecipient; - emit PrimarySaleRecipientUpdated(_saleRecipient); - } - - /// @dev Lets a module admin update the fees on primary sales. - function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) - external - onlyRole(DEFAULT_ADMIN_ROLE) - { - require(_platformFeeBps <= MAX_BPS, "bps <= 10000."); - - platformFeeBps = uint64(_platformFeeBps); - platformFeeRecipient = _platformFeeRecipient; - - emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); - } - - /// @dev Returns the platform fee bps and recipient. - function getPlatformFeeInfo() external view returns (address, uint16) { - return (platformFeeRecipient, uint16(platformFeeBps)); - } - - /// @dev Collects and distributes the primary sale value of tokens being claimed. - function collectPrice( - address _primarySaleRecipient, - address _currency, - uint256 _price - ) internal { - if (_price == 0) { - return; - } - - uint256 platformFees = (_price * platformFeeBps) / MAX_BPS; - (address twFeeRecipient, uint256 twFeeBps) = thirdwebFee.getFeeInfo(address(this), FeeType.PRIMARY_SALE); - uint256 twFee = (_price * twFeeBps) / MAX_BPS; - - if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { - require(msg.value == _price, "must send total price."); - } - - CurrencyTransferLib.transferCurrency(_currency, _msgSender(), platformFeeRecipient, platformFees); - CurrencyTransferLib.transferCurrency(_currency, _msgSender(), twFeeRecipient, twFee); - CurrencyTransferLib.transferCurrency( - _currency, - _msgSender(), - _primarySaleRecipient, - _price - platformFees - twFee - ); - } - - /// @dev Mints `amount` of tokens to `to` - function _mintTo(address _to, uint256 _amount) internal { - _mint(_to, _amount); - emit TokensMinted(_to, _amount); - } - - /// @dev Verifies that a mint request is valid. - function verifyRequest(MintRequest calldata _req, bytes calldata _signature) internal returns (address) { - (bool success, address signer) = verify(_req, _signature); - require(success, "invalid signature"); - - require( - _req.validityStartTimestamp <= block.timestamp && _req.validityEndTimestamp >= block.timestamp, - "request expired" - ); - - minted[_req.uid] = true; - - return signer; - } - - /// @dev Returns the address of the signer of the mint request. - function recoverAddress(MintRequest calldata _req, bytes calldata _signature) internal view returns (address) { - return _hashTypedDataV4(keccak256(_encodeRequest(_req))).recover(_signature); - } - - /// @dev Resolves 'stack too deep' error in `recoverAddress`. - function _encodeRequest(MintRequest calldata _req) internal pure returns (bytes memory) { - return - abi.encode( - TYPEHASH, - _req.to, - _req.primarySaleRecipient, - _req.quantity, - _req.price, - _req.currency, - _req.validityStartTimestamp, - _req.validityEndTimestamp, - _req.uid - ); - } - - /** - * @dev Pauses all token transfers. - * - * See {ERC20Pausable} and {Pausable-_pause}. - * - * Requirements: - * - * - the caller must have the `PAUSER_ROLE`. - */ - function pause() public virtual { - require(hasRole(PAUSER_ROLE, _msgSender()), "not pauser."); - _pause(); - } - - /** - * @dev Unpauses all token transfers. - * - * See {ERC20Pausable} and {Pausable-_unpause}. - * - * Requirements: - * - * - the caller must have the `PAUSER_ROLE`. - */ - function unpause() public virtual { - require(hasRole(PAUSER_ROLE, _msgSender()), "not pauser."); - _unpause(); - } - - /// @dev Sets contract URI for the storefront-level metadata of the contract. - function setContractURI(string calldata _uri) external onlyRole(DEFAULT_ADMIN_ROLE) { - contractURI = _uri; - } - - function _msgSender() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (address sender) - { - return ERC2771ContextUpgradeable._msgSender(); - } - - function _msgData() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (bytes calldata) - { - return ERC2771ContextUpgradeable._msgData(); - } -} diff --git a/docs/AccessControl.md b/docs/AccessControl.md deleted file mode 100644 index fb03636e7..000000000 --- a/docs/AccessControl.md +++ /dev/null @@ -1,207 +0,0 @@ -# AccessControl - - - - - - - -*Contract module that allows children to implement role-based access control mechanisms. This is a lightweight version that doesn't allow enumerating role members except through off-chain means by accessing the contract event logs. Some applications may benefit from on-chain enumerability, for those cases see {AccessControlEnumerable}. Roles are referred to by their `bytes32` identifier. These should be exposed in the external API and be unique. The best way to achieve this is by using `public constant` hash digests: ``` bytes32 public constant MY_ROLE = keccak256("MY_ROLE"); ``` Roles can be used to represent a set of permissions. To restrict access to a function call, use {hasRole}: ``` function foo() public { require(hasRole(MY_ROLE, msg.sender)); ... } ``` Roles can be granted and revoked dynamically via the {grantRole} and {revokeRole} functions. Each role has an associated admin role, and only accounts that have a role's admin role can call {grantRole} and {revokeRole}. By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means that only accounts with this role will be able to grant or revoke other roles. More complex role relationships can be created by using {_setRoleAdmin}. WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to grant and revoke this role. Extra precautions should be taken to secure accounts that have been granted it.* - -## Methods - -### DEFAULT_ADMIN_ROLE - -```solidity -function DEFAULT_ADMIN_ROLE() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been revoked `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - -## Events - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - - - diff --git a/docs/AccessControlEnumerable.md b/docs/AccessControlEnumerable.md deleted file mode 100644 index 691b6431d..000000000 --- a/docs/AccessControlEnumerable.md +++ /dev/null @@ -1,252 +0,0 @@ -# AccessControlEnumerable - - - - - - - -*Extension of {AccessControl} that allows enumerating the members of each role.* - -## Methods - -### DEFAULT_ADMIN_ROLE - -```solidity -function DEFAULT_ADMIN_ROLE() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleMember - -```solidity -function getRoleMember(bytes32 role, uint256 index) external view returns (address) -``` - - - -*Returns one of the accounts that have `role`. `index` must be a value between 0 and {getRoleMemberCount}, non-inclusive. Role bearers are not sorted in any particular way, and their ordering may change at any point. WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure you perform all queries on the same block. See the following https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] for more information.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getRoleMemberCount - -```solidity -function getRoleMemberCount(bytes32 role) external view returns (uint256) -``` - - - -*Returns the number of accounts that have `role`. Can be used together with {getRoleMember} to enumerate all bearers of a role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been revoked `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - -## Events - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - - - diff --git a/docs/AccessControlEnumerableUpgradeable.md b/docs/AccessControlEnumerableUpgradeable.md deleted file mode 100644 index fb90836e0..000000000 --- a/docs/AccessControlEnumerableUpgradeable.md +++ /dev/null @@ -1,252 +0,0 @@ -# AccessControlEnumerableUpgradeable - - - - - - - -*Extension of {AccessControl} that allows enumerating the members of each role.* - -## Methods - -### DEFAULT_ADMIN_ROLE - -```solidity -function DEFAULT_ADMIN_ROLE() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleMember - -```solidity -function getRoleMember(bytes32 role, uint256 index) external view returns (address) -``` - - - -*Returns one of the accounts that have `role`. `index` must be a value between 0 and {getRoleMemberCount}, non-inclusive. Role bearers are not sorted in any particular way, and their ordering may change at any point. WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure you perform all queries on the same block. See the following https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] for more information.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getRoleMemberCount - -```solidity -function getRoleMemberCount(bytes32 role) external view returns (uint256) -``` - - - -*Returns the number of accounts that have `role`. Can be used together with {getRoleMember} to enumerate all bearers of a role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been revoked `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - -## Events - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - - - diff --git a/docs/AccessControlUpgradeable.md b/docs/AccessControlUpgradeable.md deleted file mode 100644 index da10fd744..000000000 --- a/docs/AccessControlUpgradeable.md +++ /dev/null @@ -1,207 +0,0 @@ -# AccessControlUpgradeable - - - - - - - -*Contract module that allows children to implement role-based access control mechanisms. This is a lightweight version that doesn't allow enumerating role members except through off-chain means by accessing the contract event logs. Some applications may benefit from on-chain enumerability, for those cases see {AccessControlEnumerable}. Roles are referred to by their `bytes32` identifier. These should be exposed in the external API and be unique. The best way to achieve this is by using `public constant` hash digests: ``` bytes32 public constant MY_ROLE = keccak256("MY_ROLE"); ``` Roles can be used to represent a set of permissions. To restrict access to a function call, use {hasRole}: ``` function foo() public { require(hasRole(MY_ROLE, msg.sender)); ... } ``` Roles can be granted and revoked dynamically via the {grantRole} and {revokeRole} functions. Each role has an associated admin role, and only accounts that have a role's admin role can call {grantRole} and {revokeRole}. By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means that only accounts with this role will be able to grant or revoke other roles. More complex role relationships can be created by using {_setRoleAdmin}. WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to grant and revoke this role. Extra precautions should be taken to secure accounts that have been granted it.* - -## Methods - -### DEFAULT_ADMIN_ROLE - -```solidity -function DEFAULT_ADMIN_ROLE() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been revoked `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - -## Events - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - - - diff --git a/docs/Address.md b/docs/Address.md deleted file mode 100644 index 927f4068e..000000000 --- a/docs/Address.md +++ /dev/null @@ -1,12 +0,0 @@ -# Address - - - - - - - -*Collection of functions related to the address type* - - - diff --git a/docs/AddressUpgradeable.md b/docs/AddressUpgradeable.md deleted file mode 100644 index 456cdd15d..000000000 --- a/docs/AddressUpgradeable.md +++ /dev/null @@ -1,12 +0,0 @@ -# AddressUpgradeable - - - - - - - -*Collection of functions related to the address type* - - - diff --git a/docs/BatchMintMetadata.md b/docs/BatchMintMetadata.md deleted file mode 100644 index 111a89cc8..000000000 --- a/docs/BatchMintMetadata.md +++ /dev/null @@ -1,54 +0,0 @@ -# BatchMintMetadata - - - - - -The `BatchMintMetadata` is a contract extension for any base NFT contract. It lets the smart contract using this extension set metadata for `n` number of NFTs all at once. This is enabled by storing a single base URI for a batch of `n` NFTs, where the metadata for each NFT in a relevant batch is `baseURI/tokenId`. - - - -## Methods - -### getBaseURICount - -```solidity -function getBaseURICount() external view returns (uint256) -``` - - - -*Returns the number of batches of tokens having the same baseURI.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getBatchIdAtIndex - -```solidity -function getBatchIdAtIndex(uint256 _index) external view returns (uint256) -``` - - - -*Returns the id for the batch of tokens the given tokenId belongs to.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - - - - diff --git a/docs/BitMapsUpgradeable.md b/docs/BitMapsUpgradeable.md deleted file mode 100644 index bec8278d3..000000000 --- a/docs/BitMapsUpgradeable.md +++ /dev/null @@ -1,12 +0,0 @@ -# BitMapsUpgradeable - - - - - - - -*Library for managing uint256 to bool mapping in a compact and efficient way, providing the keys are sequential. Largelly inspired by Uniswap's https://github.com/Uniswap/merkle-distributor/blob/master/contracts/MerkleDistributor.sol[merkle-distributor].* - - - diff --git a/docs/Clones.md b/docs/Clones.md deleted file mode 100644 index 05da81c8f..000000000 --- a/docs/Clones.md +++ /dev/null @@ -1,12 +0,0 @@ -# Clones - - - - - - - -*https://eips.ethereum.org/EIPS/eip-1167[EIP 1167] is a standard for deploying minimal proxy contracts, also known as "clones". > To simply and cheaply clone contract functionality in an immutable way, this standard specifies > a minimal bytecode implementation that delegates all calls to a known, fixed address. The library includes functions to deploy a proxy using either `create` (traditional deployment) or `create2` (salted deterministic deployment). It also includes functions to predict the addresses of clones deployed using the deterministic method. _Available since v3.4._* - - - diff --git a/docs/Context.md b/docs/Context.md deleted file mode 100644 index 4ab2a3548..000000000 --- a/docs/Context.md +++ /dev/null @@ -1,12 +0,0 @@ -# Context - - - - - - - -*Provides information about the current execution context, including the sender of the transaction and its data. While these are generally available via msg.sender and msg.data, they should not be accessed in such a direct manner, since when dealing with meta-transactions the account sending and paying for execution may not be the actual sender (as far as an application is concerned). This contract is only required for intermediate, library-like contracts.* - - - diff --git a/docs/ContextUpgradeable.md b/docs/ContextUpgradeable.md deleted file mode 100644 index 18f33b80f..000000000 --- a/docs/ContextUpgradeable.md +++ /dev/null @@ -1,12 +0,0 @@ -# ContextUpgradeable - - - - - - - -*Provides information about the current execution context, including the sender of the transaction and its data. While these are generally available via msg.sender and msg.data, they should not be accessed in such a direct manner, since when dealing with meta-transactions the account sending and paying for execution may not be the actual sender (as far as an application is concerned). This contract is only required for intermediate, library-like contracts.* - - - diff --git a/docs/ContractMetadata.md b/docs/ContractMetadata.md deleted file mode 100644 index bf7a18f56..000000000 --- a/docs/ContractMetadata.md +++ /dev/null @@ -1,68 +0,0 @@ -# ContractMetadata - - - - - -Thirdweb's `ContractMetadata` is a contract extension for any base contracts. It lets you set a metadata URI for you contract. Additionally, `ContractMetadata` is necessary for NFT contracts that want royalties to get distributed on OpenSea. - - - -## Methods - -### contractURI - -```solidity -function contractURI() external view returns (string) -``` - - - -*Contract level metadata.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### setContractURI - -```solidity -function setContractURI(string _uri) external nonpayable -``` - - - -*Lets a contract admin set the URI for contract-level metadata.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _uri | string | undefined - - - -## Events - -### ContractURIUpdated - -```solidity -event ContractURIUpdated(string prevURI, string newURI) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| prevURI | string | undefined | -| newURI | string | undefined | - - - diff --git a/docs/ContractPublisher.md b/docs/ContractPublisher.md deleted file mode 100644 index c290a6582..000000000 --- a/docs/ContractPublisher.md +++ /dev/null @@ -1,548 +0,0 @@ -# ContractPublisher - - - - - - - - - -## Methods - -### DEFAULT_ADMIN_ROLE - -```solidity -function DEFAULT_ADMIN_ROLE() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getAllPublishedContracts - -```solidity -function getAllPublishedContracts(address _publisher) external view returns (struct IContractPublisher.CustomContractInstance[] published) -``` - -Returns the latest version of all contracts published by a publisher. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _publisher | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| published | IContractPublisher.CustomContractInstance[] | undefined - -### getPublishedContract - -```solidity -function getPublishedContract(address _publisher, string _contractId) external view returns (struct IContractPublisher.CustomContractInstance published) -``` - -Returns the latest version of a contract published by a publisher. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _publisher | address | undefined -| _contractId | string | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| published | IContractPublisher.CustomContractInstance | undefined - -### getPublishedContractVersions - -```solidity -function getPublishedContractVersions(address _publisher, string _contractId) external view returns (struct IContractPublisher.CustomContractInstance[] published) -``` - -Returns all versions of a published contract. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _publisher | address | undefined -| _contractId | string | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| published | IContractPublisher.CustomContractInstance[] | undefined - -### getPublishedUriFromCompilerUri - -```solidity -function getPublishedUriFromCompilerUri(string compilerMetadataUri) external view returns (string[] publishedMetadataUris) -``` - -Retrieve the published metadata URI from a compiler metadata URI - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| compilerMetadataUri | string | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| publishedMetadataUris | string[] | undefined - -### getPublisherProfileUri - -```solidity -function getPublisherProfileUri(address publisher) external view returns (string uri) -``` - -get the publisher profile uri - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| publisher | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| uri | string | undefined - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleMember - -```solidity -function getRoleMember(bytes32 role, uint256 index) external view returns (address) -``` - - - -*Returns one of the accounts that have `role`. `index` must be a value between 0 and {getRoleMemberCount}, non-inclusive. Role bearers are not sorted in any particular way, and their ordering may change at any point. WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure you perform all queries on the same block. See the following https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] for more information.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getRoleMemberCount - -```solidity -function getRoleMemberCount(bytes32 role) external view returns (uint256) -``` - - - -*Returns the number of accounts that have `role`. Can be used together with {getRoleMember} to enumerate all bearers of a role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### isPaused - -```solidity -function isPaused() external view returns (bool) -``` - - - -*Whether the registry is paused.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### isTrustedForwarder - -```solidity -function isTrustedForwarder(address forwarder) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| forwarder | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### multicall - -```solidity -function multicall(bytes[] data) external nonpayable returns (bytes[] results) -``` - - - -*Receives and executes a batch of function calls on this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| results | bytes[] | undefined - -### publishContract - -```solidity -function publishContract(address _publisher, string _contractId, string _publishMetadataUri, string _compilerMetadataUri, bytes32 _bytecodeHash, address _implementation) external nonpayable -``` - -Let's an account publish a contract. The account must be approved by the publisher, or be the publisher. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _publisher | address | undefined -| _contractId | string | undefined -| _publishMetadataUri | string | undefined -| _compilerMetadataUri | string | undefined -| _bytecodeHash | bytes32 | undefined -| _implementation | address | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been revoked `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### setPause - -```solidity -function setPause(bool _pause) external nonpayable -``` - - - -*Lets a contract admin pause the registry.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _pause | bool | undefined - -### setPublisherProfileUri - -```solidity -function setPublisherProfileUri(address publisher, string uri) external nonpayable -``` - -Lets an account set its own publisher profile uri - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| publisher | address | undefined -| uri | string | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### unpublishContract - -```solidity -function unpublishContract(address _publisher, string _contractId) external nonpayable -``` - -Lets an account unpublish a contract and all its versions. The account must be approved by the publisher, or be the publisher. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _publisher | address | undefined -| _contractId | string | undefined - - - -## Events - -### ContractPublished - -```solidity -event ContractPublished(address indexed operator, address indexed publisher, IContractPublisher.CustomContractInstance publishedContract) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| publisher `indexed` | address | undefined | -| publishedContract | IContractPublisher.CustomContractInstance | undefined | - -### ContractUnpublished - -```solidity -event ContractUnpublished(address indexed operator, address indexed publisher, string indexed contractId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| publisher `indexed` | address | undefined | -| contractId `indexed` | string | undefined | - -### Paused - -```solidity -event Paused(bool isPaused) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| isPaused | bool | undefined | - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - - - diff --git a/docs/CountersUpgradeable.md b/docs/CountersUpgradeable.md deleted file mode 100644 index 62599cad1..000000000 --- a/docs/CountersUpgradeable.md +++ /dev/null @@ -1,12 +0,0 @@ -# CountersUpgradeable - -*Matt Condon (@shrugs)* - -> Counters - - - -*Provides counters that can only be incremented, decremented or reset. This can be used e.g. to track the number of elements in a mapping, issuing ERC721 ids, or counting request ids. Include with `using Counters for Counters.Counter;`* - - - diff --git a/docs/Create2.md b/docs/Create2.md deleted file mode 100644 index f51a139ba..000000000 --- a/docs/Create2.md +++ /dev/null @@ -1,12 +0,0 @@ -# Create2 - - - - - - - -*Helper to make usage of the `CREATE2` EVM opcode easier and safer. `CREATE2` can be used to compute in advance the address where a smart contract will be deployed, which allows for interesting new mechanisms known as 'counterfactual interactions'. See the https://eips.ethereum.org/EIPS/eip-1014#motivation[EIP] for more information.* - - - diff --git a/docs/CurrencyTransferLib.md b/docs/CurrencyTransferLib.md deleted file mode 100644 index 29c861563..000000000 --- a/docs/CurrencyTransferLib.md +++ /dev/null @@ -1,32 +0,0 @@ -# CurrencyTransferLib - - - - - - - - - -## Methods - -### NATIVE_TOKEN - -```solidity -function NATIVE_TOKEN() external view returns (address) -``` - - - -*The address interpreted as native token of the chain.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - - - - diff --git a/docs/DelayedReveal.md b/docs/DelayedReveal.md deleted file mode 100644 index eca08e675..000000000 --- a/docs/DelayedReveal.md +++ /dev/null @@ -1,148 +0,0 @@ -# DelayedReveal - - - - - -Thirdweb's `DelayedReveal` is a contract extension for base NFT contracts. It lets you create batches of 'delayed-reveal' NFTs. You can learn more about the usage of delayed reveal NFTs here - https://blog.thirdweb.com/delayed-reveal-nfts - - - -## Methods - -### encryptDecrypt - -```solidity -function encryptDecrypt(bytes data, bytes key) external pure returns (bytes result) -``` - - - -*See: https://ethereum.stackexchange.com/questions/69825/decrypt-message-on-chain* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes | undefined -| key | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| result | bytes | undefined - -### encryptedBaseURI - -```solidity -function encryptedBaseURI(uint256) external view returns (bytes) -``` - - - -*Mapping from id of a batch of tokens => to encrypted base URI for the respective batch of tokens.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes | undefined - -### getRevealURI - -```solidity -function getRevealURI(uint256 _batchId, bytes _key) external view returns (string revealedURI) -``` - - - -*Returns the decrypted i.e. revealed URI for a batch of tokens.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _batchId | uint256 | undefined -| _key | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| revealedURI | string | undefined - -### isEncryptedBatch - -```solidity -function isEncryptedBatch(uint256 _batchId) external view returns (bool) -``` - - - -*Returns whether the relvant batch of NFTs is subject to a delayed reveal.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _batchId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### reveal - -```solidity -function reveal(uint256 identifier, bytes key) external nonpayable returns (string revealedURI) -``` - -Reveals a batch of delayed reveal NFTs. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| identifier | uint256 | The ID for the batch of delayed-reveal NFTs to reveal. -| key | bytes | The key with which the base URI for the relevant batch of NFTs was encrypted. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| revealedURI | string | undefined - - - -## Events - -### TokenURIRevealed - -```solidity -event TokenURIRevealed(uint256 indexed index, string revealedURI) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| index `indexed` | uint256 | undefined | -| revealedURI | string | undefined | - - - diff --git a/docs/Drop.md b/docs/Drop.md deleted file mode 100644 index 0293c0695..000000000 --- a/docs/Drop.md +++ /dev/null @@ -1,355 +0,0 @@ -# Drop - - - - - - - - - -## Methods - -### claim - -```solidity -function claim(address _receiver, uint256 _quantity, address _currency, uint256 _pricePerToken, IDrop.AllowlistProof _allowlistProof, bytes _data) external payable -``` - - - -*Lets an account claim tokens.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _receiver | address | undefined -| _quantity | uint256 | undefined -| _currency | address | undefined -| _pricePerToken | uint256 | undefined -| _allowlistProof | IDrop.AllowlistProof | undefined -| _data | bytes | undefined - -### claimCondition - -```solidity -function claimCondition() external view returns (uint256 currentStartId, uint256 count) -``` - - - -*The active conditions for claiming tokens.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| currentStartId | uint256 | undefined -| count | uint256 | undefined - -### getActiveClaimConditionId - -```solidity -function getActiveClaimConditionId() external view returns (uint256) -``` - - - -*At any given moment, returns the uid for the active claim condition.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getClaimConditionById - -```solidity -function getClaimConditionById(uint256 _conditionId) external view returns (struct IClaimCondition.ClaimCondition condition) -``` - - - -*Returns the claim condition at the given uid.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _conditionId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| condition | IClaimCondition.ClaimCondition | undefined - -### getClaimTimestamp - -```solidity -function getClaimTimestamp(uint256 _conditionId, address _claimer) external view returns (uint256 lastClaimTimestamp, uint256 nextValidClaimTimestamp) -``` - - - -*Returns the timestamp for when a claimer is eligible for claiming NFTs again.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _conditionId | uint256 | undefined -| _claimer | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| lastClaimTimestamp | uint256 | undefined -| nextValidClaimTimestamp | uint256 | undefined - -### setClaimConditions - -```solidity -function setClaimConditions(IClaimCondition.ClaimCondition[] _conditions, bool _resetClaimEligibility) external nonpayable -``` - - - -*Lets a contract admin set claim conditions.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _conditions | IClaimCondition.ClaimCondition[] | undefined -| _resetClaimEligibility | bool | undefined - -### verifyClaim - -```solidity -function verifyClaim(uint256 _conditionId, address _claimer, uint256 _quantity, address _currency, uint256 _pricePerToken, bool verifyMaxQuantityPerTransaction) external view -``` - - - -*Checks a request to claim NFTs against the active claim condition's criteria.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _conditionId | uint256 | undefined -| _claimer | address | undefined -| _quantity | uint256 | undefined -| _currency | address | undefined -| _pricePerToken | uint256 | undefined -| verifyMaxQuantityPerTransaction | bool | undefined - -### verifyClaimMerkleProof - -```solidity -function verifyClaimMerkleProof(uint256 _conditionId, address _claimer, uint256 _quantity, IDrop.AllowlistProof _allowlistProof) external view returns (bool validMerkleProof, uint256 merkleProofIndex) -``` - - - -*Checks whether a claimer meets the claim condition's allowlist criteria.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _conditionId | uint256 | undefined -| _claimer | address | undefined -| _quantity | uint256 | undefined -| _allowlistProof | IDrop.AllowlistProof | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| validMerkleProof | bool | undefined -| merkleProofIndex | uint256 | undefined - - - -## Events - -### ClaimConditionsUpdated - -```solidity -event ClaimConditionsUpdated(IClaimCondition.ClaimCondition[] claimConditions, bool resetEligibility) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| claimConditions | IClaimCondition.ClaimCondition[] | undefined | -| resetEligibility | bool | undefined | - -### TokensClaimed - -```solidity -event TokensClaimed(uint256 indexed claimConditionIndex, address indexed claimer, address indexed receiver, uint256 startTokenId, uint256 quantityClaimed) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| claimConditionIndex `indexed` | uint256 | undefined | -| claimer `indexed` | address | undefined | -| receiver `indexed` | address | undefined | -| startTokenId | uint256 | undefined | -| quantityClaimed | uint256 | undefined | - - - -## Errors - -### Drop__CannotClaimYet - -```solidity -error Drop__CannotClaimYet(uint256 blockTimestamp, uint256 startTimestamp, uint256 lastClaimedAt, uint256 nextValidClaimTimestamp) -``` - -Emitted when the current timestamp is invalid for claim. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| blockTimestamp | uint256 | undefined | -| startTimestamp | uint256 | undefined | -| lastClaimedAt | uint256 | undefined | -| nextValidClaimTimestamp | uint256 | undefined | - -### Drop__ExceedMaxClaimableSupply - -```solidity -error Drop__ExceedMaxClaimableSupply(uint256 supplyClaimed, uint256 maxClaimableSupply) -``` - -Emitted when claiming given quantity will exceed max claimable supply. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| supplyClaimed | uint256 | undefined | -| maxClaimableSupply | uint256 | undefined | - -### Drop__InvalidCurrencyOrPrice - -```solidity -error Drop__InvalidCurrencyOrPrice(address givenCurrency, address requiredCurrency, uint256 givenPricePerToken, uint256 requiredPricePerToken) -``` - -Emitted when given currency or price is invalid. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| givenCurrency | address | undefined | -| requiredCurrency | address | undefined | -| givenPricePerToken | uint256 | undefined | -| requiredPricePerToken | uint256 | undefined | - -### Drop__InvalidQuantity - -```solidity -error Drop__InvalidQuantity() -``` - -Emitted when claiming invalid quantity of tokens. - - - - -### Drop__InvalidQuantityProof - -```solidity -error Drop__InvalidQuantityProof(uint256 maxQuantityInAllowlist) -``` - -Emitted when claiming more than allowed quantity in allowlist. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| maxQuantityInAllowlist | uint256 | undefined | - -### Drop__MaxSupplyClaimedAlready - -```solidity -error Drop__MaxSupplyClaimedAlready(uint256 supplyClaimedAlready) -``` - -Emitted when max claimable supply in given condition is less than supply claimed already. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| supplyClaimedAlready | uint256 | undefined | - -### Drop__NotAuthorized - -```solidity -error Drop__NotAuthorized() -``` - - - -*Emitted when an unauthorized caller tries to set claim conditions.* - - -### Drop__NotInWhitelist - -```solidity -error Drop__NotInWhitelist() -``` - -Emitted when given allowlist proof is invalid. - - - - -### Drop__ProofClaimed - -```solidity -error Drop__ProofClaimed() -``` - -Emitted when allowlist spot is already used. - - - - - diff --git a/docs/DropERC1155.md b/docs/DropERC1155.md deleted file mode 100644 index 7e3722aac..000000000 --- a/docs/DropERC1155.md +++ /dev/null @@ -1,1488 +0,0 @@ -# DropERC1155 - - - - - - - - - -## Methods - -### DEFAULT_ADMIN_ROLE - -```solidity -function DEFAULT_ADMIN_ROLE() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### balanceOf - -```solidity -function balanceOf(address account, uint256 id) external view returns (uint256) -``` - - - -*See {IERC1155-balanceOf}. Requirements: - `account` cannot be the zero address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| id | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### balanceOfBatch - -```solidity -function balanceOfBatch(address[] accounts, uint256[] ids) external view returns (uint256[]) -``` - - - -*See {IERC1155-balanceOfBatch}. Requirements: - `accounts` and `ids` must have the same length.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| accounts | address[] | undefined -| ids | uint256[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256[] | undefined - -### burn - -```solidity -function burn(address account, uint256 id, uint256 value) external nonpayable -``` - - - -*Lets a token owner burn the tokens they own (i.e. destroy for good)* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| id | uint256 | undefined -| value | uint256 | undefined - -### burnBatch - -```solidity -function burnBatch(address account, uint256[] ids, uint256[] values) external nonpayable -``` - - - -*Lets a token owner burn multiple tokens they own at once (i.e. destroy for good)* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| ids | uint256[] | undefined -| values | uint256[] | undefined - -### claim - -```solidity -function claim(address _receiver, uint256 _tokenId, uint256 _quantity, address _currency, uint256 _pricePerToken, bytes32[] _proofs, uint256 _proofMaxQuantityPerTransaction) external payable -``` - - - -*Lets an account claim a given quantity of NFTs, of a single tokenId.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _receiver | address | undefined -| _tokenId | uint256 | undefined -| _quantity | uint256 | undefined -| _currency | address | undefined -| _pricePerToken | uint256 | undefined -| _proofs | bytes32[] | undefined -| _proofMaxQuantityPerTransaction | uint256 | undefined - -### claimCondition - -```solidity -function claimCondition(uint256) external view returns (uint256 currentStartId, uint256 count) -``` - - - -*Mapping from token ID => the set of all claim conditions, at any given moment, for tokens of the token ID.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| currentStartId | uint256 | undefined -| count | uint256 | undefined - -### contractType - -```solidity -function contractType() external pure returns (bytes32) -``` - - - -*Returns the type of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### contractURI - -```solidity -function contractURI() external view returns (string) -``` - - - -*Contract level metadata.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### contractVersion - -```solidity -function contractVersion() external pure returns (uint8) -``` - - - -*Returns the version of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint8 | undefined - -### getActiveClaimConditionId - -```solidity -function getActiveClaimConditionId(uint256 _tokenId) external view returns (uint256) -``` - - - -*At any given moment, returns the uid for the active claim condition, for a given tokenId.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getClaimConditionById - -```solidity -function getClaimConditionById(uint256 _tokenId, uint256 _conditionId) external view returns (struct IDropClaimCondition.ClaimCondition condition) -``` - - - -*Returns the claim condition at the given uid.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined -| _conditionId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| condition | IDropClaimCondition.ClaimCondition | undefined - -### getClaimTimestamp - -```solidity -function getClaimTimestamp(uint256 _tokenId, uint256 _conditionId, address _claimer) external view returns (uint256 lastClaimTimestamp, uint256 nextValidClaimTimestamp) -``` - - - -*Returns the timestamp for when a claimer is eligible for claiming NFTs again.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined -| _conditionId | uint256 | undefined -| _claimer | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| lastClaimTimestamp | uint256 | undefined -| nextValidClaimTimestamp | uint256 | undefined - -### getDefaultRoyaltyInfo - -```solidity -function getDefaultRoyaltyInfo() external view returns (address, uint16) -``` - - - -*Returns the default royalty recipient and bps.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### getPlatformFeeInfo - -```solidity -function getPlatformFeeInfo() external view returns (address, uint16) -``` - - - -*Returns the platform fee recipient and bps.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleMember - -```solidity -function getRoleMember(bytes32 role, uint256 index) external view returns (address) -``` - - - -*Returns one of the accounts that have `role`. `index` must be a value between 0 and {getRoleMemberCount}, non-inclusive. Role bearers are not sorted in any particular way, and their ordering may change at any point. WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure you perform all queries on the same block. See the following https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] for more information.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getRoleMemberCount - -```solidity -function getRoleMemberCount(bytes32 role) external view returns (uint256) -``` - - - -*Returns the number of accounts that have `role`. Can be used together with {getRoleMember} to enumerate all bearers of a role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getRoyaltyInfoForToken - -```solidity -function getRoyaltyInfoForToken(uint256 _tokenId) external view returns (address, uint16) -``` - - - -*Returns the royalty recipient and bps for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### initialize - -```solidity -function initialize(address _defaultAdmin, string _name, string _symbol, string _contractURI, address[] _trustedForwarders, address _saleRecipient, address _royaltyRecipient, uint128 _royaltyBps, uint128 _platformFeeBps, address _platformFeeRecipient) external nonpayable -``` - - - -*Initiliazes the contract, like a constructor.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _defaultAdmin | address | undefined -| _name | string | undefined -| _symbol | string | undefined -| _contractURI | string | undefined -| _trustedForwarders | address[] | undefined -| _saleRecipient | address | undefined -| _royaltyRecipient | address | undefined -| _royaltyBps | uint128 | undefined -| _platformFeeBps | uint128 | undefined -| _platformFeeRecipient | address | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address account, address operator) external view returns (bool) -``` - - - -*See {IERC1155-isApprovedForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### isTrustedForwarder - -```solidity -function isTrustedForwarder(address forwarder) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| forwarder | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### lazyMint - -```solidity -function lazyMint(uint256 _amount, string _baseURIForTokens) external nonpayable -``` - - - -*Lets an account with `MINTER_ROLE` lazy mint 'n' NFTs. The URIs for each token is the provided `_baseURIForTokens` + `{tokenId}`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _amount | uint256 | undefined -| _baseURIForTokens | string | undefined - -### maxTotalSupply - -```solidity -function maxTotalSupply(uint256) external view returns (uint256) -``` - - - -*Mapping from token ID => maximum possible total circulating supply of tokens with that ID.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### maxWalletClaimCount - -```solidity -function maxWalletClaimCount(uint256) external view returns (uint256) -``` - - - -*Mapping from token ID => the max number of NFTs of the token ID a wallet can claim.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### multicall - -```solidity -function multicall(bytes[] data) external nonpayable returns (bytes[] results) -``` - - - -*Receives and executes a batch of function calls on this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| results | bytes[] | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### nextTokenIdToMint - -```solidity -function nextTokenIdToMint() external view returns (uint256) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### owner - -```solidity -function owner() external view returns (address) -``` - - - -*Returns the address of the current owner.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### primarySaleRecipient - -```solidity -function primarySaleRecipient() external view returns (address) -``` - - - -*The address that receives all primary sales value.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been revoked `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### royaltyInfo - -```solidity -function royaltyInfo(uint256 tokenId, uint256 salePrice) external view returns (address receiver, uint256 royaltyAmount) -``` - - - -*Returns the royalty recipient and amount, given a tokenId and sale price.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined -| salePrice | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| receiver | address | undefined -| royaltyAmount | uint256 | undefined - -### safeBatchTransferFrom - -```solidity -function safeBatchTransferFrom(address from, address to, uint256[] ids, uint256[] amounts, bytes data) external nonpayable -``` - - - -*See {IERC1155-safeBatchTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| ids | uint256[] | undefined -| amounts | uint256[] | undefined -| data | bytes | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data) external nonpayable -``` - - - -*See {IERC1155-safeTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| id | uint256 | undefined -| amount | uint256 | undefined -| data | bytes | undefined - -### saleRecipient - -```solidity -function saleRecipient(uint256) external view returns (address) -``` - - - -*Mapping from token ID => the address of the recipient of primary sales.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool approved) external nonpayable -``` - - - -*See {IERC1155-setApprovalForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| approved | bool | undefined - -### setClaimConditions - -```solidity -function setClaimConditions(uint256 _tokenId, IDropClaimCondition.ClaimCondition[] _phases, bool _resetClaimEligibility) external nonpayable -``` - - - -*Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions, for a tokenId.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined -| _phases | IDropClaimCondition.ClaimCondition[] | undefined -| _resetClaimEligibility | bool | undefined - -### setContractURI - -```solidity -function setContractURI(string _uri) external nonpayable -``` - - - -*Lets a contract admin set the URI for contract-level metadata.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _uri | string | undefined - -### setDefaultRoyaltyInfo - -```solidity -function setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) external nonpayable -``` - - - -*Lets a contract admin update the default royalty recipient and bps.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _royaltyRecipient | address | undefined -| _royaltyBps | uint256 | undefined - -### setMaxTotalSupply - -```solidity -function setMaxTotalSupply(uint256 _tokenId, uint256 _maxTotalSupply) external nonpayable -``` - - - -*Lets a module admin set a max total supply for token.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined -| _maxTotalSupply | uint256 | undefined - -### setMaxWalletClaimCount - -```solidity -function setMaxWalletClaimCount(uint256 _tokenId, uint256 _count) external nonpayable -``` - - - -*Lets a contract admin set a maximum number of NFTs of a tokenId that can be claimed by any wallet.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined -| _count | uint256 | undefined - -### setOwner - -```solidity -function setOwner(address _newOwner) external nonpayable -``` - - - -*Lets a contract admin set a new owner for the contract. The new owner must be a contract admin.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _newOwner | address | undefined - -### setPlatformFeeInfo - -```solidity -function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) external nonpayable -``` - - - -*Lets a contract admin update the platform fee recipient and bps* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _platformFeeRecipient | address | undefined -| _platformFeeBps | uint256 | undefined - -### setPrimarySaleRecipient - -```solidity -function setPrimarySaleRecipient(address _saleRecipient) external nonpayable -``` - - - -*Lets a contract admin set the recipient for all primary sales.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _saleRecipient | address | undefined - -### setRoyaltyInfoForToken - -```solidity -function setRoyaltyInfoForToken(uint256 _tokenId, address _recipient, uint256 _bps) external nonpayable -``` - - - -*Lets a contract admin set the royalty recipient and bps for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined -| _recipient | address | undefined -| _bps | uint256 | undefined - -### setSaleRecipientForToken - -```solidity -function setSaleRecipientForToken(uint256 _tokenId, address _saleRecipient) external nonpayable -``` - - - -*Lets a contract admin set the recipient for all primary sales.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined -| _saleRecipient | address | undefined - -### setWalletClaimCount - -```solidity -function setWalletClaimCount(uint256 _tokenId, address _claimer, uint256 _count) external nonpayable -``` - - - -*Lets a contract admin set a claim count for a wallet.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined -| _claimer | address | undefined -| _count | uint256 | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See ERC 165* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### totalSupply - -```solidity -function totalSupply(uint256) external view returns (uint256) -``` - - - -*Mapping from token ID => total circulating supply of tokens with that ID.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### uri - -```solidity -function uri(uint256 _tokenId) external view returns (string _tokenURI) -``` - - - -*Returns the URI for a given tokenId.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _tokenURI | string | undefined - -### verifyClaim - -```solidity -function verifyClaim(uint256 _conditionId, address _claimer, uint256 _tokenId, uint256 _quantity, address _currency, uint256 _pricePerToken, bool verifyMaxQuantityPerTransaction) external view -``` - - - -*Checks a request to claim NFTs against the active claim condition's criteria.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _conditionId | uint256 | undefined -| _claimer | address | undefined -| _tokenId | uint256 | undefined -| _quantity | uint256 | undefined -| _currency | address | undefined -| _pricePerToken | uint256 | undefined -| verifyMaxQuantityPerTransaction | bool | undefined - -### verifyClaimMerkleProof - -```solidity -function verifyClaimMerkleProof(uint256 _conditionId, address _claimer, uint256 _tokenId, uint256 _quantity, bytes32[] _proofs, uint256 _proofMaxQuantityPerTransaction) external view returns (bool validMerkleProof, uint256 merkleProofIndex) -``` - - - -*Checks whether a claimer meets the claim condition's allowlist criteria.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _conditionId | uint256 | undefined -| _claimer | address | undefined -| _tokenId | uint256 | undefined -| _quantity | uint256 | undefined -| _proofs | bytes32[] | undefined -| _proofMaxQuantityPerTransaction | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| validMerkleProof | bool | undefined -| merkleProofIndex | uint256 | undefined - -### walletClaimCount - -```solidity -function walletClaimCount(uint256, address) external view returns (uint256) -``` - - - -*Mapping from token ID => claimer wallet address => total number of NFTs of the token ID a wallet has claimed.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined -| _1 | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - - - -## Events - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed account, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### ClaimConditionsUpdated - -```solidity -event ClaimConditionsUpdated(uint256 indexed tokenId, IDropClaimCondition.ClaimCondition[] claimConditions) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId `indexed` | uint256 | undefined | -| claimConditions | IDropClaimCondition.ClaimCondition[] | undefined | - -### DefaultRoyalty - -```solidity -event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| newRoyaltyRecipient `indexed` | address | undefined | -| newRoyaltyBps | uint256 | undefined | - -### MaxTotalSupplyUpdated - -```solidity -event MaxTotalSupplyUpdated(uint256 tokenId, uint256 maxTotalSupply) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined | -| maxTotalSupply | uint256 | undefined | - -### MaxWalletClaimCountUpdated - -```solidity -event MaxWalletClaimCountUpdated(uint256 tokenId, uint256 count) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined | -| count | uint256 | undefined | - -### OwnerUpdated - -```solidity -event OwnerUpdated(address indexed prevOwner, address indexed newOwner) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| prevOwner `indexed` | address | undefined | -| newOwner `indexed` | address | undefined | - -### PlatformFeeInfoUpdated - -```solidity -event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| platformFeeRecipient `indexed` | address | undefined | -| platformFeeBps | uint256 | undefined | - -### PrimarySaleRecipientUpdated - -```solidity -event PrimarySaleRecipientUpdated(address indexed recipient) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| recipient `indexed` | address | undefined | - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoyaltyForToken - -```solidity -event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, uint256 royaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId `indexed` | uint256 | undefined | -| royaltyRecipient `indexed` | address | undefined | -| royaltyBps | uint256 | undefined | - -### SaleRecipientForTokenUpdated - -```solidity -event SaleRecipientForTokenUpdated(uint256 indexed tokenId, address saleRecipient) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId `indexed` | uint256 | undefined | -| saleRecipient | address | undefined | - -### TokensClaimed - -```solidity -event TokensClaimed(uint256 indexed claimConditionIndex, uint256 indexed tokenId, address indexed claimer, address receiver, uint256 quantityClaimed) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| claimConditionIndex `indexed` | uint256 | undefined | -| tokenId `indexed` | uint256 | undefined | -| claimer `indexed` | address | undefined | -| receiver | address | undefined | -| quantityClaimed | uint256 | undefined | - -### TokensLazyMinted - -```solidity -event TokensLazyMinted(uint256 startTokenId, uint256 endTokenId, string baseURI) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| startTokenId | uint256 | undefined | -| endTokenId | uint256 | undefined | -| baseURI | string | undefined | - -### TransferBatch - -```solidity -event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| ids | uint256[] | undefined | -| values | uint256[] | undefined | - -### TransferSingle - -```solidity -event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| id | uint256 | undefined | -| value | uint256 | undefined | - -### URI - -```solidity -event URI(string value, uint256 indexed id) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| value | string | undefined | -| id `indexed` | uint256 | undefined | - -### WalletClaimCountUpdated - -```solidity -event WalletClaimCountUpdated(uint256 tokenId, address indexed wallet, uint256 count) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined | -| wallet `indexed` | address | undefined | -| count | uint256 | undefined | - - - diff --git a/docs/DropERC20.md b/docs/DropERC20.md deleted file mode 100644 index 5962a5173..000000000 --- a/docs/DropERC20.md +++ /dev/null @@ -1,1409 +0,0 @@ -# DropERC20 - - - - - - - - - -## Methods - -### DEFAULT_ADMIN_ROLE - -```solidity -function DEFAULT_ADMIN_ROLE() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### DOMAIN_SEPARATOR - -```solidity -function DOMAIN_SEPARATOR() external view returns (bytes32) -``` - - - -*See {IERC20Permit-DOMAIN_SEPARATOR}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### allowance - -```solidity -function allowance(address owner, address spender) external view returns (uint256) -``` - - - -*See {IERC20-allowance}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| spender | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### approve - -```solidity -function approve(address spender, uint256 amount) external nonpayable returns (bool) -``` - - - -*See {IERC20-approve}. NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on `transferFrom`. This is semantically equivalent to an infinite approval. Requirements: - `spender` cannot be the zero address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### balanceOf - -```solidity -function balanceOf(address account) external view returns (uint256) -``` - - - -*See {IERC20-balanceOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### burn - -```solidity -function burn(uint256 amount) external nonpayable -``` - - - -*Destroys `amount` tokens from the caller. See {ERC20-_burn}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| amount | uint256 | undefined - -### burnFrom - -```solidity -function burnFrom(address account, uint256 amount) external nonpayable -``` - - - -*Destroys `amount` tokens from `account`, deducting from the caller's allowance. See {ERC20-_burn} and {ERC20-allowance}. Requirements: - the caller must have allowance for ``accounts``'s tokens of at least `amount`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| amount | uint256 | undefined - -### checkpoints - -```solidity -function checkpoints(address account, uint32 pos) external view returns (struct ERC20VotesUpgradeable.Checkpoint) -``` - - - -*Get the `pos`-th checkpoint for `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| pos | uint32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | ERC20VotesUpgradeable.Checkpoint | undefined - -### claim - -```solidity -function claim(address _receiver, uint256 _quantity, address _currency, uint256 _pricePerToken, bytes32[] _proofs, uint256 _proofMaxQuantityPerTransaction) external payable -``` - - - -*Lets an account claim tokens.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _receiver | address | undefined -| _quantity | uint256 | undefined -| _currency | address | undefined -| _pricePerToken | uint256 | undefined -| _proofs | bytes32[] | undefined -| _proofMaxQuantityPerTransaction | uint256 | undefined - -### claimCondition - -```solidity -function claimCondition() external view returns (uint256 currentStartId, uint256 count) -``` - - - -*The set of all claim conditions, at any given moment.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| currentStartId | uint256 | undefined -| count | uint256 | undefined - -### contractType - -```solidity -function contractType() external pure returns (bytes32) -``` - - - -*Returns the type of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### contractURI - -```solidity -function contractURI() external view returns (string) -``` - - - -*Contract level metadata.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### contractVersion - -```solidity -function contractVersion() external pure returns (uint8) -``` - - - -*Returns the version of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint8 | undefined - -### decimals - -```solidity -function decimals() external view returns (uint8) -``` - - - -*Returns the number of decimals used to get its user representation. For example, if `decimals` equals `2`, a balance of `505` tokens should be displayed to a user as `5.05` (`505 / 10 ** 2`). Tokens usually opt for a value of 18, imitating the relationship between Ether and Wei. This is the value {ERC20} uses, unless this function is overridden; NOTE: This information is only used for _display_ purposes: it in no way affects any of the arithmetic of the contract, including {IERC20-balanceOf} and {IERC20-transfer}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint8 | undefined - -### decreaseAllowance - -```solidity -function decreaseAllowance(address spender, uint256 subtractedValue) external nonpayable returns (bool) -``` - - - -*Atomically decreases the allowance granted to `spender` by the caller. This is an alternative to {approve} that can be used as a mitigation for problems described in {IERC20-approve}. Emits an {Approval} event indicating the updated allowance. Requirements: - `spender` cannot be the zero address. - `spender` must have allowance for the caller of at least `subtractedValue`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| subtractedValue | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### delegate - -```solidity -function delegate(address delegatee) external nonpayable -``` - - - -*Delegate votes from the sender to `delegatee`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| delegatee | address | undefined - -### delegateBySig - -```solidity -function delegateBySig(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) external nonpayable -``` - - - -*Delegates votes from signer to `delegatee`* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| delegatee | address | undefined -| nonce | uint256 | undefined -| expiry | uint256 | undefined -| v | uint8 | undefined -| r | bytes32 | undefined -| s | bytes32 | undefined - -### delegates - -```solidity -function delegates(address account) external view returns (address) -``` - - - -*Get the address `account` is currently delegating to.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getActiveClaimConditionId - -```solidity -function getActiveClaimConditionId() external view returns (uint256) -``` - - - -*At any given moment, returns the uid for the active claim condition.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getClaimConditionById - -```solidity -function getClaimConditionById(uint256 _conditionId) external view returns (struct IDropClaimCondition.ClaimCondition condition) -``` - - - -*Returns the claim condition at the given uid.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _conditionId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| condition | IDropClaimCondition.ClaimCondition | undefined - -### getClaimTimestamp - -```solidity -function getClaimTimestamp(uint256 _conditionId, address _claimer) external view returns (uint256 lastClaimTimestamp, uint256 nextValidClaimTimestamp) -``` - - - -*Returns the timestamp for when a claimer is eligible for claiming tokens again.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _conditionId | uint256 | undefined -| _claimer | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| lastClaimTimestamp | uint256 | undefined -| nextValidClaimTimestamp | uint256 | undefined - -### getPastTotalSupply - -```solidity -function getPastTotalSupply(uint256 blockNumber) external view returns (uint256) -``` - - - -*Retrieve the `totalSupply` at the end of `blockNumber`. Note, this value is the sum of all balances. It is but NOT the sum of all the delegated votes! Requirements: - `blockNumber` must have been already mined* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| blockNumber | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getPastVotes - -```solidity -function getPastVotes(address account, uint256 blockNumber) external view returns (uint256) -``` - - - -*Retrieve the number of votes for `account` at the end of `blockNumber`. Requirements: - `blockNumber` must have been already mined* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| blockNumber | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getPlatformFeeInfo - -```solidity -function getPlatformFeeInfo() external view returns (address, uint16) -``` - - - -*Returns the platform fee recipient and bps.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleMember - -```solidity -function getRoleMember(bytes32 role, uint256 index) external view returns (address) -``` - - - -*Returns one of the accounts that have `role`. `index` must be a value between 0 and {getRoleMemberCount}, non-inclusive. Role bearers are not sorted in any particular way, and their ordering may change at any point. WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure you perform all queries on the same block. See the following https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] for more information.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getRoleMemberCount - -```solidity -function getRoleMemberCount(bytes32 role) external view returns (uint256) -``` - - - -*Returns the number of accounts that have `role`. Can be used together with {getRoleMember} to enumerate all bearers of a role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getVotes - -```solidity -function getVotes(address account) external view returns (uint256) -``` - - - -*Gets the current votes balance for `account`* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### increaseAllowance - -```solidity -function increaseAllowance(address spender, uint256 addedValue) external nonpayable returns (bool) -``` - - - -*Atomically increases the allowance granted to `spender` by the caller. This is an alternative to {approve} that can be used as a mitigation for problems described in {IERC20-approve}. Emits an {Approval} event indicating the updated allowance. Requirements: - `spender` cannot be the zero address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| addedValue | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### initialize - -```solidity -function initialize(address _defaultAdmin, string _name, string _symbol, string _contractURI, address[] _trustedForwarders, address _primarySaleRecipient, address _platformFeeRecipient, uint256 _platformFeeBps) external nonpayable -``` - - - -*Initiliazes the contract, like a constructor.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _defaultAdmin | address | undefined -| _name | string | undefined -| _symbol | string | undefined -| _contractURI | string | undefined -| _trustedForwarders | address[] | undefined -| _primarySaleRecipient | address | undefined -| _platformFeeRecipient | address | undefined -| _platformFeeBps | uint256 | undefined - -### isTrustedForwarder - -```solidity -function isTrustedForwarder(address forwarder) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| forwarder | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### maxTotalSupply - -```solidity -function maxTotalSupply() external view returns (uint256) -``` - - - -*Global max total supply of tokens.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### maxWalletClaimCount - -```solidity -function maxWalletClaimCount() external view returns (uint256) -``` - - - -*The max number of tokens a wallet can claim.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### multicall - -```solidity -function multicall(bytes[] data) external nonpayable returns (bytes[] results) -``` - - - -*Receives and executes a batch of function calls on this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| results | bytes[] | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*Returns the name of the token.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### nonces - -```solidity -function nonces(address owner) external view returns (uint256) -``` - - - -*See {IERC20Permit-nonces}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### numCheckpoints - -```solidity -function numCheckpoints(address account) external view returns (uint32) -``` - - - -*Get number of checkpoints for `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint32 | undefined - -### permit - -```solidity -function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external nonpayable -``` - - - -*See {IERC20Permit-permit}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| spender | address | undefined -| value | uint256 | undefined -| deadline | uint256 | undefined -| v | uint8 | undefined -| r | bytes32 | undefined -| s | bytes32 | undefined - -### primarySaleRecipient - -```solidity -function primarySaleRecipient() external view returns (address) -``` - - - -*The address that receives all primary sales value.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been revoked `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### setClaimConditions - -```solidity -function setClaimConditions(IDropClaimCondition.ClaimCondition[] _phases, bool _resetClaimEligibility) external nonpayable -``` - - - -*Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _phases | IDropClaimCondition.ClaimCondition[] | undefined -| _resetClaimEligibility | bool | undefined - -### setContractURI - -```solidity -function setContractURI(string _uri) external nonpayable -``` - - - -*Lets a contract admin set the URI for contract-level metadata.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _uri | string | undefined - -### setMaxTotalSupply - -```solidity -function setMaxTotalSupply(uint256 _maxTotalSupply) external nonpayable -``` - - - -*Lets a contract admin set the global maximum supply of tokens.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _maxTotalSupply | uint256 | undefined - -### setMaxWalletClaimCount - -```solidity -function setMaxWalletClaimCount(uint256 _count) external nonpayable -``` - - - -*Lets a contract admin set a maximum number of tokens that can be claimed by any wallet.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _count | uint256 | undefined - -### setPlatformFeeInfo - -```solidity -function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) external nonpayable -``` - - - -*Lets a contract admin update the platform fee recipient and bps* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _platformFeeRecipient | address | undefined -| _platformFeeBps | uint256 | undefined - -### setPrimarySaleRecipient - -```solidity -function setPrimarySaleRecipient(address _saleRecipient) external nonpayable -``` - - - -*Lets a contract admin set the recipient for all primary sales.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _saleRecipient | address | undefined - -### setWalletClaimCount - -```solidity -function setWalletClaimCount(address _claimer, uint256 _count) external nonpayable -``` - - - -*Lets a contract admin set a claim count for a wallet.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _claimer | address | undefined -| _count | uint256 | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See ERC 165* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - -*Returns the symbol of the token, usually a shorter version of the name.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - -*See {IERC20-totalSupply}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transfer - -```solidity -function transfer(address to, uint256 amount) external nonpayable returns (bool) -``` - - - -*See {IERC20-transfer}. Requirements: - `to` cannot be the zero address. - the caller must have a balance of at least `amount`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 amount) external nonpayable returns (bool) -``` - - - -*See {IERC20-transferFrom}. Emits an {Approval} event indicating the updated allowance. This is not required by the EIP. See the note at the beginning of {ERC20}. NOTE: Does not update the allowance if the current allowance is the maximum `uint256`. Requirements: - `from` and `to` cannot be the zero address. - `from` must have a balance of at least `amount`. - the caller must have allowance for ``from``'s tokens of at least `amount`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### verifyClaim - -```solidity -function verifyClaim(uint256 _conditionId, address _claimer, uint256 _quantity, address _currency, uint256 _pricePerToken, bool verifyMaxQuantityPerTransaction) external view -``` - - - -*Checks a request to claim tokens against the active claim condition's criteria.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _conditionId | uint256 | undefined -| _claimer | address | undefined -| _quantity | uint256 | undefined -| _currency | address | undefined -| _pricePerToken | uint256 | undefined -| verifyMaxQuantityPerTransaction | bool | undefined - -### verifyClaimMerkleProof - -```solidity -function verifyClaimMerkleProof(uint256 _conditionId, address _claimer, uint256 _quantity, bytes32[] _proofs, uint256 _proofMaxQuantityPerTransaction) external view returns (bool validMerkleProof, uint256 merkleProofIndex) -``` - - - -*Checks whether a claimer meets the claim condition's allowlist criteria.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _conditionId | uint256 | undefined -| _claimer | address | undefined -| _quantity | uint256 | undefined -| _proofs | bytes32[] | undefined -| _proofMaxQuantityPerTransaction | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| validMerkleProof | bool | undefined -| merkleProofIndex | uint256 | undefined - -### walletClaimCount - -```solidity -function walletClaimCount(address) external view returns (uint256) -``` - - - -*Mapping from address => number of tokens a wallet has claimed.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed spender, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| spender `indexed` | address | undefined | -| value | uint256 | undefined | - -### ClaimConditionsUpdated - -```solidity -event ClaimConditionsUpdated(IDropClaimCondition.ClaimCondition[] claimConditions) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| claimConditions | IDropClaimCondition.ClaimCondition[] | undefined | - -### DelegateChanged - -```solidity -event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| delegator `indexed` | address | undefined | -| fromDelegate `indexed` | address | undefined | -| toDelegate `indexed` | address | undefined | - -### DelegateVotesChanged - -```solidity -event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| delegate `indexed` | address | undefined | -| previousBalance | uint256 | undefined | -| newBalance | uint256 | undefined | - -### MaxTotalSupplyUpdated - -```solidity -event MaxTotalSupplyUpdated(uint256 maxTotalSupply) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| maxTotalSupply | uint256 | undefined | - -### MaxWalletClaimCountUpdated - -```solidity -event MaxWalletClaimCountUpdated(uint256 count) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| count | uint256 | undefined | - -### PlatformFeeInfoUpdated - -```solidity -event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| platformFeeRecipient `indexed` | address | undefined | -| platformFeeBps | uint256 | undefined | - -### PrimarySaleRecipientUpdated - -```solidity -event PrimarySaleRecipientUpdated(address indexed recipient) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| recipient `indexed` | address | undefined | - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### TokensClaimed - -```solidity -event TokensClaimed(uint256 indexed claimConditionIndex, address indexed claimer, address indexed receiver, uint256 quantityClaimed) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| claimConditionIndex `indexed` | uint256 | undefined | -| claimer `indexed` | address | undefined | -| receiver `indexed` | address | undefined | -| quantityClaimed | uint256 | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| value | uint256 | undefined | - -### WalletClaimCountUpdated - -```solidity -event WalletClaimCountUpdated(address indexed wallet, uint256 count) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| wallet `indexed` | address | undefined | -| count | uint256 | undefined | - - - diff --git a/docs/DropERC721.md b/docs/DropERC721.md deleted file mode 100644 index 6518abd99..000000000 --- a/docs/DropERC721.md +++ /dev/null @@ -1,1574 +0,0 @@ -# DropERC721 - - - - - - - - - -## Methods - -### DEFAULT_ADMIN_ROLE - -```solidity -function DEFAULT_ADMIN_ROLE() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### approve - -```solidity -function approve(address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-approve}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| tokenId | uint256 | undefined - -### balanceOf - -```solidity -function balanceOf(address owner) external view returns (uint256) -``` - - - -*See {IERC721-balanceOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### baseURIIndices - -```solidity -function baseURIIndices(uint256) external view returns (uint256) -``` - - - -*Largest tokenId of each batch of tokens with the same baseURI* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### burn - -```solidity -function burn(uint256 tokenId) external nonpayable -``` - - - -*Burns `tokenId`. See {ERC721-_burn}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -### claim - -```solidity -function claim(address _receiver, uint256 _quantity, address _currency, uint256 _pricePerToken, bytes32[] _proofs, uint256 _proofMaxQuantityPerTransaction) external payable -``` - - - -*Lets an account claim NFTs.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _receiver | address | undefined -| _quantity | uint256 | undefined -| _currency | address | undefined -| _pricePerToken | uint256 | undefined -| _proofs | bytes32[] | undefined -| _proofMaxQuantityPerTransaction | uint256 | undefined - -### claimCondition - -```solidity -function claimCondition() external view returns (uint256 currentStartId, uint256 count) -``` - - - -*The set of all claim conditions, at any given moment.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| currentStartId | uint256 | undefined -| count | uint256 | undefined - -### contractType - -```solidity -function contractType() external pure returns (bytes32) -``` - - - -*Returns the type of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### contractURI - -```solidity -function contractURI() external view returns (string) -``` - - - -*Contract level metadata.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### contractVersion - -```solidity -function contractVersion() external pure returns (uint8) -``` - - - -*Returns the version of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint8 | undefined - -### encryptDecrypt - -```solidity -function encryptDecrypt(bytes data, bytes key) external pure returns (bytes result) -``` - - - -*See: https://ethereum.stackexchange.com/questions/69825/decrypt-message-on-chain* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes | undefined -| key | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| result | bytes | undefined - -### encryptedBaseURI - -```solidity -function encryptedBaseURI(uint256) external view returns (bytes) -``` - - - -*Mapping from 'Largest tokenId of a batch of 'delayed-reveal' tokens with the same baseURI' to encrypted base URI for the respective batch of tokens.** - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes | undefined - -### getActiveClaimConditionId - -```solidity -function getActiveClaimConditionId() external view returns (uint256) -``` - - - -*At any given moment, returns the uid for the active claim condition.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getApproved - -```solidity -function getApproved(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-getApproved}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getBaseURICount - -```solidity -function getBaseURICount() external view returns (uint256) -``` - - - -*Returns the amount of stored baseURIs* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getClaimConditionById - -```solidity -function getClaimConditionById(uint256 _conditionId) external view returns (struct IDropClaimCondition.ClaimCondition condition) -``` - - - -*Returns the claim condition at the given uid.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _conditionId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| condition | IDropClaimCondition.ClaimCondition | undefined - -### getClaimTimestamp - -```solidity -function getClaimTimestamp(uint256 _conditionId, address _claimer) external view returns (uint256 lastClaimTimestamp, uint256 nextValidClaimTimestamp) -``` - - - -*Returns the timestamp for when a claimer is eligible for claiming NFTs again.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _conditionId | uint256 | undefined -| _claimer | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| lastClaimTimestamp | uint256 | undefined -| nextValidClaimTimestamp | uint256 | undefined - -### getDefaultRoyaltyInfo - -```solidity -function getDefaultRoyaltyInfo() external view returns (address, uint16) -``` - - - -*Returns the default royalty recipient and bps.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### getPlatformFeeInfo - -```solidity -function getPlatformFeeInfo() external view returns (address, uint16) -``` - - - -*Returns the platform fee recipient and bps.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleMember - -```solidity -function getRoleMember(bytes32 role, uint256 index) external view returns (address) -``` - - - -*Returns one of the accounts that have `role`. `index` must be a value between 0 and {getRoleMemberCount}, non-inclusive. Role bearers are not sorted in any particular way, and their ordering may change at any point. WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure you perform all queries on the same block. See the following https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] for more information.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getRoleMemberCount - -```solidity -function getRoleMemberCount(bytes32 role) external view returns (uint256) -``` - - - -*Returns the number of accounts that have `role`. Can be used together with {getRoleMember} to enumerate all bearers of a role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getRoyaltyInfoForToken - -```solidity -function getRoyaltyInfoForToken(uint256 _tokenId) external view returns (address, uint16) -``` - - - -*Returns the royalty recipient and bps for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### initialize - -```solidity -function initialize(address _defaultAdmin, string _name, string _symbol, string _contractURI, address[] _trustedForwarders, address _saleRecipient, address _royaltyRecipient, uint128 _royaltyBps, uint128 _platformFeeBps, address _platformFeeRecipient) external nonpayable -``` - - - -*Initiliazes the contract, like a constructor.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _defaultAdmin | address | undefined -| _name | string | undefined -| _symbol | string | undefined -| _contractURI | string | undefined -| _trustedForwarders | address[] | undefined -| _saleRecipient | address | undefined -| _royaltyRecipient | address | undefined -| _royaltyBps | uint128 | undefined -| _platformFeeBps | uint128 | undefined -| _platformFeeRecipient | address | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address owner, address operator) external view returns (bool) -``` - - - -*See {IERC721-isApprovedForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### isTrustedForwarder - -```solidity -function isTrustedForwarder(address forwarder) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| forwarder | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### lazyMint - -```solidity -function lazyMint(uint256 _amount, string _baseURIForTokens, bytes _encryptedBaseURI) external nonpayable -``` - - - -*Lets an account with `MINTER_ROLE` lazy mint 'n' NFTs. The URIs for each token is the provided `_baseURIForTokens` + `{tokenId}`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _amount | uint256 | undefined -| _baseURIForTokens | string | undefined -| _encryptedBaseURI | bytes | undefined - -### maxTotalSupply - -```solidity -function maxTotalSupply() external view returns (uint256) -``` - - - -*Global max total supply of NFTs.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### maxWalletClaimCount - -```solidity -function maxWalletClaimCount() external view returns (uint256) -``` - - - -*The max number of NFTs a wallet can claim.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### multicall - -```solidity -function multicall(bytes[] data) external nonpayable returns (bytes[] results) -``` - - - -*Receives and executes a batch of function calls on this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| results | bytes[] | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*See {IERC721Metadata-name}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### nextTokenIdToClaim - -```solidity -function nextTokenIdToClaim() external view returns (uint256) -``` - - - -*The next token ID of the NFT that can be claimed.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### nextTokenIdToMint - -```solidity -function nextTokenIdToMint() external view returns (uint256) -``` - - - -*The next token ID of the NFT to "lazy mint".* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### owner - -```solidity -function owner() external view returns (address) -``` - - - -*Returns the address of the current owner.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### ownerOf - -```solidity -function ownerOf(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-ownerOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### primarySaleRecipient - -```solidity -function primarySaleRecipient() external view returns (address) -``` - - - -*The address that receives all primary sales value.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been revoked `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### reveal - -```solidity -function reveal(uint256 index, bytes _key) external nonpayable returns (string revealedURI) -``` - - - -*Lets an account with `MINTER_ROLE` reveal the URI for a batch of 'delayed-reveal' NFTs.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| index | uint256 | undefined -| _key | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| revealedURI | string | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### royaltyInfo - -```solidity -function royaltyInfo(uint256 tokenId, uint256 salePrice) external view returns (address receiver, uint256 royaltyAmount) -``` - - - -*Returns the royalty recipient and amount, given a tokenId and sale price.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined -| salePrice | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| receiver | address | undefined -| royaltyAmount | uint256 | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 tokenId, bytes _data) external nonpayable -``` - - - -*See {IERC721-safeTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined -| _data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool approved) external nonpayable -``` - - - -*See {IERC721-setApprovalForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| approved | bool | undefined - -### setClaimConditions - -```solidity -function setClaimConditions(IDropClaimCondition.ClaimCondition[] _phases, bool _resetClaimEligibility) external nonpayable -``` - - - -*Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _phases | IDropClaimCondition.ClaimCondition[] | undefined -| _resetClaimEligibility | bool | undefined - -### setContractURI - -```solidity -function setContractURI(string _uri) external nonpayable -``` - - - -*Lets a contract admin set the URI for contract-level metadata.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _uri | string | undefined - -### setDefaultRoyaltyInfo - -```solidity -function setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) external nonpayable -``` - - - -*Lets a contract admin update the default royalty recipient and bps.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _royaltyRecipient | address | undefined -| _royaltyBps | uint256 | undefined - -### setMaxTotalSupply - -```solidity -function setMaxTotalSupply(uint256 _maxTotalSupply) external nonpayable -``` - - - -*Lets a contract admin set the global maximum supply for collection's NFTs.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _maxTotalSupply | uint256 | undefined - -### setMaxWalletClaimCount - -```solidity -function setMaxWalletClaimCount(uint256 _count) external nonpayable -``` - - - -*Lets a contract admin set a maximum number of NFTs that can be claimed by any wallet.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _count | uint256 | undefined - -### setOwner - -```solidity -function setOwner(address _newOwner) external nonpayable -``` - - - -*Lets a contract admin set a new owner for the contract. The new owner must be a contract admin.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _newOwner | address | undefined - -### setPlatformFeeInfo - -```solidity -function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) external nonpayable -``` - - - -*Lets a contract admin update the platform fee recipient and bps* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _platformFeeRecipient | address | undefined -| _platformFeeBps | uint256 | undefined - -### setPrimarySaleRecipient - -```solidity -function setPrimarySaleRecipient(address _saleRecipient) external nonpayable -``` - - - -*Lets a contract admin set the recipient for all primary sales.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _saleRecipient | address | undefined - -### setRoyaltyInfoForToken - -```solidity -function setRoyaltyInfoForToken(uint256 _tokenId, address _recipient, uint256 _bps) external nonpayable -``` - - - -*Lets a contract admin set the royalty recipient and bps for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined -| _recipient | address | undefined -| _bps | uint256 | undefined - -### setWalletClaimCount - -```solidity -function setWalletClaimCount(address _claimer, uint256 _count) external nonpayable -``` - - - -*Lets a contract admin set a claim count for a wallet.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _claimer | address | undefined -| _count | uint256 | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See ERC 165* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - -*See {IERC721Metadata-symbol}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### tokenByIndex - -```solidity -function tokenByIndex(uint256 index) external view returns (uint256) -``` - - - -*See {IERC721Enumerable-tokenByIndex}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### tokenOfOwnerByIndex - -```solidity -function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256) -``` - - - -*See {IERC721Enumerable-tokenOfOwnerByIndex}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### tokenURI - -```solidity -function tokenURI(uint256 _tokenId) external view returns (string) -``` - - - -*Returns the URI for a given tokenId.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - -*See {IERC721Enumerable-totalSupply}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-transferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined - -### verifyClaim - -```solidity -function verifyClaim(uint256 _conditionId, address _claimer, uint256 _quantity, address _currency, uint256 _pricePerToken, bool verifyMaxQuantityPerTransaction) external view -``` - - - -*Checks a request to claim NFTs against the active claim condition's criteria.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _conditionId | uint256 | undefined -| _claimer | address | undefined -| _quantity | uint256 | undefined -| _currency | address | undefined -| _pricePerToken | uint256 | undefined -| verifyMaxQuantityPerTransaction | bool | undefined - -### verifyClaimMerkleProof - -```solidity -function verifyClaimMerkleProof(uint256 _conditionId, address _claimer, uint256 _quantity, bytes32[] _proofs, uint256 _proofMaxQuantityPerTransaction) external view returns (bool validMerkleProof, uint256 merkleProofIndex) -``` - - - -*Checks whether a claimer meets the claim condition's allowlist criteria.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _conditionId | uint256 | undefined -| _claimer | address | undefined -| _quantity | uint256 | undefined -| _proofs | bytes32[] | undefined -| _proofMaxQuantityPerTransaction | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| validMerkleProof | bool | undefined -| merkleProofIndex | uint256 | undefined - -### walletClaimCount - -```solidity -function walletClaimCount(address) external view returns (uint256) -``` - - - -*Mapping from address => total number of NFTs a wallet has claimed.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| approved `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed owner, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### ClaimConditionsUpdated - -```solidity -event ClaimConditionsUpdated(IDropClaimCondition.ClaimCondition[] claimConditions) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| claimConditions | IDropClaimCondition.ClaimCondition[] | undefined | - -### DefaultRoyalty - -```solidity -event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| newRoyaltyRecipient `indexed` | address | undefined | -| newRoyaltyBps | uint256 | undefined | - -### MaxTotalSupplyUpdated - -```solidity -event MaxTotalSupplyUpdated(uint256 maxTotalSupply) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| maxTotalSupply | uint256 | undefined | - -### MaxWalletClaimCountUpdated - -```solidity -event MaxWalletClaimCountUpdated(uint256 count) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| count | uint256 | undefined | - -### NFTRevealed - -```solidity -event NFTRevealed(uint256 endTokenId, string revealedURI) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| endTokenId | uint256 | undefined | -| revealedURI | string | undefined | - -### OwnerUpdated - -```solidity -event OwnerUpdated(address indexed prevOwner, address indexed newOwner) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| prevOwner `indexed` | address | undefined | -| newOwner `indexed` | address | undefined | - -### PlatformFeeInfoUpdated - -```solidity -event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| platformFeeRecipient `indexed` | address | undefined | -| platformFeeBps | uint256 | undefined | - -### PrimarySaleRecipientUpdated - -```solidity -event PrimarySaleRecipientUpdated(address indexed recipient) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| recipient `indexed` | address | undefined | - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoyaltyForToken - -```solidity -event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, uint256 royaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId `indexed` | uint256 | undefined | -| royaltyRecipient `indexed` | address | undefined | -| royaltyBps | uint256 | undefined | - -### TokensClaimed - -```solidity -event TokensClaimed(uint256 indexed claimConditionIndex, address indexed claimer, address indexed receiver, uint256 startTokenId, uint256 quantityClaimed) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| claimConditionIndex `indexed` | uint256 | undefined | -| claimer `indexed` | address | undefined | -| receiver `indexed` | address | undefined | -| startTokenId | uint256 | undefined | -| quantityClaimed | uint256 | undefined | - -### TokensLazyMinted - -```solidity -event TokensLazyMinted(uint256 startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| startTokenId | uint256 | undefined | -| endTokenId | uint256 | undefined | -| baseURI | string | undefined | -| encryptedBaseURI | bytes | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - -### WalletClaimCountUpdated - -```solidity -event WalletClaimCountUpdated(address indexed wallet, uint256 count) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| wallet `indexed` | address | undefined | -| count | uint256 | undefined | - - - diff --git a/docs/DropSinglePhase.md b/docs/DropSinglePhase.md deleted file mode 100644 index 81b554082..000000000 --- a/docs/DropSinglePhase.md +++ /dev/null @@ -1,184 +0,0 @@ -# DropSinglePhase - - - - - - - - - -## Methods - -### claim - -```solidity -function claim(address _receiver, uint256 _quantity, address _currency, uint256 _pricePerToken, IDropSinglePhase.AllowlistProof _allowlistProof, bytes _data) external payable -``` - - - -*Lets an account claim tokens.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _receiver | address | undefined -| _quantity | uint256 | undefined -| _currency | address | undefined -| _pricePerToken | uint256 | undefined -| _allowlistProof | IDropSinglePhase.AllowlistProof | undefined -| _data | bytes | undefined - -### claimCondition - -```solidity -function claimCondition() external view returns (uint256 startTimestamp, uint256 maxClaimableSupply, uint256 supplyClaimed, uint256 quantityLimitPerTransaction, uint256 waitTimeInSecondsBetweenClaims, bytes32 merkleRoot, uint256 pricePerToken, address currency) -``` - - - -*The active conditions for claiming tokens.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| startTimestamp | uint256 | undefined -| maxClaimableSupply | uint256 | undefined -| supplyClaimed | uint256 | undefined -| quantityLimitPerTransaction | uint256 | undefined -| waitTimeInSecondsBetweenClaims | uint256 | undefined -| merkleRoot | bytes32 | undefined -| pricePerToken | uint256 | undefined -| currency | address | undefined - -### getClaimTimestamp - -```solidity -function getClaimTimestamp(address _claimer) external view returns (uint256 lastClaimedAt, uint256 nextValidClaimTimestamp) -``` - - - -*Returns the timestamp for when a claimer is eligible for claiming NFTs again.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _claimer | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| lastClaimedAt | uint256 | undefined -| nextValidClaimTimestamp | uint256 | undefined - -### setClaimConditions - -```solidity -function setClaimConditions(IClaimCondition.ClaimCondition _condition, bool _resetClaimEligibility) external nonpayable -``` - - - -*Lets a contract admin set claim conditions.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _condition | IClaimCondition.ClaimCondition | undefined -| _resetClaimEligibility | bool | undefined - -### verifyClaim - -```solidity -function verifyClaim(address _claimer, uint256 _quantity, address _currency, uint256 _pricePerToken, bool verifyMaxQuantityPerTransaction) external view -``` - - - -*Checks a request to claim NFTs against the active claim condition's criteria.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _claimer | address | undefined -| _quantity | uint256 | undefined -| _currency | address | undefined -| _pricePerToken | uint256 | undefined -| verifyMaxQuantityPerTransaction | bool | undefined - -### verifyClaimMerkleProof - -```solidity -function verifyClaimMerkleProof(address _claimer, uint256 _quantity, IDropSinglePhase.AllowlistProof _allowlistProof) external view returns (bool validMerkleProof, uint256 merkleProofIndex) -``` - - - -*Checks whether a claimer meets the claim condition's allowlist criteria.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _claimer | address | undefined -| _quantity | uint256 | undefined -| _allowlistProof | IDropSinglePhase.AllowlistProof | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| validMerkleProof | bool | undefined -| merkleProofIndex | uint256 | undefined - - - -## Events - -### ClaimConditionUpdated - -```solidity -event ClaimConditionUpdated(IClaimCondition.ClaimCondition condition, bool resetEligibility) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| condition | IClaimCondition.ClaimCondition | undefined | -| resetEligibility | bool | undefined | - -### TokensClaimed - -```solidity -event TokensClaimed(address indexed claimer, address indexed receiver, uint256 indexed startTokenId, uint256 quantityClaimed) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| claimer `indexed` | address | undefined | -| receiver `indexed` | address | undefined | -| startTokenId `indexed` | uint256 | undefined | -| quantityClaimed | uint256 | undefined | - - - diff --git a/docs/ECDSA.md b/docs/ECDSA.md deleted file mode 100644 index d4fef0408..000000000 --- a/docs/ECDSA.md +++ /dev/null @@ -1,12 +0,0 @@ -# ECDSA - - - - - - - -*Elliptic Curve Digital Signature Algorithm (ECDSA) operations. These functions can be used to verify that a message was signed by the holder of the private keys of a given address.* - - - diff --git a/docs/ECDSAUpgradeable.md b/docs/ECDSAUpgradeable.md deleted file mode 100644 index 5035a7b74..000000000 --- a/docs/ECDSAUpgradeable.md +++ /dev/null @@ -1,12 +0,0 @@ -# ECDSAUpgradeable - - - - - - - -*Elliptic Curve Digital Signature Algorithm (ECDSA) operations. These functions can be used to verify that a message was signed by the holder of the private keys of a given address.* - - - diff --git a/docs/EIP712.md b/docs/EIP712.md deleted file mode 100644 index d93f4f4f4..000000000 --- a/docs/EIP712.md +++ /dev/null @@ -1,12 +0,0 @@ -# EIP712 - - - - - - - -*https://eips.ethereum.org/EIPS/eip-712[EIP 712] is a standard for hashing and signing of typed structured data. The encoding specified in the EIP is very generic, and such a generic implementation in Solidity is not feasible, thus this contract does not implement the encoding itself. Protocols need to implement the type-specific encoding they need in their contracts using a combination of `abi.encode` and `keccak256`. This contract implements the EIP 712 domain separator ({_domainSeparatorV4}) that is used as part of the encoding scheme, and the final step of the encoding to obtain the message digest that is then signed via ECDSA ({_hashTypedDataV4}). The implementation of the domain separator was designed to be as efficient as possible while still properly updating the chain id to protect against replay attacks on an eventual fork of the chain. NOTE: This contract implements the version of the encoding known as "v4", as implemented by the JSON RPC method https://docs.metamask.io/guide/signing-data.html[`eth_signTypedDataV4` in MetaMask]. _Available since v3.4._* - - - diff --git a/docs/EIP712Upgradeable.md b/docs/EIP712Upgradeable.md deleted file mode 100644 index 5dfe58185..000000000 --- a/docs/EIP712Upgradeable.md +++ /dev/null @@ -1,12 +0,0 @@ -# EIP712Upgradeable - - - - - - - -*https://eips.ethereum.org/EIPS/eip-712[EIP 712] is a standard for hashing and signing of typed structured data. The encoding specified in the EIP is very generic, and such a generic implementation in Solidity is not feasible, thus this contract does not implement the encoding itself. Protocols need to implement the type-specific encoding they need in their contracts using a combination of `abi.encode` and `keccak256`. This contract implements the EIP 712 domain separator ({_domainSeparatorV4}) that is used as part of the encoding scheme, and the final step of the encoding to obtain the message digest that is then signed via ECDSA ({_hashTypedDataV4}). The implementation of the domain separator was designed to be as efficient as possible while still properly updating the chain id to protect against replay attacks on an eventual fork of the chain. NOTE: This contract implements the version of the encoding known as "v4", as implemented by the JSON RPC method https://docs.metamask.io/guide/signing-data.html[`eth_signTypedDataV4` in MetaMask]. _Available since v3.4._* - - - diff --git a/docs/ERC1155BurnableUpgradeable.md b/docs/ERC1155BurnableUpgradeable.md deleted file mode 100644 index 2e93bc956..000000000 --- a/docs/ERC1155BurnableUpgradeable.md +++ /dev/null @@ -1,299 +0,0 @@ -# ERC1155BurnableUpgradeable - - - - - - - -*Extension of {ERC1155} that allows token holders to destroy both their own tokens and those that they have been approved to use. _Available since v3.1._* - -## Methods - -### balanceOf - -```solidity -function balanceOf(address account, uint256 id) external view returns (uint256) -``` - - - -*See {IERC1155-balanceOf}. Requirements: - `account` cannot be the zero address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| id | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### balanceOfBatch - -```solidity -function balanceOfBatch(address[] accounts, uint256[] ids) external view returns (uint256[]) -``` - - - -*See {IERC1155-balanceOfBatch}. Requirements: - `accounts` and `ids` must have the same length.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| accounts | address[] | undefined -| ids | uint256[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256[] | undefined - -### burn - -```solidity -function burn(address account, uint256 id, uint256 value) external nonpayable -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| id | uint256 | undefined -| value | uint256 | undefined - -### burnBatch - -```solidity -function burnBatch(address account, uint256[] ids, uint256[] values) external nonpayable -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| ids | uint256[] | undefined -| values | uint256[] | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address account, address operator) external view returns (bool) -``` - - - -*See {IERC1155-isApprovedForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### safeBatchTransferFrom - -```solidity -function safeBatchTransferFrom(address from, address to, uint256[] ids, uint256[] amounts, bytes data) external nonpayable -``` - - - -*See {IERC1155-safeBatchTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| ids | uint256[] | undefined -| amounts | uint256[] | undefined -| data | bytes | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data) external nonpayable -``` - - - -*See {IERC1155-safeTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| id | uint256 | undefined -| amount | uint256 | undefined -| data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool approved) external nonpayable -``` - - - -*See {IERC1155-setApprovalForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| approved | bool | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### uri - -```solidity -function uri(uint256) external view returns (string) -``` - - - -*See {IERC1155MetadataURI-uri}. This implementation returns the same URI for *all* token types. It relies on the token type ID substitution mechanism https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the EIP]. Clients calling this function must replace the `\{id\}` substring with the actual token type ID.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - - - -## Events - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed account, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### TransferBatch - -```solidity -event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| ids | uint256[] | undefined | -| values | uint256[] | undefined | - -### TransferSingle - -```solidity -event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| id | uint256 | undefined | -| value | uint256 | undefined | - -### URI - -```solidity -event URI(string value, uint256 indexed id) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| value | string | undefined | -| id `indexed` | uint256 | undefined | - - - diff --git a/docs/ERC1155Holder.md b/docs/ERC1155Holder.md deleted file mode 100644 index b33b6dc2d..000000000 --- a/docs/ERC1155Holder.md +++ /dev/null @@ -1,89 +0,0 @@ -# ERC1155Holder - - - - - -Simple implementation of `ERC1155Receiver` that will allow a contract to hold ERC1155 tokens. IMPORTANT: When inheriting this contract, you must include a way to use the received tokens, otherwise they will be stuck. - -*_Available since v3.1._* - -## Methods - -### onERC1155BatchReceived - -```solidity -function onERC1155BatchReceived(address, address, uint256[], uint256[], bytes) external nonpayable returns (bytes4) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | address | undefined -| _2 | uint256[] | undefined -| _3 | uint256[] | undefined -| _4 | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - -### onERC1155Received - -```solidity -function onERC1155Received(address, address, uint256, uint256, bytes) external nonpayable returns (bytes4) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | address | undefined -| _2 | uint256 | undefined -| _3 | uint256 | undefined -| _4 | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - - diff --git a/docs/ERC1155HolderUpgradeable.md b/docs/ERC1155HolderUpgradeable.md deleted file mode 100644 index 5466aac6c..000000000 --- a/docs/ERC1155HolderUpgradeable.md +++ /dev/null @@ -1,89 +0,0 @@ -# ERC1155HolderUpgradeable - - - - - -Simple implementation of `ERC1155Receiver` that will allow a contract to hold ERC1155 tokens. IMPORTANT: When inheriting this contract, you must include a way to use the received tokens, otherwise they will be stuck. - -*_Available since v3.1._* - -## Methods - -### onERC1155BatchReceived - -```solidity -function onERC1155BatchReceived(address, address, uint256[], uint256[], bytes) external nonpayable returns (bytes4) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | address | undefined -| _2 | uint256[] | undefined -| _3 | uint256[] | undefined -| _4 | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - -### onERC1155Received - -```solidity -function onERC1155Received(address, address, uint256, uint256, bytes) external nonpayable returns (bytes4) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | address | undefined -| _2 | uint256 | undefined -| _3 | uint256 | undefined -| _4 | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - - diff --git a/docs/ERC1155PausableUpgradeable.md b/docs/ERC1155PausableUpgradeable.md deleted file mode 100644 index aaec0a299..000000000 --- a/docs/ERC1155PausableUpgradeable.md +++ /dev/null @@ -1,312 +0,0 @@ -# ERC1155PausableUpgradeable - - - - - - - -*ERC1155 token with pausable token transfers, minting and burning. Useful for scenarios such as preventing trades until the end of an evaluation period, or having an emergency switch for freezing all token transfers in the event of a large bug. _Available since v3.1._* - -## Methods - -### balanceOf - -```solidity -function balanceOf(address account, uint256 id) external view returns (uint256) -``` - - - -*See {IERC1155-balanceOf}. Requirements: - `account` cannot be the zero address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| id | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### balanceOfBatch - -```solidity -function balanceOfBatch(address[] accounts, uint256[] ids) external view returns (uint256[]) -``` - - - -*See {IERC1155-balanceOfBatch}. Requirements: - `accounts` and `ids` must have the same length.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| accounts | address[] | undefined -| ids | uint256[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256[] | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address account, address operator) external view returns (bool) -``` - - - -*See {IERC1155-isApprovedForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### paused - -```solidity -function paused() external view returns (bool) -``` - - - -*Returns true if the contract is paused, and false otherwise.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### safeBatchTransferFrom - -```solidity -function safeBatchTransferFrom(address from, address to, uint256[] ids, uint256[] amounts, bytes data) external nonpayable -``` - - - -*See {IERC1155-safeBatchTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| ids | uint256[] | undefined -| amounts | uint256[] | undefined -| data | bytes | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data) external nonpayable -``` - - - -*See {IERC1155-safeTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| id | uint256 | undefined -| amount | uint256 | undefined -| data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool approved) external nonpayable -``` - - - -*See {IERC1155-setApprovalForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| approved | bool | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### uri - -```solidity -function uri(uint256) external view returns (string) -``` - - - -*See {IERC1155MetadataURI-uri}. This implementation returns the same URI for *all* token types. It relies on the token type ID substitution mechanism https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the EIP]. Clients calling this function must replace the `\{id\}` substring with the actual token type ID.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - - - -## Events - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed account, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### Paused - -```solidity -event Paused(address account) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined | - -### TransferBatch - -```solidity -event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| ids | uint256[] | undefined | -| values | uint256[] | undefined | - -### TransferSingle - -```solidity -event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| id | uint256 | undefined | -| value | uint256 | undefined | - -### URI - -```solidity -event URI(string value, uint256 indexed id) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| value | string | undefined | -| id `indexed` | uint256 | undefined | - -### Unpaused - -```solidity -event Unpaused(address account) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined | - - - diff --git a/docs/ERC1155PresetUpgradeable.md b/docs/ERC1155PresetUpgradeable.md deleted file mode 100644 index b025b8e73..000000000 --- a/docs/ERC1155PresetUpgradeable.md +++ /dev/null @@ -1,719 +0,0 @@ -# ERC1155PresetUpgradeable - - - - - - - -*{ERC1155} token, including: - ability for holders to burn (destroy) their tokens - a minter role that allows for token minting (creation) - a pauser role that allows to stop all token transfers This contract uses {AccessControl} to lock permissioned functions using the different roles - head to its documentation for details. The account that deploys the contract will be granted the minter and pauser roles, as well as the default admin role, which will let it grant both minter and pauser roles to other accounts.* - -## Methods - -### DEFAULT_ADMIN_ROLE - -```solidity -function DEFAULT_ADMIN_ROLE() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### balanceOf - -```solidity -function balanceOf(address account, uint256 id) external view returns (uint256) -``` - - - -*See {IERC1155-balanceOf}. Requirements: - `account` cannot be the zero address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| id | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### balanceOfBatch - -```solidity -function balanceOfBatch(address[] accounts, uint256[] ids) external view returns (uint256[]) -``` - - - -*See {IERC1155-balanceOfBatch}. Requirements: - `accounts` and `ids` must have the same length.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| accounts | address[] | undefined -| ids | uint256[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256[] | undefined - -### burn - -```solidity -function burn(address account, uint256 id, uint256 value) external nonpayable -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| id | uint256 | undefined -| value | uint256 | undefined - -### burnBatch - -```solidity -function burnBatch(address account, uint256[] ids, uint256[] values) external nonpayable -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| ids | uint256[] | undefined -| values | uint256[] | undefined - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleMember - -```solidity -function getRoleMember(bytes32 role, uint256 index) external view returns (address) -``` - - - -*Returns one of the accounts that have `role`. `index` must be a value between 0 and {getRoleMemberCount}, non-inclusive. Role bearers are not sorted in any particular way, and their ordering may change at any point. WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure you perform all queries on the same block. See the following https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] for more information.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getRoleMemberCount - -```solidity -function getRoleMemberCount(bytes32 role) external view returns (uint256) -``` - - - -*Returns the number of accounts that have `role`. Can be used together with {getRoleMember} to enumerate all bearers of a role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address account, address operator) external view returns (bool) -``` - - - -*See {IERC1155-isApprovedForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### mint - -```solidity -function mint(address to, uint256 id, uint256 amount, bytes data) external nonpayable -``` - - - -*Creates `amount` new tokens for `to`, of token type `id`. See {ERC1155-_mint}. Requirements: - the caller must have the `MINTER_ROLE`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| id | uint256 | undefined -| amount | uint256 | undefined -| data | bytes | undefined - -### mintBatch - -```solidity -function mintBatch(address to, uint256[] ids, uint256[] amounts, bytes data) external nonpayable -``` - - - -*xref:ROOT:erc1155.adoc#batch-operations[Batched] variant of {mint}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| ids | uint256[] | undefined -| amounts | uint256[] | undefined -| data | bytes | undefined - -### onERC1155BatchReceived - -```solidity -function onERC1155BatchReceived(address, address, uint256[], uint256[], bytes) external nonpayable returns (bytes4) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | address | undefined -| _2 | uint256[] | undefined -| _3 | uint256[] | undefined -| _4 | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - -### onERC1155Received - -```solidity -function onERC1155Received(address, address, uint256, uint256, bytes) external nonpayable returns (bytes4) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | address | undefined -| _2 | uint256 | undefined -| _3 | uint256 | undefined -| _4 | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - -### onERC721Received - -```solidity -function onERC721Received(address, address, uint256, bytes) external nonpayable returns (bytes4) -``` - - - -*See {IERC721Receiver-onERC721Received}. Always returns `IERC721Receiver.onERC721Received.selector`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | address | undefined -| _2 | uint256 | undefined -| _3 | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - -### pause - -```solidity -function pause() external nonpayable -``` - - - -*Pauses all token transfers. See {ERC1155Pausable} and {Pausable-_pause}. Requirements: - the caller must have the `PAUSER_ROLE`.* - - -### paused - -```solidity -function paused() external view returns (bool) -``` - - - -*Returns true if the contract is paused, and false otherwise.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been revoked `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### safeBatchTransferFrom - -```solidity -function safeBatchTransferFrom(address from, address to, uint256[] ids, uint256[] amounts, bytes data) external nonpayable -``` - - - -*See {IERC1155-safeBatchTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| ids | uint256[] | undefined -| amounts | uint256[] | undefined -| data | bytes | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data) external nonpayable -``` - - - -*See {IERC1155-safeTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| id | uint256 | undefined -| amount | uint256 | undefined -| data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool approved) external nonpayable -``` - - - -*See {IERC1155-setApprovalForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| approved | bool | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### totalSupply - -```solidity -function totalSupply(uint256 id) external view returns (uint256) -``` - - - -*Total amount of tokens in with a given id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| id | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### unpause - -```solidity -function unpause() external nonpayable -``` - - - -*Unpauses all token transfers. See {ERC1155Pausable} and {Pausable-_unpause}. Requirements: - the caller must have the `PAUSER_ROLE`.* - - -### uri - -```solidity -function uri(uint256) external view returns (string) -``` - - - -*See {IERC1155MetadataURI-uri}. This implementation returns the same URI for *all* token types. It relies on the token type ID substitution mechanism https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the EIP]. Clients calling this function must replace the `\{id\}` substring with the actual token type ID.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - - - -## Events - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed account, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### Paused - -```solidity -event Paused(address account) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined | - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### TransferBatch - -```solidity -event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| ids | uint256[] | undefined | -| values | uint256[] | undefined | - -### TransferSingle - -```solidity -event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| id | uint256 | undefined | -| value | uint256 | undefined | - -### URI - -```solidity -event URI(string value, uint256 indexed id) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| value | string | undefined | -| id `indexed` | uint256 | undefined | - -### Unpaused - -```solidity -event Unpaused(address account) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined | - - - diff --git a/docs/ERC1155Receiver.md b/docs/ERC1155Receiver.md deleted file mode 100644 index b328787a6..000000000 --- a/docs/ERC1155Receiver.md +++ /dev/null @@ -1,89 +0,0 @@ -# ERC1155Receiver - - - - - - - -*_Available since v3.1._* - -## Methods - -### onERC1155BatchReceived - -```solidity -function onERC1155BatchReceived(address operator, address from, uint256[] ids, uint256[] values, bytes data) external nonpayable returns (bytes4) -``` - - - -*Handles the receipt of a multiple ERC1155 token types. This function is called at the end of a `safeBatchTransferFrom` after the balances have been updated. NOTE: To accept the transfer(s), this must return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` (i.e. 0xbc197c81, or its own function selector).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | The address which initiated the batch transfer (i.e. msg.sender) -| from | address | The address which previously owned the token -| ids | uint256[] | An array containing ids of each token being transferred (order and length must match values array) -| values | uint256[] | An array containing amounts of each token being transferred (order and length must match ids array) -| data | bytes | Additional data with no specified format - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed - -### onERC1155Received - -```solidity -function onERC1155Received(address operator, address from, uint256 id, uint256 value, bytes data) external nonpayable returns (bytes4) -``` - - - -*Handles the receipt of a single ERC1155 token type. This function is called at the end of a `safeTransferFrom` after the balance has been updated. NOTE: To accept the transfer, this must return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` (i.e. 0xf23a6e61, or its own function selector).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | The address which initiated the transfer (i.e. msg.sender) -| from | address | The address which previously owned the token -| id | uint256 | The ID of the token being transferred -| value | uint256 | The amount of tokens being transferred -| data | bytes | Additional data with no specified format - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - - diff --git a/docs/ERC1155ReceiverUpgradeable.md b/docs/ERC1155ReceiverUpgradeable.md deleted file mode 100644 index 06c39b677..000000000 --- a/docs/ERC1155ReceiverUpgradeable.md +++ /dev/null @@ -1,89 +0,0 @@ -# ERC1155ReceiverUpgradeable - - - - - - - -*_Available since v3.1._* - -## Methods - -### onERC1155BatchReceived - -```solidity -function onERC1155BatchReceived(address operator, address from, uint256[] ids, uint256[] values, bytes data) external nonpayable returns (bytes4) -``` - - - -*Handles the receipt of a multiple ERC1155 token types. This function is called at the end of a `safeBatchTransferFrom` after the balances have been updated. NOTE: To accept the transfer(s), this must return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` (i.e. 0xbc197c81, or its own function selector).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | The address which initiated the batch transfer (i.e. msg.sender) -| from | address | The address which previously owned the token -| ids | uint256[] | An array containing ids of each token being transferred (order and length must match values array) -| values | uint256[] | An array containing amounts of each token being transferred (order and length must match ids array) -| data | bytes | Additional data with no specified format - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed - -### onERC1155Received - -```solidity -function onERC1155Received(address operator, address from, uint256 id, uint256 value, bytes data) external nonpayable returns (bytes4) -``` - - - -*Handles the receipt of a single ERC1155 token type. This function is called at the end of a `safeTransferFrom` after the balance has been updated. NOTE: To accept the transfer, this must return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` (i.e. 0xf23a6e61, or its own function selector).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | The address which initiated the transfer (i.e. msg.sender) -| from | address | The address which previously owned the token -| id | uint256 | The ID of the token being transferred -| value | uint256 | The amount of tokens being transferred -| data | bytes | Additional data with no specified format - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - - diff --git a/docs/ERC1155Upgradeable.md b/docs/ERC1155Upgradeable.md deleted file mode 100644 index 270295069..000000000 --- a/docs/ERC1155Upgradeable.md +++ /dev/null @@ -1,263 +0,0 @@ -# ERC1155Upgradeable - - - - - - - -*Implementation of the basic standard multi-token. See https://eips.ethereum.org/EIPS/eip-1155 Originally based on code by Enjin: https://github.com/enjin/erc-1155 _Available since v3.1._* - -## Methods - -### balanceOf - -```solidity -function balanceOf(address account, uint256 id) external view returns (uint256) -``` - - - -*See {IERC1155-balanceOf}. Requirements: - `account` cannot be the zero address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| id | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### balanceOfBatch - -```solidity -function balanceOfBatch(address[] accounts, uint256[] ids) external view returns (uint256[]) -``` - - - -*See {IERC1155-balanceOfBatch}. Requirements: - `accounts` and `ids` must have the same length.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| accounts | address[] | undefined -| ids | uint256[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256[] | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address account, address operator) external view returns (bool) -``` - - - -*See {IERC1155-isApprovedForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### safeBatchTransferFrom - -```solidity -function safeBatchTransferFrom(address from, address to, uint256[] ids, uint256[] amounts, bytes data) external nonpayable -``` - - - -*See {IERC1155-safeBatchTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| ids | uint256[] | undefined -| amounts | uint256[] | undefined -| data | bytes | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data) external nonpayable -``` - - - -*See {IERC1155-safeTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| id | uint256 | undefined -| amount | uint256 | undefined -| data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool approved) external nonpayable -``` - - - -*See {IERC1155-setApprovalForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| approved | bool | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### uri - -```solidity -function uri(uint256) external view returns (string) -``` - - - -*See {IERC1155MetadataURI-uri}. This implementation returns the same URI for *all* token types. It relies on the token type ID substitution mechanism https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the EIP]. Clients calling this function must replace the `\{id\}` substring with the actual token type ID.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - - - -## Events - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed account, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### TransferBatch - -```solidity -event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| ids | uint256[] | undefined | -| values | uint256[] | undefined | - -### TransferSingle - -```solidity -event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| id | uint256 | undefined | -| value | uint256 | undefined | - -### URI - -```solidity -event URI(string value, uint256 indexed id) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| value | string | undefined | -| id `indexed` | uint256 | undefined | - - - diff --git a/docs/ERC165.md b/docs/ERC165.md deleted file mode 100644 index 4b5f2aa5e..000000000 --- a/docs/ERC165.md +++ /dev/null @@ -1,37 +0,0 @@ -# ERC165 - - - - - - - -*Implementation of the {IERC165} interface. Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check for the additional interface id that will be supported. For example: ```solidity function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId); } ``` Alternatively, {ERC165Storage} provides an easier to use but more expensive implementation.* - -## Methods - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - - diff --git a/docs/ERC165Upgradeable.md b/docs/ERC165Upgradeable.md deleted file mode 100644 index fd93d1854..000000000 --- a/docs/ERC165Upgradeable.md +++ /dev/null @@ -1,37 +0,0 @@ -# ERC165Upgradeable - - - - - - - -*Implementation of the {IERC165} interface. Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check for the additional interface id that will be supported. For example: ```solidity function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId); } ``` Alternatively, {ERC165Storage} provides an easier to use but more expensive implementation.* - -## Methods - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - - diff --git a/docs/ERC20BurnableUpgradeable.md b/docs/ERC20BurnableUpgradeable.md deleted file mode 100644 index 02b4cb900..000000000 --- a/docs/ERC20BurnableUpgradeable.md +++ /dev/null @@ -1,316 +0,0 @@ -# ERC20BurnableUpgradeable - - - - - - - -*Extension of {ERC20} that allows token holders to destroy both their own tokens and those that they have an allowance for, in a way that can be recognized off-chain (via event analysis).* - -## Methods - -### allowance - -```solidity -function allowance(address owner, address spender) external view returns (uint256) -``` - - - -*See {IERC20-allowance}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| spender | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### approve - -```solidity -function approve(address spender, uint256 amount) external nonpayable returns (bool) -``` - - - -*See {IERC20-approve}. NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on `transferFrom`. This is semantically equivalent to an infinite approval. Requirements: - `spender` cannot be the zero address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### balanceOf - -```solidity -function balanceOf(address account) external view returns (uint256) -``` - - - -*See {IERC20-balanceOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### burn - -```solidity -function burn(uint256 amount) external nonpayable -``` - - - -*Destroys `amount` tokens from the caller. See {ERC20-_burn}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| amount | uint256 | undefined - -### burnFrom - -```solidity -function burnFrom(address account, uint256 amount) external nonpayable -``` - - - -*Destroys `amount` tokens from `account`, deducting from the caller's allowance. See {ERC20-_burn} and {ERC20-allowance}. Requirements: - the caller must have allowance for ``accounts``'s tokens of at least `amount`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| amount | uint256 | undefined - -### decimals - -```solidity -function decimals() external view returns (uint8) -``` - - - -*Returns the number of decimals used to get its user representation. For example, if `decimals` equals `2`, a balance of `505` tokens should be displayed to a user as `5.05` (`505 / 10 ** 2`). Tokens usually opt for a value of 18, imitating the relationship between Ether and Wei. This is the value {ERC20} uses, unless this function is overridden; NOTE: This information is only used for _display_ purposes: it in no way affects any of the arithmetic of the contract, including {IERC20-balanceOf} and {IERC20-transfer}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint8 | undefined - -### decreaseAllowance - -```solidity -function decreaseAllowance(address spender, uint256 subtractedValue) external nonpayable returns (bool) -``` - - - -*Atomically decreases the allowance granted to `spender` by the caller. This is an alternative to {approve} that can be used as a mitigation for problems described in {IERC20-approve}. Emits an {Approval} event indicating the updated allowance. Requirements: - `spender` cannot be the zero address. - `spender` must have allowance for the caller of at least `subtractedValue`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| subtractedValue | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### increaseAllowance - -```solidity -function increaseAllowance(address spender, uint256 addedValue) external nonpayable returns (bool) -``` - - - -*Atomically increases the allowance granted to `spender` by the caller. This is an alternative to {approve} that can be used as a mitigation for problems described in {IERC20-approve}. Emits an {Approval} event indicating the updated allowance. Requirements: - `spender` cannot be the zero address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| addedValue | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*Returns the name of the token.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - -*Returns the symbol of the token, usually a shorter version of the name.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - -*See {IERC20-totalSupply}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transfer - -```solidity -function transfer(address to, uint256 amount) external nonpayable returns (bool) -``` - - - -*See {IERC20-transfer}. Requirements: - `to` cannot be the zero address. - the caller must have a balance of at least `amount`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 amount) external nonpayable returns (bool) -``` - - - -*See {IERC20-transferFrom}. Emits an {Approval} event indicating the updated allowance. This is not required by the EIP. See the note at the beginning of {ERC20}. NOTE: Does not update the allowance if the current allowance is the maximum `uint256`. Requirements: - `from` and `to` cannot be the zero address. - `from` must have a balance of at least `amount`. - the caller must have allowance for ``from``'s tokens of at least `amount`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed spender, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| spender `indexed` | address | undefined | -| value | uint256 | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| value | uint256 | undefined | - - - diff --git a/docs/ERC20PausableUpgradeable.md b/docs/ERC20PausableUpgradeable.md deleted file mode 100644 index 2fe4ad881..000000000 --- a/docs/ERC20PausableUpgradeable.md +++ /dev/null @@ -1,332 +0,0 @@ -# ERC20PausableUpgradeable - - - - - - - -*ERC20 token with pausable token transfers, minting and burning. Useful for scenarios such as preventing trades until the end of an evaluation period, or having an emergency switch for freezing all token transfers in the event of a large bug.* - -## Methods - -### allowance - -```solidity -function allowance(address owner, address spender) external view returns (uint256) -``` - - - -*See {IERC20-allowance}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| spender | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### approve - -```solidity -function approve(address spender, uint256 amount) external nonpayable returns (bool) -``` - - - -*See {IERC20-approve}. NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on `transferFrom`. This is semantically equivalent to an infinite approval. Requirements: - `spender` cannot be the zero address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### balanceOf - -```solidity -function balanceOf(address account) external view returns (uint256) -``` - - - -*See {IERC20-balanceOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### decimals - -```solidity -function decimals() external view returns (uint8) -``` - - - -*Returns the number of decimals used to get its user representation. For example, if `decimals` equals `2`, a balance of `505` tokens should be displayed to a user as `5.05` (`505 / 10 ** 2`). Tokens usually opt for a value of 18, imitating the relationship between Ether and Wei. This is the value {ERC20} uses, unless this function is overridden; NOTE: This information is only used for _display_ purposes: it in no way affects any of the arithmetic of the contract, including {IERC20-balanceOf} and {IERC20-transfer}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint8 | undefined - -### decreaseAllowance - -```solidity -function decreaseAllowance(address spender, uint256 subtractedValue) external nonpayable returns (bool) -``` - - - -*Atomically decreases the allowance granted to `spender` by the caller. This is an alternative to {approve} that can be used as a mitigation for problems described in {IERC20-approve}. Emits an {Approval} event indicating the updated allowance. Requirements: - `spender` cannot be the zero address. - `spender` must have allowance for the caller of at least `subtractedValue`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| subtractedValue | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### increaseAllowance - -```solidity -function increaseAllowance(address spender, uint256 addedValue) external nonpayable returns (bool) -``` - - - -*Atomically increases the allowance granted to `spender` by the caller. This is an alternative to {approve} that can be used as a mitigation for problems described in {IERC20-approve}. Emits an {Approval} event indicating the updated allowance. Requirements: - `spender` cannot be the zero address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| addedValue | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*Returns the name of the token.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### paused - -```solidity -function paused() external view returns (bool) -``` - - - -*Returns true if the contract is paused, and false otherwise.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - -*Returns the symbol of the token, usually a shorter version of the name.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - -*See {IERC20-totalSupply}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transfer - -```solidity -function transfer(address to, uint256 amount) external nonpayable returns (bool) -``` - - - -*See {IERC20-transfer}. Requirements: - `to` cannot be the zero address. - the caller must have a balance of at least `amount`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 amount) external nonpayable returns (bool) -``` - - - -*See {IERC20-transferFrom}. Emits an {Approval} event indicating the updated allowance. This is not required by the EIP. See the note at the beginning of {ERC20}. NOTE: Does not update the allowance if the current allowance is the maximum `uint256`. Requirements: - `from` and `to` cannot be the zero address. - `from` must have a balance of at least `amount`. - the caller must have allowance for ``from``'s tokens of at least `amount`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed spender, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| spender `indexed` | address | undefined | -| value | uint256 | undefined | - -### Paused - -```solidity -event Paused(address account) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| value | uint256 | undefined | - -### Unpaused - -```solidity -event Unpaused(address account) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined | - - - diff --git a/docs/ERC20PermitUpgradeable.md b/docs/ERC20PermitUpgradeable.md deleted file mode 100644 index 3dd2a2d28..000000000 --- a/docs/ERC20PermitUpgradeable.md +++ /dev/null @@ -1,344 +0,0 @@ -# ERC20PermitUpgradeable - - - - - - - -*Implementation of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by presenting a message signed by the account. By not relying on `{IERC20-approve}`, the token holder account doesn't need to send a transaction, and thus is not required to hold Ether at all. _Available since v3.4._* - -## Methods - -### DOMAIN_SEPARATOR - -```solidity -function DOMAIN_SEPARATOR() external view returns (bytes32) -``` - - - -*See {IERC20Permit-DOMAIN_SEPARATOR}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### allowance - -```solidity -function allowance(address owner, address spender) external view returns (uint256) -``` - - - -*See {IERC20-allowance}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| spender | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### approve - -```solidity -function approve(address spender, uint256 amount) external nonpayable returns (bool) -``` - - - -*See {IERC20-approve}. NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on `transferFrom`. This is semantically equivalent to an infinite approval. Requirements: - `spender` cannot be the zero address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### balanceOf - -```solidity -function balanceOf(address account) external view returns (uint256) -``` - - - -*See {IERC20-balanceOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### decimals - -```solidity -function decimals() external view returns (uint8) -``` - - - -*Returns the number of decimals used to get its user representation. For example, if `decimals` equals `2`, a balance of `505` tokens should be displayed to a user as `5.05` (`505 / 10 ** 2`). Tokens usually opt for a value of 18, imitating the relationship between Ether and Wei. This is the value {ERC20} uses, unless this function is overridden; NOTE: This information is only used for _display_ purposes: it in no way affects any of the arithmetic of the contract, including {IERC20-balanceOf} and {IERC20-transfer}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint8 | undefined - -### decreaseAllowance - -```solidity -function decreaseAllowance(address spender, uint256 subtractedValue) external nonpayable returns (bool) -``` - - - -*Atomically decreases the allowance granted to `spender` by the caller. This is an alternative to {approve} that can be used as a mitigation for problems described in {IERC20-approve}. Emits an {Approval} event indicating the updated allowance. Requirements: - `spender` cannot be the zero address. - `spender` must have allowance for the caller of at least `subtractedValue`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| subtractedValue | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### increaseAllowance - -```solidity -function increaseAllowance(address spender, uint256 addedValue) external nonpayable returns (bool) -``` - - - -*Atomically increases the allowance granted to `spender` by the caller. This is an alternative to {approve} that can be used as a mitigation for problems described in {IERC20-approve}. Emits an {Approval} event indicating the updated allowance. Requirements: - `spender` cannot be the zero address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| addedValue | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*Returns the name of the token.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### nonces - -```solidity -function nonces(address owner) external view returns (uint256) -``` - - - -*See {IERC20Permit-nonces}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### permit - -```solidity -function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external nonpayable -``` - - - -*See {IERC20Permit-permit}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| spender | address | undefined -| value | uint256 | undefined -| deadline | uint256 | undefined -| v | uint8 | undefined -| r | bytes32 | undefined -| s | bytes32 | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - -*Returns the symbol of the token, usually a shorter version of the name.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - -*See {IERC20-totalSupply}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transfer - -```solidity -function transfer(address to, uint256 amount) external nonpayable returns (bool) -``` - - - -*See {IERC20-transfer}. Requirements: - `to` cannot be the zero address. - the caller must have a balance of at least `amount`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 amount) external nonpayable returns (bool) -``` - - - -*See {IERC20-transferFrom}. Emits an {Approval} event indicating the updated allowance. This is not required by the EIP. See the note at the beginning of {ERC20}. NOTE: Does not update the allowance if the current allowance is the maximum `uint256`. Requirements: - `from` and `to` cannot be the zero address. - `from` must have a balance of at least `amount`. - the caller must have allowance for ``from``'s tokens of at least `amount`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed spender, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| spender `indexed` | address | undefined | -| value | uint256 | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| value | uint256 | undefined | - - - diff --git a/docs/ERC20Upgradeable.md b/docs/ERC20Upgradeable.md deleted file mode 100644 index e6520306d..000000000 --- a/docs/ERC20Upgradeable.md +++ /dev/null @@ -1,283 +0,0 @@ -# ERC20Upgradeable - - - - - - - -*Implementation of the {IERC20} interface. This implementation is agnostic to the way tokens are created. This means that a supply mechanism has to be added in a derived contract using {_mint}. For a generic mechanism see {ERC20PresetMinterPauser}. TIP: For a detailed writeup see our guide https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How to implement supply mechanisms]. We have followed general OpenZeppelin Contracts guidelines: functions revert instead returning `false` on failure. This behavior is nonetheless conventional and does not conflict with the expectations of ERC20 applications. Additionally, an {Approval} event is emitted on calls to {transferFrom}. This allows applications to reconstruct the allowance for all accounts just by listening to said events. Other implementations of the EIP may not emit these events, as it isn't required by the specification. Finally, the non-standard {decreaseAllowance} and {increaseAllowance} functions have been added to mitigate the well-known issues around setting allowances. See {IERC20-approve}.* - -## Methods - -### allowance - -```solidity -function allowance(address owner, address spender) external view returns (uint256) -``` - - - -*See {IERC20-allowance}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| spender | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### approve - -```solidity -function approve(address spender, uint256 amount) external nonpayable returns (bool) -``` - - - -*See {IERC20-approve}. NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on `transferFrom`. This is semantically equivalent to an infinite approval. Requirements: - `spender` cannot be the zero address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### balanceOf - -```solidity -function balanceOf(address account) external view returns (uint256) -``` - - - -*See {IERC20-balanceOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### decimals - -```solidity -function decimals() external view returns (uint8) -``` - - - -*Returns the number of decimals used to get its user representation. For example, if `decimals` equals `2`, a balance of `505` tokens should be displayed to a user as `5.05` (`505 / 10 ** 2`). Tokens usually opt for a value of 18, imitating the relationship between Ether and Wei. This is the value {ERC20} uses, unless this function is overridden; NOTE: This information is only used for _display_ purposes: it in no way affects any of the arithmetic of the contract, including {IERC20-balanceOf} and {IERC20-transfer}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint8 | undefined - -### decreaseAllowance - -```solidity -function decreaseAllowance(address spender, uint256 subtractedValue) external nonpayable returns (bool) -``` - - - -*Atomically decreases the allowance granted to `spender` by the caller. This is an alternative to {approve} that can be used as a mitigation for problems described in {IERC20-approve}. Emits an {Approval} event indicating the updated allowance. Requirements: - `spender` cannot be the zero address. - `spender` must have allowance for the caller of at least `subtractedValue`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| subtractedValue | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### increaseAllowance - -```solidity -function increaseAllowance(address spender, uint256 addedValue) external nonpayable returns (bool) -``` - - - -*Atomically increases the allowance granted to `spender` by the caller. This is an alternative to {approve} that can be used as a mitigation for problems described in {IERC20-approve}. Emits an {Approval} event indicating the updated allowance. Requirements: - `spender` cannot be the zero address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| addedValue | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*Returns the name of the token.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - -*Returns the symbol of the token, usually a shorter version of the name.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - -*See {IERC20-totalSupply}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transfer - -```solidity -function transfer(address to, uint256 amount) external nonpayable returns (bool) -``` - - - -*See {IERC20-transfer}. Requirements: - `to` cannot be the zero address. - the caller must have a balance of at least `amount`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 amount) external nonpayable returns (bool) -``` - - - -*See {IERC20-transferFrom}. Emits an {Approval} event indicating the updated allowance. This is not required by the EIP. See the note at the beginning of {ERC20}. NOTE: Does not update the allowance if the current allowance is the maximum `uint256`. Requirements: - `from` and `to` cannot be the zero address. - `from` must have a balance of at least `amount`. - the caller must have allowance for ``from``'s tokens of at least `amount`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed spender, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| spender `indexed` | address | undefined | -| value | uint256 | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| value | uint256 | undefined | - - - diff --git a/docs/ERC20VotesUpgradeable.md b/docs/ERC20VotesUpgradeable.md deleted file mode 100644 index f05b559c9..000000000 --- a/docs/ERC20VotesUpgradeable.md +++ /dev/null @@ -1,551 +0,0 @@ -# ERC20VotesUpgradeable - - - - - - - -*Extension of ERC20 to support Compound-like voting and delegation. This version is more generic than Compound's, and supports token supply up to 2^224^ - 1, while COMP is limited to 2^96^ - 1. NOTE: If exact COMP compatibility is required, use the {ERC20VotesComp} variant of this module. This extension keeps a history (checkpoints) of each account's vote power. Vote power can be delegated either by calling the {delegate} function directly, or by providing a signature to be used with {delegateBySig}. Voting power can be queried through the public accessors {getVotes} and {getPastVotes}. By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. _Available since v4.2._* - -## Methods - -### DOMAIN_SEPARATOR - -```solidity -function DOMAIN_SEPARATOR() external view returns (bytes32) -``` - - - -*See {IERC20Permit-DOMAIN_SEPARATOR}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### allowance - -```solidity -function allowance(address owner, address spender) external view returns (uint256) -``` - - - -*See {IERC20-allowance}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| spender | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### approve - -```solidity -function approve(address spender, uint256 amount) external nonpayable returns (bool) -``` - - - -*See {IERC20-approve}. NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on `transferFrom`. This is semantically equivalent to an infinite approval. Requirements: - `spender` cannot be the zero address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### balanceOf - -```solidity -function balanceOf(address account) external view returns (uint256) -``` - - - -*See {IERC20-balanceOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### checkpoints - -```solidity -function checkpoints(address account, uint32 pos) external view returns (struct ERC20VotesUpgradeable.Checkpoint) -``` - - - -*Get the `pos`-th checkpoint for `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| pos | uint32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | ERC20VotesUpgradeable.Checkpoint | undefined - -### decimals - -```solidity -function decimals() external view returns (uint8) -``` - - - -*Returns the number of decimals used to get its user representation. For example, if `decimals` equals `2`, a balance of `505` tokens should be displayed to a user as `5.05` (`505 / 10 ** 2`). Tokens usually opt for a value of 18, imitating the relationship between Ether and Wei. This is the value {ERC20} uses, unless this function is overridden; NOTE: This information is only used for _display_ purposes: it in no way affects any of the arithmetic of the contract, including {IERC20-balanceOf} and {IERC20-transfer}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint8 | undefined - -### decreaseAllowance - -```solidity -function decreaseAllowance(address spender, uint256 subtractedValue) external nonpayable returns (bool) -``` - - - -*Atomically decreases the allowance granted to `spender` by the caller. This is an alternative to {approve} that can be used as a mitigation for problems described in {IERC20-approve}. Emits an {Approval} event indicating the updated allowance. Requirements: - `spender` cannot be the zero address. - `spender` must have allowance for the caller of at least `subtractedValue`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| subtractedValue | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### delegate - -```solidity -function delegate(address delegatee) external nonpayable -``` - - - -*Delegate votes from the sender to `delegatee`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| delegatee | address | undefined - -### delegateBySig - -```solidity -function delegateBySig(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) external nonpayable -``` - - - -*Delegates votes from signer to `delegatee`* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| delegatee | address | undefined -| nonce | uint256 | undefined -| expiry | uint256 | undefined -| v | uint8 | undefined -| r | bytes32 | undefined -| s | bytes32 | undefined - -### delegates - -```solidity -function delegates(address account) external view returns (address) -``` - - - -*Get the address `account` is currently delegating to.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getPastTotalSupply - -```solidity -function getPastTotalSupply(uint256 blockNumber) external view returns (uint256) -``` - - - -*Retrieve the `totalSupply` at the end of `blockNumber`. Note, this value is the sum of all balances. It is but NOT the sum of all the delegated votes! Requirements: - `blockNumber` must have been already mined* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| blockNumber | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getPastVotes - -```solidity -function getPastVotes(address account, uint256 blockNumber) external view returns (uint256) -``` - - - -*Retrieve the number of votes for `account` at the end of `blockNumber`. Requirements: - `blockNumber` must have been already mined* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| blockNumber | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getVotes - -```solidity -function getVotes(address account) external view returns (uint256) -``` - - - -*Gets the current votes balance for `account`* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### increaseAllowance - -```solidity -function increaseAllowance(address spender, uint256 addedValue) external nonpayable returns (bool) -``` - - - -*Atomically increases the allowance granted to `spender` by the caller. This is an alternative to {approve} that can be used as a mitigation for problems described in {IERC20-approve}. Emits an {Approval} event indicating the updated allowance. Requirements: - `spender` cannot be the zero address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| addedValue | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*Returns the name of the token.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### nonces - -```solidity -function nonces(address owner) external view returns (uint256) -``` - - - -*See {IERC20Permit-nonces}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### numCheckpoints - -```solidity -function numCheckpoints(address account) external view returns (uint32) -``` - - - -*Get number of checkpoints for `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint32 | undefined - -### permit - -```solidity -function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external nonpayable -``` - - - -*See {IERC20Permit-permit}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| spender | address | undefined -| value | uint256 | undefined -| deadline | uint256 | undefined -| v | uint8 | undefined -| r | bytes32 | undefined -| s | bytes32 | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - -*Returns the symbol of the token, usually a shorter version of the name.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - -*See {IERC20-totalSupply}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transfer - -```solidity -function transfer(address to, uint256 amount) external nonpayable returns (bool) -``` - - - -*See {IERC20-transfer}. Requirements: - `to` cannot be the zero address. - the caller must have a balance of at least `amount`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 amount) external nonpayable returns (bool) -``` - - - -*See {IERC20-transferFrom}. Emits an {Approval} event indicating the updated allowance. This is not required by the EIP. See the note at the beginning of {ERC20}. NOTE: Does not update the allowance if the current allowance is the maximum `uint256`. Requirements: - `from` and `to` cannot be the zero address. - `from` must have a balance of at least `amount`. - the caller must have allowance for ``from``'s tokens of at least `amount`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed spender, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| spender `indexed` | address | undefined | -| value | uint256 | undefined | - -### DelegateChanged - -```solidity -event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| delegator `indexed` | address | undefined | -| fromDelegate `indexed` | address | undefined | -| toDelegate `indexed` | address | undefined | - -### DelegateVotesChanged - -```solidity -event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| delegate `indexed` | address | undefined | -| previousBalance | uint256 | undefined | -| newBalance | uint256 | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| value | uint256 | undefined | - - - diff --git a/docs/ERC2771Context.md b/docs/ERC2771Context.md deleted file mode 100644 index d7cb75b4e..000000000 --- a/docs/ERC2771Context.md +++ /dev/null @@ -1,37 +0,0 @@ -# ERC2771Context - - - - - - - -*Context variant with ERC2771 support.* - -## Methods - -### isTrustedForwarder - -```solidity -function isTrustedForwarder(address forwarder) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| forwarder | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - - diff --git a/docs/ERC2771ContextUpgradeable.md b/docs/ERC2771ContextUpgradeable.md deleted file mode 100644 index 22d3a8682..000000000 --- a/docs/ERC2771ContextUpgradeable.md +++ /dev/null @@ -1,37 +0,0 @@ -# ERC2771ContextUpgradeable - - - - - - - -*Context variant with ERC2771 support.* - -## Methods - -### isTrustedForwarder - -```solidity -function isTrustedForwarder(address forwarder) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| forwarder | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - - diff --git a/docs/ERC721A.md b/docs/ERC721A.md deleted file mode 100644 index 5ee2eea53..000000000 --- a/docs/ERC721A.md +++ /dev/null @@ -1,473 +0,0 @@ -# ERC721A - - - - - - - -*Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including the Metadata extension. Built to optimize for lower gas during batch mints. Assumes serials are sequentially minted starting at _startTokenId() (defaults to 0, e.g. 0, 1, 2, 3..). Assumes that an owner cannot have more than 2**64 - 1 (max value of uint64) of supply. Assumes that the maximum token id cannot exceed 2**256 - 1 (max value of uint256).* - -## Methods - -### approve - -```solidity -function approve(address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-approve}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| tokenId | uint256 | undefined - -### balanceOf - -```solidity -function balanceOf(address owner) external view returns (uint256) -``` - - - -*See {IERC721-balanceOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getApproved - -```solidity -function getApproved(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-getApproved}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address owner, address operator) external view returns (bool) -``` - - - -*See {IERC721-isApprovedForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*See {IERC721Metadata-name}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### ownerOf - -```solidity -function ownerOf(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-ownerOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 tokenId, bytes _data) external nonpayable -``` - - - -*See {IERC721-safeTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined -| _data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool approved) external nonpayable -``` - - - -*See {IERC721-setApprovalForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| approved | bool | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - -*See {IERC721Metadata-symbol}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### tokenURI - -```solidity -function tokenURI(uint256 tokenId) external view returns (string) -``` - - - -*See {IERC721Metadata-tokenURI}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - -*Burned tokens are calculated here, use _totalMinted() if you want to count just minted tokens.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-transferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| approved `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed owner, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - - - -## Errors - -### ApprovalCallerNotOwnerNorApproved - -```solidity -error ApprovalCallerNotOwnerNorApproved() -``` - -The caller must own the token or be an approved operator. - - - - -### ApprovalQueryForNonexistentToken - -```solidity -error ApprovalQueryForNonexistentToken() -``` - -The token does not exist. - - - - -### ApprovalToCurrentOwner - -```solidity -error ApprovalToCurrentOwner() -``` - -The caller cannot approve to the current owner. - - - - -### ApproveToCaller - -```solidity -error ApproveToCaller() -``` - -The caller cannot approve to their own address. - - - - -### BalanceQueryForZeroAddress - -```solidity -error BalanceQueryForZeroAddress() -``` - -Cannot query the balance for the zero address. - - - - -### MintToZeroAddress - -```solidity -error MintToZeroAddress() -``` - -Cannot mint to the zero address. - - - - -### MintZeroQuantity - -```solidity -error MintZeroQuantity() -``` - -The quantity of tokens minted must be more than zero. - - - - -### OwnerQueryForNonexistentToken - -```solidity -error OwnerQueryForNonexistentToken() -``` - -The token does not exist. - - - - -### TransferCallerNotOwnerNorApproved - -```solidity -error TransferCallerNotOwnerNorApproved() -``` - -The caller must own the token or be an approved operator. - - - - -### TransferFromIncorrectOwner - -```solidity -error TransferFromIncorrectOwner() -``` - -The token must be owned by `from`. - - - - -### TransferToNonERC721ReceiverImplementer - -```solidity -error TransferToNonERC721ReceiverImplementer() -``` - -Cannot safely transfer to a contract that does not implement the ERC721Receiver interface. - - - - -### TransferToZeroAddress - -```solidity -error TransferToZeroAddress() -``` - -Cannot transfer to the zero address. - - - - -### URIQueryForNonexistentToken - -```solidity -error URIQueryForNonexistentToken() -``` - -The token does not exist. - - - - - diff --git a/docs/ERC721AUpgradeable.md b/docs/ERC721AUpgradeable.md deleted file mode 100644 index f1665380f..000000000 --- a/docs/ERC721AUpgradeable.md +++ /dev/null @@ -1,473 +0,0 @@ -# ERC721AUpgradeable - - - - - - - -*Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including the Metadata extension. Built to optimize for lower gas during batch mints. Assumes serials are sequentially minted starting at _startTokenId() (defaults to 0, e.g. 0, 1, 2, 3..). Assumes that an owner cannot have more than 2**64 - 1 (max value of uint64) of supply. Assumes that the maximum token id cannot exceed 2**256 - 1 (max value of uint256).* - -## Methods - -### approve - -```solidity -function approve(address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-approve}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| tokenId | uint256 | undefined - -### balanceOf - -```solidity -function balanceOf(address owner) external view returns (uint256) -``` - - - -*See {IERC721-balanceOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getApproved - -```solidity -function getApproved(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-getApproved}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address owner, address operator) external view returns (bool) -``` - - - -*See {IERC721-isApprovedForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*See {IERC721Metadata-name}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### ownerOf - -```solidity -function ownerOf(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-ownerOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 tokenId, bytes _data) external nonpayable -``` - - - -*See {IERC721-safeTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined -| _data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool approved) external nonpayable -``` - - - -*See {IERC721-setApprovalForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| approved | bool | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - -*See {IERC721Metadata-symbol}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### tokenURI - -```solidity -function tokenURI(uint256 tokenId) external view returns (string) -``` - - - -*See {IERC721Metadata-tokenURI}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - -*Burned tokens are calculated here, use _totalMinted() if you want to count just minted tokens.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-transferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| approved `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed owner, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - - - -## Errors - -### ApprovalCallerNotOwnerNorApproved - -```solidity -error ApprovalCallerNotOwnerNorApproved() -``` - -The caller must own the token or be an approved operator. - - - - -### ApprovalQueryForNonexistentToken - -```solidity -error ApprovalQueryForNonexistentToken() -``` - -The token does not exist. - - - - -### ApprovalToCurrentOwner - -```solidity -error ApprovalToCurrentOwner() -``` - -The caller cannot approve to the current owner. - - - - -### ApproveToCaller - -```solidity -error ApproveToCaller() -``` - -The caller cannot approve to their own address. - - - - -### BalanceQueryForZeroAddress - -```solidity -error BalanceQueryForZeroAddress() -``` - -Cannot query the balance for the zero address. - - - - -### MintToZeroAddress - -```solidity -error MintToZeroAddress() -``` - -Cannot mint to the zero address. - - - - -### MintZeroQuantity - -```solidity -error MintZeroQuantity() -``` - -The quantity of tokens minted must be more than zero. - - - - -### OwnerQueryForNonexistentToken - -```solidity -error OwnerQueryForNonexistentToken() -``` - -The token does not exist. - - - - -### TransferCallerNotOwnerNorApproved - -```solidity -error TransferCallerNotOwnerNorApproved() -``` - -The caller must own the token or be an approved operator. - - - - -### TransferFromIncorrectOwner - -```solidity -error TransferFromIncorrectOwner() -``` - -The token must be owned by `from`. - - - - -### TransferToNonERC721ReceiverImplementer - -```solidity -error TransferToNonERC721ReceiverImplementer() -``` - -Cannot safely transfer to a contract that does not implement the ERC721Receiver interface. - - - - -### TransferToZeroAddress - -```solidity -error TransferToZeroAddress() -``` - -Cannot transfer to the zero address. - - - - -### URIQueryForNonexistentToken - -```solidity -error URIQueryForNonexistentToken() -``` - -The token does not exist. - - - - - diff --git a/docs/ERC721Base.md b/docs/ERC721Base.md deleted file mode 100644 index 4f71272d8..000000000 --- a/docs/ERC721Base.md +++ /dev/null @@ -1,838 +0,0 @@ -# ERC721Base - - - - - -The `ERC721Base` smart contract implements the ERC721 NFT standard, along with the ERC721A optimization to the standard. It includes the following additions to standard ERC721 logic: - Ability to mint NFTs via the provided `mint` function. - Contract metadata for royalty support on platforms such as OpenSea that use off-chain information to distribute roaylties. - Ownership of the contract, with the ability to restrict certain functions to only be called by the contract's owner. - Multicall capability to perform multiple actions atomically - EIP 2981 compliance for royalty support on NFT marketplaces. - - - -## Methods - -### approve - -```solidity -function approve(address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-approve}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| tokenId | uint256 | undefined - -### balanceOf - -```solidity -function balanceOf(address owner) external view returns (uint256) -``` - - - -*See {IERC721-balanceOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### batchMintTo - -```solidity -function batchMintTo(address _to, uint256 _quantity, string _baseURI, bytes _data) external nonpayable -``` - -Lets an authorized address mint multiple NFTs at once to a recipient. - -*The logic in the `_canMint` function determines whether the caller is authorized to mint NFTs.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _to | address | The recipient of the NFT to mint. -| _quantity | uint256 | The number of NFTs to mint. -| _baseURI | string | The baseURI for the `n` number of NFTs minted. The metadata for each NFT is `baseURI/tokenId` -| _data | bytes | Additional data to pass along during the minting of the NFT. - -### burn - -```solidity -function burn(uint256 _tokenId) external nonpayable -``` - -Lets an owner or approved operator burn the NFT of the given tokenId. - -*ERC721A's `_burn(uint256,bool)` internally checks for token approvals.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | The tokenId of the NFT to burn. - -### contractURI - -```solidity -function contractURI() external view returns (string) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### getApproved - -```solidity -function getApproved(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-getApproved}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getBaseURICount - -```solidity -function getBaseURICount() external view returns (uint256) -``` - - - -*Returns the number of batches of tokens having the same baseURI.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getBatchIdAtIndex - -```solidity -function getBatchIdAtIndex(uint256 _index) external view returns (uint256) -``` - - - -*Returns the id for the batch of tokens the given tokenId belongs to.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getDefaultRoyaltyInfo - -```solidity -function getDefaultRoyaltyInfo() external view returns (address, uint16) -``` - - - -*Returns the default royalty recipient and bps.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### getRoyaltyInfoForToken - -```solidity -function getRoyaltyInfoForToken(uint256 _tokenId) external view returns (address, uint16) -``` - - - -*Returns the royalty recipient and bps for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address owner, address operator) external view returns (bool) -``` - - - -*See {IERC721-isApprovedForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### mintTo - -```solidity -function mintTo(address _to, string _tokenURI) external nonpayable -``` - -Lets an authorized address mint an NFT to a recipient. - -*The logic in the `_canMint` function determines whether the caller is authorized to mint NFTs.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _to | address | The recipient of the NFT to mint. -| _tokenURI | string | The full metadata URI for the NFT minted. - -### multicall - -```solidity -function multicall(bytes[] data) external nonpayable returns (bytes[] results) -``` - - - -*Receives and executes a batch of function calls on this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| results | bytes[] | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*See {IERC721Metadata-name}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### nextTokenIdToMint - -```solidity -function nextTokenIdToMint() external view returns (uint256) -``` - -The tokenId assigned to the next new NFT to be minted. - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### owner - -```solidity -function owner() external view returns (address) -``` - - - -*Returns the owner of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### ownerOf - -```solidity -function ownerOf(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-ownerOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### royaltyInfo - -```solidity -function royaltyInfo(uint256 tokenId, uint256 salePrice) external view returns (address receiver, uint256 royaltyAmount) -``` - - - -*Returns the royalty recipient and amount, given a tokenId and sale price.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined -| salePrice | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| receiver | address | undefined -| royaltyAmount | uint256 | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 tokenId, bytes _data) external nonpayable -``` - - - -*See {IERC721-safeTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined -| _data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool approved) external nonpayable -``` - - - -*See {IERC721-setApprovalForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| approved | bool | undefined - -### setContractURI - -```solidity -function setContractURI(string _uri) external nonpayable -``` - - - -*Lets a contract admin set the URI for contract-level metadata.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _uri | string | undefined - -### setDefaultRoyaltyInfo - -```solidity -function setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) external nonpayable -``` - - - -*Lets a contract admin update the default royalty recipient and bps.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _royaltyRecipient | address | undefined -| _royaltyBps | uint256 | undefined - -### setOwner - -```solidity -function setOwner(address _newOwner) external nonpayable -``` - - - -*Lets a contract admin set a new owner for the contract. The new owner must be a contract admin.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _newOwner | address | undefined - -### setRoyaltyInfoForToken - -```solidity -function setRoyaltyInfoForToken(uint256 _tokenId, address _recipient, uint256 _bps) external nonpayable -``` - - - -*Lets a contract admin set the royalty recipient and bps for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined -| _recipient | address | undefined -| _bps | uint256 | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See ERC165: https://eips.ethereum.org/EIPS/eip-165* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - -*See {IERC721Metadata-symbol}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### tokenURI - -```solidity -function tokenURI(uint256 _tokenId) external view returns (string) -``` - -Returns the metadata URI for an NFT. - -*See `BatchMintMetadata` for handling of metadata in this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | The tokenId of an NFT. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - -*Burned tokens are calculated here, use _totalMinted() if you want to count just minted tokens.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-transferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| approved `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed owner, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### ContractURIUpdated - -```solidity -event ContractURIUpdated(string prevURI, string newURI) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| prevURI | string | undefined | -| newURI | string | undefined | - -### DefaultRoyalty - -```solidity -event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| newRoyaltyRecipient `indexed` | address | undefined | -| newRoyaltyBps | uint256 | undefined | - -### OwnerUpdated - -```solidity -event OwnerUpdated(address indexed prevOwner, address indexed newOwner) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| prevOwner `indexed` | address | undefined | -| newOwner `indexed` | address | undefined | - -### RoyaltyForToken - -```solidity -event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, uint256 royaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId `indexed` | uint256 | undefined | -| royaltyRecipient `indexed` | address | undefined | -| royaltyBps | uint256 | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - - - -## Errors - -### ApprovalCallerNotOwnerNorApproved - -```solidity -error ApprovalCallerNotOwnerNorApproved() -``` - -The caller must own the token or be an approved operator. - - - - -### ApprovalQueryForNonexistentToken - -```solidity -error ApprovalQueryForNonexistentToken() -``` - -The token does not exist. - - - - -### ApprovalToCurrentOwner - -```solidity -error ApprovalToCurrentOwner() -``` - -The caller cannot approve to the current owner. - - - - -### ApproveToCaller - -```solidity -error ApproveToCaller() -``` - -The caller cannot approve to their own address. - - - - -### BalanceQueryForZeroAddress - -```solidity -error BalanceQueryForZeroAddress() -``` - -Cannot query the balance for the zero address. - - - - -### MintToZeroAddress - -```solidity -error MintToZeroAddress() -``` - -Cannot mint to the zero address. - - - - -### MintZeroQuantity - -```solidity -error MintZeroQuantity() -``` - -The quantity of tokens minted must be more than zero. - - - - -### OwnerQueryForNonexistentToken - -```solidity -error OwnerQueryForNonexistentToken() -``` - -The token does not exist. - - - - -### TransferCallerNotOwnerNorApproved - -```solidity -error TransferCallerNotOwnerNorApproved() -``` - -The caller must own the token or be an approved operator. - - - - -### TransferFromIncorrectOwner - -```solidity -error TransferFromIncorrectOwner() -``` - -The token must be owned by `from`. - - - - -### TransferToNonERC721ReceiverImplementer - -```solidity -error TransferToNonERC721ReceiverImplementer() -``` - -Cannot safely transfer to a contract that does not implement the ERC721Receiver interface. - - - - -### TransferToZeroAddress - -```solidity -error TransferToZeroAddress() -``` - -Cannot transfer to the zero address. - - - - -### URIQueryForNonexistentToken - -```solidity -error URIQueryForNonexistentToken() -``` - -The token does not exist. - - - - - diff --git a/docs/ERC721DelayedReveal.md b/docs/ERC721DelayedReveal.md deleted file mode 100644 index b897d33d9..000000000 --- a/docs/ERC721DelayedReveal.md +++ /dev/null @@ -1,1011 +0,0 @@ -# ERC721DelayedReveal - - - - - -BASE: ERC721A EXTENSION: LazyMint, DelayedReveal The `ERC721DelayedReveal` contract uses the `ERC721Base` contract, along with the `LazyMint` and `DelayedReveal` extension. 'Lazy minting' means defining the metadata of NFTs without minting it to an address. Regular 'minting' of NFTs means actually assigning an owner to an NFT. As a contract admin, this lets you prepare the metadata for NFTs that will be minted by an external party, without paying the gas cost for actually minting the NFTs. 'Delayed reveal' is a mechanism by which you can distribute NFTs to your audience and reveal the metadata of the distributed NFTs, after the fact. You can read more about how the `DelayedReveal` extension works, here: https://blog.thirdweb.com/delayed-reveal-nfts - - - -## Methods - -### approve - -```solidity -function approve(address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-approve}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| tokenId | uint256 | undefined - -### balanceOf - -```solidity -function balanceOf(address owner) external view returns (uint256) -``` - - - -*See {IERC721-balanceOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### batchMintTo - -```solidity -function batchMintTo(address _to, uint256 _quantity, string, bytes _data) external nonpayable -``` - -Lets an authorized address mint multiple lazy minted NFTs at once to a recipient. - -*The logic in the `_canMint` function determines whether the caller is authorized to mint NFTs.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _to | address | The recipient of the NFT to mint. -| _quantity | uint256 | The number of NFTs to mint. -| _2 | string | undefined -| _data | bytes | Additional data to pass along during the minting of the NFT. - -### burn - -```solidity -function burn(uint256 _tokenId) external nonpayable -``` - -Lets an owner or approved operator burn the NFT of the given tokenId. - -*ERC721A's `_burn(uint256,bool)` internally checks for token approvals.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | The tokenId of the NFT to burn. - -### contractURI - -```solidity -function contractURI() external view returns (string) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### encryptDecrypt - -```solidity -function encryptDecrypt(bytes data, bytes key) external pure returns (bytes result) -``` - - - -*See: https://ethereum.stackexchange.com/questions/69825/decrypt-message-on-chain* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes | undefined -| key | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| result | bytes | undefined - -### encryptedBaseURI - -```solidity -function encryptedBaseURI(uint256) external view returns (bytes) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes | undefined - -### getApproved - -```solidity -function getApproved(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-getApproved}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getBaseURICount - -```solidity -function getBaseURICount() external view returns (uint256) -``` - - - -*Returns the number of batches of tokens having the same baseURI.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getBatchIdAtIndex - -```solidity -function getBatchIdAtIndex(uint256 _index) external view returns (uint256) -``` - - - -*Returns the id for the batch of tokens the given tokenId belongs to.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getDefaultRoyaltyInfo - -```solidity -function getDefaultRoyaltyInfo() external view returns (address, uint16) -``` - - - -*Returns the default royalty recipient and bps.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### getRevealURI - -```solidity -function getRevealURI(uint256 _batchId, bytes _key) external view returns (string revealedURI) -``` - - - -*Returns the decrypted i.e. revealed URI for a batch of tokens.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _batchId | uint256 | undefined -| _key | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| revealedURI | string | undefined - -### getRoyaltyInfoForToken - -```solidity -function getRoyaltyInfoForToken(uint256 _tokenId) external view returns (address, uint16) -``` - - - -*Returns the royalty recipient and bps for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address owner, address operator) external view returns (bool) -``` - - - -*See {IERC721-isApprovedForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### isEncryptedBatch - -```solidity -function isEncryptedBatch(uint256 _batchId) external view returns (bool) -``` - - - -*Returns whether the relvant batch of NFTs is subject to a delayed reveal.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _batchId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### lazyMint - -```solidity -function lazyMint(uint256 _amount, string _baseURIForTokens, bytes _encryptedBaseURI) external nonpayable returns (uint256 batchId) -``` - -Lets an authorized address lazy mint a given amount of NFTs. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _amount | uint256 | The number of NFTs to lazy mint. -| _baseURIForTokens | string | The placeholder base URI for the 'n' number of NFTs being lazy minted, where the metadata for each of those NFTs is `${baseURIForTokens}/${tokenId}`. -| _encryptedBaseURI | bytes | The encrypted base URI for the batch of NFTs being lazy minted. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| batchId | uint256 | A unique integer identifier for the batch of NFTs lazy minted together. - -### mintTo - -```solidity -function mintTo(address _to, string) external nonpayable -``` - -Lets an authorized address mint a lazy minted NFT to a recipient. - -*The logic in the `_canMint` function determines whether the caller is authorized to mint NFTs.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _to | address | The recipient of the NFT to mint. -| _1 | string | undefined - -### multicall - -```solidity -function multicall(bytes[] data) external nonpayable returns (bytes[] results) -``` - - - -*Receives and executes a batch of function calls on this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| results | bytes[] | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*See {IERC721Metadata-name}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### nextTokenIdToMint - -```solidity -function nextTokenIdToMint() external view returns (uint256) -``` - -The tokenId assigned to the next new NFT to be lazy minted. - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### owner - -```solidity -function owner() external view returns (address) -``` - - - -*Returns the owner of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### ownerOf - -```solidity -function ownerOf(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-ownerOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### reveal - -```solidity -function reveal(uint256 _index, bytes _key) external nonpayable returns (string revealedURI) -``` - -Lets an authorized address reveal a batch of delayed reveal NFTs. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _index | uint256 | The ID for the batch of delayed-reveal NFTs to reveal. -| _key | bytes | The key with which the base URI for the relevant batch of NFTs was encrypted. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| revealedURI | string | undefined - -### royaltyInfo - -```solidity -function royaltyInfo(uint256 tokenId, uint256 salePrice) external view returns (address receiver, uint256 royaltyAmount) -``` - - - -*Returns the royalty recipient and amount, given a tokenId and sale price.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined -| salePrice | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| receiver | address | undefined -| royaltyAmount | uint256 | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 tokenId, bytes _data) external nonpayable -``` - - - -*See {IERC721-safeTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined -| _data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool approved) external nonpayable -``` - - - -*See {IERC721-setApprovalForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| approved | bool | undefined - -### setContractURI - -```solidity -function setContractURI(string _uri) external nonpayable -``` - - - -*Lets a contract admin set the URI for contract-level metadata.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _uri | string | undefined - -### setDefaultRoyaltyInfo - -```solidity -function setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) external nonpayable -``` - - - -*Lets a contract admin update the default royalty recipient and bps.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _royaltyRecipient | address | undefined -| _royaltyBps | uint256 | undefined - -### setOwner - -```solidity -function setOwner(address _newOwner) external nonpayable -``` - - - -*Lets a contract admin set a new owner for the contract. The new owner must be a contract admin.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _newOwner | address | undefined - -### setRoyaltyInfoForToken - -```solidity -function setRoyaltyInfoForToken(uint256 _tokenId, address _recipient, uint256 _bps) external nonpayable -``` - - - -*Lets a contract admin set the royalty recipient and bps for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined -| _recipient | address | undefined -| _bps | uint256 | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See ERC165: https://eips.ethereum.org/EIPS/eip-165* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - -*See {IERC721Metadata-symbol}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### tokenURI - -```solidity -function tokenURI(uint256 _tokenId) external view returns (string) -``` - -Returns the metadata URI for an NFT. - -*See `BatchMintMetadata` for handling of metadata in this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | The tokenId of an NFT. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - -*Burned tokens are calculated here, use _totalMinted() if you want to count just minted tokens.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-transferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| approved `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed owner, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### ContractURIUpdated - -```solidity -event ContractURIUpdated(string prevURI, string newURI) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| prevURI | string | undefined | -| newURI | string | undefined | - -### DefaultRoyalty - -```solidity -event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| newRoyaltyRecipient `indexed` | address | undefined | -| newRoyaltyBps | uint256 | undefined | - -### OwnerUpdated - -```solidity -event OwnerUpdated(address indexed prevOwner, address indexed newOwner) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| prevOwner `indexed` | address | undefined | -| newOwner `indexed` | address | undefined | - -### RoyaltyForToken - -```solidity -event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, uint256 royaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId `indexed` | uint256 | undefined | -| royaltyRecipient `indexed` | address | undefined | -| royaltyBps | uint256 | undefined | - -### TokenURIRevealed - -```solidity -event TokenURIRevealed(uint256 indexed index, string revealedURI) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| index `indexed` | uint256 | undefined | -| revealedURI | string | undefined | - -### TokensLazyMinted - -```solidity -event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| startTokenId `indexed` | uint256 | undefined | -| endTokenId | uint256 | undefined | -| baseURI | string | undefined | -| encryptedBaseURI | bytes | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - - - -## Errors - -### ApprovalCallerNotOwnerNorApproved - -```solidity -error ApprovalCallerNotOwnerNorApproved() -``` - -The caller must own the token or be an approved operator. - - - - -### ApprovalQueryForNonexistentToken - -```solidity -error ApprovalQueryForNonexistentToken() -``` - -The token does not exist. - - - - -### ApprovalToCurrentOwner - -```solidity -error ApprovalToCurrentOwner() -``` - -The caller cannot approve to the current owner. - - - - -### ApproveToCaller - -```solidity -error ApproveToCaller() -``` - -The caller cannot approve to their own address. - - - - -### BalanceQueryForZeroAddress - -```solidity -error BalanceQueryForZeroAddress() -``` - -Cannot query the balance for the zero address. - - - - -### MintToZeroAddress - -```solidity -error MintToZeroAddress() -``` - -Cannot mint to the zero address. - - - - -### MintZeroQuantity - -```solidity -error MintZeroQuantity() -``` - -The quantity of tokens minted must be more than zero. - - - - -### OwnerQueryForNonexistentToken - -```solidity -error OwnerQueryForNonexistentToken() -``` - -The token does not exist. - - - - -### TransferCallerNotOwnerNorApproved - -```solidity -error TransferCallerNotOwnerNorApproved() -``` - -The caller must own the token or be an approved operator. - - - - -### TransferFromIncorrectOwner - -```solidity -error TransferFromIncorrectOwner() -``` - -The token must be owned by `from`. - - - - -### TransferToNonERC721ReceiverImplementer - -```solidity -error TransferToNonERC721ReceiverImplementer() -``` - -Cannot safely transfer to a contract that does not implement the ERC721Receiver interface. - - - - -### TransferToZeroAddress - -```solidity -error TransferToZeroAddress() -``` - -Cannot transfer to the zero address. - - - - -### URIQueryForNonexistentToken - -```solidity -error URIQueryForNonexistentToken() -``` - -The token does not exist. - - - - - diff --git a/docs/ERC721Drop.md b/docs/ERC721Drop.md deleted file mode 100644 index 71cd1811b..000000000 --- a/docs/ERC721Drop.md +++ /dev/null @@ -1,1292 +0,0 @@ -# ERC721Drop - - - - - -BASE: ERC721A EXTENSION: SignatureMintERC721, DropSinglePhase The `ERC721Drop` contract uses the `ERC721Base` contract, along with the `SignatureMintERC721` and `DropSinglePhase` extension. The 'signature minting' mechanism in the `SignatureMintERC721` extension is a way for a contract admin to authorize an external party's request to mint tokens on the admin's contract. At a high level, this means you can authorize some external party to mint tokens on your contract, and specify what exactly will be minted by that external party. The `drop` mechanism in the `DropSinglePhase` extension is a distribution mechanism for lazy minted tokens. It lets you set restrictions such as a price to charge, an allowlist etc. when an address atttempts to mint lazy minted tokens. The `ERC721Drop` contract lets you lazy mint tokens, and distribute those lazy minted tokens via signature minting, or via the drop mechanism. - - - -## Methods - -### approve - -```solidity -function approve(address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-approve}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| tokenId | uint256 | undefined - -### balanceOf - -```solidity -function balanceOf(address owner) external view returns (uint256) -``` - - - -*See {IERC721-balanceOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### batchMintTo - -```solidity -function batchMintTo(address, uint256, string, bytes) external nonpayable -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint256 | undefined -| _2 | string | undefined -| _3 | bytes | undefined - -### burn - -```solidity -function burn(uint256 _tokenId) external nonpayable -``` - -Lets an owner or approved operator burn the NFT of the given tokenId. - -*ERC721A's `_burn(uint256,bool)` internally checks for token approvals.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | The tokenId of the NFT to burn. - -### claim - -```solidity -function claim(address _receiver, uint256 _quantity, address _currency, uint256 _pricePerToken, IDropSinglePhase.AllowlistProof _allowlistProof, bytes _data) external payable -``` - - - -*Lets an account claim tokens.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _receiver | address | undefined -| _quantity | uint256 | undefined -| _currency | address | undefined -| _pricePerToken | uint256 | undefined -| _allowlistProof | IDropSinglePhase.AllowlistProof | undefined -| _data | bytes | undefined - -### claimCondition - -```solidity -function claimCondition() external view returns (uint256 startTimestamp, uint256 maxClaimableSupply, uint256 supplyClaimed, uint256 quantityLimitPerTransaction, uint256 waitTimeInSecondsBetweenClaims, bytes32 merkleRoot, uint256 pricePerToken, address currency) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| startTimestamp | uint256 | undefined -| maxClaimableSupply | uint256 | undefined -| supplyClaimed | uint256 | undefined -| quantityLimitPerTransaction | uint256 | undefined -| waitTimeInSecondsBetweenClaims | uint256 | undefined -| merkleRoot | bytes32 | undefined -| pricePerToken | uint256 | undefined -| currency | address | undefined - -### contractURI - -```solidity -function contractURI() external view returns (string) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### encryptDecrypt - -```solidity -function encryptDecrypt(bytes data, bytes key) external pure returns (bytes result) -``` - - - -*See: https://ethereum.stackexchange.com/questions/69825/decrypt-message-on-chain* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes | undefined -| key | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| result | bytes | undefined - -### encryptedBaseURI - -```solidity -function encryptedBaseURI(uint256) external view returns (bytes) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes | undefined - -### getApproved - -```solidity -function getApproved(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-getApproved}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getBaseURICount - -```solidity -function getBaseURICount() external view returns (uint256) -``` - - - -*Returns the number of batches of tokens having the same baseURI.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getBatchIdAtIndex - -```solidity -function getBatchIdAtIndex(uint256 _index) external view returns (uint256) -``` - - - -*Returns the id for the batch of tokens the given tokenId belongs to.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getClaimTimestamp - -```solidity -function getClaimTimestamp(address _claimer) external view returns (uint256 lastClaimedAt, uint256 nextValidClaimTimestamp) -``` - - - -*Returns the timestamp for when a claimer is eligible for claiming NFTs again.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _claimer | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| lastClaimedAt | uint256 | undefined -| nextValidClaimTimestamp | uint256 | undefined - -### getDefaultRoyaltyInfo - -```solidity -function getDefaultRoyaltyInfo() external view returns (address, uint16) -``` - - - -*Returns the default royalty recipient and bps.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### getRevealURI - -```solidity -function getRevealURI(uint256 _batchId, bytes _key) external view returns (string revealedURI) -``` - - - -*Returns the decrypted i.e. revealed URI for a batch of tokens.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _batchId | uint256 | undefined -| _key | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| revealedURI | string | undefined - -### getRoyaltyInfoForToken - -```solidity -function getRoyaltyInfoForToken(uint256 _tokenId) external view returns (address, uint16) -``` - - - -*Returns the royalty recipient and bps for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address owner, address operator) external view returns (bool) -``` - - - -*See {IERC721-isApprovedForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### isEncryptedBatch - -```solidity -function isEncryptedBatch(uint256 _batchId) external view returns (bool) -``` - - - -*Returns whether the relvant batch of NFTs is subject to a delayed reveal.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _batchId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### lazyMint - -```solidity -function lazyMint(uint256 _amount, string _baseURIForTokens, bytes _encryptedBaseURI) external nonpayable returns (uint256 batchId) -``` - -Lets an authorized address lazy mint a given amount of NFTs. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _amount | uint256 | The number of NFTs to lazy mint. -| _baseURIForTokens | string | The placeholder base URI for the 'n' number of NFTs being lazy minted, where the metadata for each of those NFTs is `${baseURIForTokens}/${tokenId}`. -| _encryptedBaseURI | bytes | The encrypted base URI for the batch of NFTs being lazy minted. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| batchId | uint256 | A unique integer identifier for the batch of NFTs lazy minted together. - -### mintTo - -```solidity -function mintTo(address, string) external nonpayable -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | string | undefined - -### mintWithSignature - -```solidity -function mintWithSignature(ISignatureMintERC721.MintRequest _req, bytes _signature) external payable returns (address signer) -``` - -Mints tokens according to the provided mint request. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _req | ISignatureMintERC721.MintRequest | The payload / mint request. -| _signature | bytes | The signature produced by an account signing the mint request. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| signer | address | undefined - -### multicall - -```solidity -function multicall(bytes[] data) external nonpayable returns (bytes[] results) -``` - - - -*Receives and executes a batch of function calls on this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| results | bytes[] | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*See {IERC721Metadata-name}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### nextTokenIdToMint - -```solidity -function nextTokenIdToMint() external view returns (uint256) -``` - -The tokenId assigned to the next new NFT to be lazy minted. - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### owner - -```solidity -function owner() external view returns (address) -``` - - - -*Returns the owner of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### ownerOf - -```solidity -function ownerOf(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-ownerOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### primarySaleRecipient - -```solidity -function primarySaleRecipient() external view returns (address) -``` - - - -*The adress that receives all primary sales value.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### reveal - -```solidity -function reveal(uint256 _index, bytes _key) external nonpayable returns (string revealedURI) -``` - -Lets an authorized address reveal a batch of delayed reveal NFTs. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _index | uint256 | The ID for the batch of delayed-reveal NFTs to reveal. -| _key | bytes | The key with which the base URI for the relevant batch of NFTs was encrypted. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| revealedURI | string | undefined - -### royaltyInfo - -```solidity -function royaltyInfo(uint256 tokenId, uint256 salePrice) external view returns (address receiver, uint256 royaltyAmount) -``` - - - -*Returns the royalty recipient and amount, given a tokenId and sale price.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined -| salePrice | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| receiver | address | undefined -| royaltyAmount | uint256 | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 tokenId, bytes _data) external nonpayable -``` - - - -*See {IERC721-safeTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined -| _data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool approved) external nonpayable -``` - - - -*See {IERC721-setApprovalForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| approved | bool | undefined - -### setClaimConditions - -```solidity -function setClaimConditions(IClaimCondition.ClaimCondition _condition, bool _resetClaimEligibility) external nonpayable -``` - - - -*Lets a contract admin set claim conditions.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _condition | IClaimCondition.ClaimCondition | undefined -| _resetClaimEligibility | bool | undefined - -### setContractURI - -```solidity -function setContractURI(string _uri) external nonpayable -``` - - - -*Lets a contract admin set the URI for contract-level metadata.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _uri | string | undefined - -### setDefaultRoyaltyInfo - -```solidity -function setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) external nonpayable -``` - - - -*Lets a contract admin update the default royalty recipient and bps.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _royaltyRecipient | address | undefined -| _royaltyBps | uint256 | undefined - -### setOwner - -```solidity -function setOwner(address _newOwner) external nonpayable -``` - - - -*Lets a contract admin set a new owner for the contract. The new owner must be a contract admin.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _newOwner | address | undefined - -### setPrimarySaleRecipient - -```solidity -function setPrimarySaleRecipient(address _saleRecipient) external nonpayable -``` - - - -*Lets a contract admin set the recipient for all primary sales.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _saleRecipient | address | undefined - -### setRoyaltyInfoForToken - -```solidity -function setRoyaltyInfoForToken(uint256 _tokenId, address _recipient, uint256 _bps) external nonpayable -``` - - - -*Lets a contract admin set the royalty recipient and bps for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined -| _recipient | address | undefined -| _bps | uint256 | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See ERC165: https://eips.ethereum.org/EIPS/eip-165* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - -*See {IERC721Metadata-symbol}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### tokenURI - -```solidity -function tokenURI(uint256 _tokenId) external view returns (string) -``` - -Returns the metadata URI for an NFT. - -*See `BatchMintMetadata` for handling of metadata in this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | The tokenId of an NFT. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - -*Burned tokens are calculated here, use _totalMinted() if you want to count just minted tokens.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-transferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined - -### verify - -```solidity -function verify(ISignatureMintERC721.MintRequest _req, bytes _signature) external view returns (bool success, address signer) -``` - - - -*Verifies that a mint request is signed by an authorized account.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _req | ISignatureMintERC721.MintRequest | undefined -| _signature | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| success | bool | undefined -| signer | address | undefined - -### verifyClaim - -```solidity -function verifyClaim(address _claimer, uint256 _quantity, address _currency, uint256 _pricePerToken, bool verifyMaxQuantityPerTransaction) external view -``` - - - -*Checks a request to claim NFTs against the active claim condition's criteria.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _claimer | address | undefined -| _quantity | uint256 | undefined -| _currency | address | undefined -| _pricePerToken | uint256 | undefined -| verifyMaxQuantityPerTransaction | bool | undefined - -### verifyClaimMerkleProof - -```solidity -function verifyClaimMerkleProof(address _claimer, uint256 _quantity, IDropSinglePhase.AllowlistProof _allowlistProof) external view returns (bool validMerkleProof, uint256 merkleProofIndex) -``` - - - -*Checks whether a claimer meets the claim condition's allowlist criteria.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _claimer | address | undefined -| _quantity | uint256 | undefined -| _allowlistProof | IDropSinglePhase.AllowlistProof | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| validMerkleProof | bool | undefined -| merkleProofIndex | uint256 | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| approved `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed owner, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### ClaimConditionUpdated - -```solidity -event ClaimConditionUpdated(IClaimCondition.ClaimCondition condition, bool resetEligibility) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| condition | IClaimCondition.ClaimCondition | undefined | -| resetEligibility | bool | undefined | - -### ContractURIUpdated - -```solidity -event ContractURIUpdated(string prevURI, string newURI) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| prevURI | string | undefined | -| newURI | string | undefined | - -### DefaultRoyalty - -```solidity -event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| newRoyaltyRecipient `indexed` | address | undefined | -| newRoyaltyBps | uint256 | undefined | - -### OwnerUpdated - -```solidity -event OwnerUpdated(address indexed prevOwner, address indexed newOwner) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| prevOwner `indexed` | address | undefined | -| newOwner `indexed` | address | undefined | - -### PrimarySaleRecipientUpdated - -```solidity -event PrimarySaleRecipientUpdated(address indexed recipient) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| recipient `indexed` | address | undefined | - -### RoyaltyForToken - -```solidity -event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, uint256 royaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId `indexed` | uint256 | undefined | -| royaltyRecipient `indexed` | address | undefined | -| royaltyBps | uint256 | undefined | - -### TokenURIRevealed - -```solidity -event TokenURIRevealed(uint256 indexed index, string revealedURI) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| index `indexed` | uint256 | undefined | -| revealedURI | string | undefined | - -### TokensClaimed - -```solidity -event TokensClaimed(address indexed claimer, address indexed receiver, uint256 indexed startTokenId, uint256 quantityClaimed) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| claimer `indexed` | address | undefined | -| receiver `indexed` | address | undefined | -| startTokenId `indexed` | uint256 | undefined | -| quantityClaimed | uint256 | undefined | - -### TokensLazyMinted - -```solidity -event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| startTokenId `indexed` | uint256 | undefined | -| endTokenId | uint256 | undefined | -| baseURI | string | undefined | -| encryptedBaseURI | bytes | undefined | - -### TokensMintedWithSignature - -```solidity -event TokensMintedWithSignature(address indexed signer, address indexed mintedTo, uint256 indexed tokenIdMinted, ISignatureMintERC721.MintRequest mintRequest) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| signer `indexed` | address | undefined | -| mintedTo `indexed` | address | undefined | -| tokenIdMinted `indexed` | uint256 | undefined | -| mintRequest | ISignatureMintERC721.MintRequest | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - - - -## Errors - -### ApprovalCallerNotOwnerNorApproved - -```solidity -error ApprovalCallerNotOwnerNorApproved() -``` - -The caller must own the token or be an approved operator. - - - - -### ApprovalQueryForNonexistentToken - -```solidity -error ApprovalQueryForNonexistentToken() -``` - -The token does not exist. - - - - -### ApprovalToCurrentOwner - -```solidity -error ApprovalToCurrentOwner() -``` - -The caller cannot approve to the current owner. - - - - -### ApproveToCaller - -```solidity -error ApproveToCaller() -``` - -The caller cannot approve to their own address. - - - - -### BalanceQueryForZeroAddress - -```solidity -error BalanceQueryForZeroAddress() -``` - -Cannot query the balance for the zero address. - - - - -### MintToZeroAddress - -```solidity -error MintToZeroAddress() -``` - -Cannot mint to the zero address. - - - - -### MintZeroQuantity - -```solidity -error MintZeroQuantity() -``` - -The quantity of tokens minted must be more than zero. - - - - -### OwnerQueryForNonexistentToken - -```solidity -error OwnerQueryForNonexistentToken() -``` - -The token does not exist. - - - - -### TransferCallerNotOwnerNorApproved - -```solidity -error TransferCallerNotOwnerNorApproved() -``` - -The caller must own the token or be an approved operator. - - - - -### TransferFromIncorrectOwner - -```solidity -error TransferFromIncorrectOwner() -``` - -The token must be owned by `from`. - - - - -### TransferToNonERC721ReceiverImplementer - -```solidity -error TransferToNonERC721ReceiverImplementer() -``` - -Cannot safely transfer to a contract that does not implement the ERC721Receiver interface. - - - - -### TransferToZeroAddress - -```solidity -error TransferToZeroAddress() -``` - -Cannot transfer to the zero address. - - - - -### URIQueryForNonexistentToken - -```solidity -error URIQueryForNonexistentToken() -``` - -The token does not exist. - - - - - diff --git a/docs/ERC721EnumerableUpgradeable.md b/docs/ERC721EnumerableUpgradeable.md deleted file mode 100644 index 207a29383..000000000 --- a/docs/ERC721EnumerableUpgradeable.md +++ /dev/null @@ -1,372 +0,0 @@ -# ERC721EnumerableUpgradeable - - - - - - - -*This implements an optional extension of {ERC721} defined in the EIP that adds enumerability of all the token ids in the contract as well as all token ids owned by each account.* - -## Methods - -### approve - -```solidity -function approve(address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-approve}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| tokenId | uint256 | undefined - -### balanceOf - -```solidity -function balanceOf(address owner) external view returns (uint256) -``` - - - -*See {IERC721-balanceOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getApproved - -```solidity -function getApproved(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-getApproved}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address owner, address operator) external view returns (bool) -``` - - - -*See {IERC721-isApprovedForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*See {IERC721Metadata-name}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### ownerOf - -```solidity -function ownerOf(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-ownerOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 tokenId, bytes _data) external nonpayable -``` - - - -*See {IERC721-safeTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined -| _data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool approved) external nonpayable -``` - - - -*See {IERC721-setApprovalForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| approved | bool | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - -*See {IERC721Metadata-symbol}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### tokenByIndex - -```solidity -function tokenByIndex(uint256 index) external view returns (uint256) -``` - - - -*See {IERC721Enumerable-tokenByIndex}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### tokenOfOwnerByIndex - -```solidity -function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256) -``` - - - -*See {IERC721Enumerable-tokenOfOwnerByIndex}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### tokenURI - -```solidity -function tokenURI(uint256 tokenId) external view returns (string) -``` - - - -*See {IERC721Metadata-tokenURI}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - -*See {IERC721Enumerable-totalSupply}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-transferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| approved `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed owner, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - - - diff --git a/docs/ERC721Holder.md b/docs/ERC721Holder.md deleted file mode 100644 index 6727c7c6f..000000000 --- a/docs/ERC721Holder.md +++ /dev/null @@ -1,40 +0,0 @@ -# ERC721Holder - - - - - - - -*Implementation of the {IERC721Receiver} interface. Accepts all token transfers. Make sure the contract is able to use its token with {IERC721-safeTransferFrom}, {IERC721-approve} or {IERC721-setApprovalForAll}.* - -## Methods - -### onERC721Received - -```solidity -function onERC721Received(address, address, uint256, bytes) external nonpayable returns (bytes4) -``` - - - -*See {IERC721Receiver-onERC721Received}. Always returns `IERC721Receiver.onERC721Received.selector`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | address | undefined -| _2 | uint256 | undefined -| _3 | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - - - - diff --git a/docs/ERC721HolderUpgradeable.md b/docs/ERC721HolderUpgradeable.md deleted file mode 100644 index 574fbebb8..000000000 --- a/docs/ERC721HolderUpgradeable.md +++ /dev/null @@ -1,40 +0,0 @@ -# ERC721HolderUpgradeable - - - - - - - -*Implementation of the {IERC721Receiver} interface. Accepts all token transfers. Make sure the contract is able to use its token with {IERC721-safeTransferFrom}, {IERC721-approve} or {IERC721-setApprovalForAll}.* - -## Methods - -### onERC721Received - -```solidity -function onERC721Received(address, address, uint256, bytes) external nonpayable returns (bytes4) -``` - - - -*See {IERC721Receiver-onERC721Received}. Always returns `IERC721Receiver.onERC721Received.selector`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | address | undefined -| _2 | uint256 | undefined -| _3 | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - - - - diff --git a/docs/ERC721LazyMint.md b/docs/ERC721LazyMint.md deleted file mode 100644 index 59b4974a9..000000000 --- a/docs/ERC721LazyMint.md +++ /dev/null @@ -1,881 +0,0 @@ -# ERC721LazyMint - - - - - -BASE: ERC721Base EXTENSION: LazyMint The `ERC721LazyMint` contract uses the `ERC721Base` contract, along with the `LazyMint` extension. 'Lazy minting' means defining the metadata of NFTs without minting it to an address. Regular 'minting' of NFTs means actually assigning an owner to an NFT. As a contract admin, this lets you prepare the metadata for NFTs that will be minted by an external party, without paying the gas cost for actually minting the NFTs. - - - -## Methods - -### approve - -```solidity -function approve(address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-approve}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| tokenId | uint256 | undefined - -### balanceOf - -```solidity -function balanceOf(address owner) external view returns (uint256) -``` - - - -*See {IERC721-balanceOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### batchMintTo - -```solidity -function batchMintTo(address _to, uint256 _quantity, string, bytes _data) external nonpayable -``` - -Lets an authorized address mint multiple lazy minted NFTs at once to a recipient. - -*The logic in the `_canMint` function determines whether the caller is authorized to mint NFTs.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _to | address | The recipient of the NFT to mint. -| _quantity | uint256 | The number of NFTs to mint. -| _2 | string | undefined -| _data | bytes | Additional data to pass along during the minting of the NFT. - -### burn - -```solidity -function burn(uint256 _tokenId) external nonpayable -``` - -Lets an owner or approved operator burn the NFT of the given tokenId. - -*ERC721A's `_burn(uint256,bool)` internally checks for token approvals.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | The tokenId of the NFT to burn. - -### contractURI - -```solidity -function contractURI() external view returns (string) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### getApproved - -```solidity -function getApproved(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-getApproved}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getBaseURICount - -```solidity -function getBaseURICount() external view returns (uint256) -``` - - - -*Returns the number of batches of tokens having the same baseURI.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getBatchIdAtIndex - -```solidity -function getBatchIdAtIndex(uint256 _index) external view returns (uint256) -``` - - - -*Returns the id for the batch of tokens the given tokenId belongs to.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getDefaultRoyaltyInfo - -```solidity -function getDefaultRoyaltyInfo() external view returns (address, uint16) -``` - - - -*Returns the default royalty recipient and bps.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### getRoyaltyInfoForToken - -```solidity -function getRoyaltyInfoForToken(uint256 _tokenId) external view returns (address, uint16) -``` - - - -*Returns the royalty recipient and bps for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address owner, address operator) external view returns (bool) -``` - - - -*See {IERC721-isApprovedForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### lazyMint - -```solidity -function lazyMint(uint256 _amount, string _baseURIForTokens, bytes _data) external nonpayable returns (uint256 batchId) -``` - -Lets an authorized address lazy mint a given amount of NFTs. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _amount | uint256 | The number of NFTs to lazy mint. -| _baseURIForTokens | string | The base URI for the 'n' number of NFTs being lazy minted, where the metadata for each of those NFTs is `${baseURIForTokens}/${tokenId}`. -| _data | bytes | Additional bytes data to be used at the discretion of the consumer of the contract. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| batchId | uint256 | A unique integer identifier for the batch of NFTs lazy minted together. - -### mintTo - -```solidity -function mintTo(address _to, string) external nonpayable -``` - -Lets an authorized address mint a lazy minted NFT to a recipient. - -*The logic in the `_canMint` function determines whether the caller is authorized to mint NFTs.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _to | address | The recipient of the NFT to mint. -| _1 | string | undefined - -### multicall - -```solidity -function multicall(bytes[] data) external nonpayable returns (bytes[] results) -``` - - - -*Receives and executes a batch of function calls on this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| results | bytes[] | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*See {IERC721Metadata-name}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### nextTokenIdToMint - -```solidity -function nextTokenIdToMint() external view returns (uint256) -``` - -The tokenId assigned to the next new NFT to be lazy minted. - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### owner - -```solidity -function owner() external view returns (address) -``` - - - -*Returns the owner of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### ownerOf - -```solidity -function ownerOf(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-ownerOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### royaltyInfo - -```solidity -function royaltyInfo(uint256 tokenId, uint256 salePrice) external view returns (address receiver, uint256 royaltyAmount) -``` - - - -*Returns the royalty recipient and amount, given a tokenId and sale price.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined -| salePrice | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| receiver | address | undefined -| royaltyAmount | uint256 | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 tokenId, bytes _data) external nonpayable -``` - - - -*See {IERC721-safeTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined -| _data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool approved) external nonpayable -``` - - - -*See {IERC721-setApprovalForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| approved | bool | undefined - -### setContractURI - -```solidity -function setContractURI(string _uri) external nonpayable -``` - - - -*Lets a contract admin set the URI for contract-level metadata.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _uri | string | undefined - -### setDefaultRoyaltyInfo - -```solidity -function setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) external nonpayable -``` - - - -*Lets a contract admin update the default royalty recipient and bps.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _royaltyRecipient | address | undefined -| _royaltyBps | uint256 | undefined - -### setOwner - -```solidity -function setOwner(address _newOwner) external nonpayable -``` - - - -*Lets a contract admin set a new owner for the contract. The new owner must be a contract admin.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _newOwner | address | undefined - -### setRoyaltyInfoForToken - -```solidity -function setRoyaltyInfoForToken(uint256 _tokenId, address _recipient, uint256 _bps) external nonpayable -``` - - - -*Lets a contract admin set the royalty recipient and bps for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined -| _recipient | address | undefined -| _bps | uint256 | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See ERC165: https://eips.ethereum.org/EIPS/eip-165* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - -*See {IERC721Metadata-symbol}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### tokenURI - -```solidity -function tokenURI(uint256 _tokenId) external view returns (string) -``` - -Returns the metadata URI for an NFT. - -*See `BatchMintMetadata` for handling of metadata in this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | The tokenId of an NFT. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - -*Burned tokens are calculated here, use _totalMinted() if you want to count just minted tokens.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-transferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| approved `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed owner, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### ContractURIUpdated - -```solidity -event ContractURIUpdated(string prevURI, string newURI) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| prevURI | string | undefined | -| newURI | string | undefined | - -### DefaultRoyalty - -```solidity -event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| newRoyaltyRecipient `indexed` | address | undefined | -| newRoyaltyBps | uint256 | undefined | - -### OwnerUpdated - -```solidity -event OwnerUpdated(address indexed prevOwner, address indexed newOwner) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| prevOwner `indexed` | address | undefined | -| newOwner `indexed` | address | undefined | - -### RoyaltyForToken - -```solidity -event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, uint256 royaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId `indexed` | uint256 | undefined | -| royaltyRecipient `indexed` | address | undefined | -| royaltyBps | uint256 | undefined | - -### TokensLazyMinted - -```solidity -event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| startTokenId `indexed` | uint256 | undefined | -| endTokenId | uint256 | undefined | -| baseURI | string | undefined | -| encryptedBaseURI | bytes | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - - - -## Errors - -### ApprovalCallerNotOwnerNorApproved - -```solidity -error ApprovalCallerNotOwnerNorApproved() -``` - -The caller must own the token or be an approved operator. - - - - -### ApprovalQueryForNonexistentToken - -```solidity -error ApprovalQueryForNonexistentToken() -``` - -The token does not exist. - - - - -### ApprovalToCurrentOwner - -```solidity -error ApprovalToCurrentOwner() -``` - -The caller cannot approve to the current owner. - - - - -### ApproveToCaller - -```solidity -error ApproveToCaller() -``` - -The caller cannot approve to their own address. - - - - -### BalanceQueryForZeroAddress - -```solidity -error BalanceQueryForZeroAddress() -``` - -Cannot query the balance for the zero address. - - - - -### MintToZeroAddress - -```solidity -error MintToZeroAddress() -``` - -Cannot mint to the zero address. - - - - -### MintZeroQuantity - -```solidity -error MintZeroQuantity() -``` - -The quantity of tokens minted must be more than zero. - - - - -### OwnerQueryForNonexistentToken - -```solidity -error OwnerQueryForNonexistentToken() -``` - -The token does not exist. - - - - -### TransferCallerNotOwnerNorApproved - -```solidity -error TransferCallerNotOwnerNorApproved() -``` - -The caller must own the token or be an approved operator. - - - - -### TransferFromIncorrectOwner - -```solidity -error TransferFromIncorrectOwner() -``` - -The token must be owned by `from`. - - - - -### TransferToNonERC721ReceiverImplementer - -```solidity -error TransferToNonERC721ReceiverImplementer() -``` - -Cannot safely transfer to a contract that does not implement the ERC721Receiver interface. - - - - -### TransferToZeroAddress - -```solidity -error TransferToZeroAddress() -``` - -Cannot transfer to the zero address. - - - - -### URIQueryForNonexistentToken - -```solidity -error URIQueryForNonexistentToken() -``` - -The token does not exist. - - - - - diff --git a/docs/ERC721SignatureMint.md b/docs/ERC721SignatureMint.md deleted file mode 100644 index cbb019f76..000000000 --- a/docs/ERC721SignatureMint.md +++ /dev/null @@ -1,953 +0,0 @@ -# ERC721SignatureMint - - - - - -BASE: ERC721A EXTENSION: SignatureMintERC721 The `ERC721SignatureMint` contract uses the `ERC721Base` contract, along with the `SignatureMintERC721` extension. The 'signature minting' mechanism in the `SignatureMintERC721` extension uses EIP 712, and is a way for a contract admin to authorize an external party's request to mint tokens on the admin's contract. At a high level, this means you can authorize some external party to mint tokens on your contract, and specify what exactly will be minted by that external party. - - - -## Methods - -### approve - -```solidity -function approve(address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-approve}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| tokenId | uint256 | undefined - -### balanceOf - -```solidity -function balanceOf(address owner) external view returns (uint256) -``` - - - -*See {IERC721-balanceOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### batchMintTo - -```solidity -function batchMintTo(address _to, uint256 _quantity, string _baseURI, bytes _data) external nonpayable -``` - -Lets an authorized address mint multiple NFTs at once to a recipient. - -*The logic in the `_canMint` function determines whether the caller is authorized to mint NFTs.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _to | address | The recipient of the NFT to mint. -| _quantity | uint256 | The number of NFTs to mint. -| _baseURI | string | The baseURI for the `n` number of NFTs minted. The metadata for each NFT is `baseURI/tokenId` -| _data | bytes | Additional data to pass along during the minting of the NFT. - -### burn - -```solidity -function burn(uint256 _tokenId) external nonpayable -``` - -Lets an owner or approved operator burn the NFT of the given tokenId. - -*ERC721A's `_burn(uint256,bool)` internally checks for token approvals.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | The tokenId of the NFT to burn. - -### contractURI - -```solidity -function contractURI() external view returns (string) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### getApproved - -```solidity -function getApproved(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-getApproved}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getBaseURICount - -```solidity -function getBaseURICount() external view returns (uint256) -``` - - - -*Returns the number of batches of tokens having the same baseURI.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getBatchIdAtIndex - -```solidity -function getBatchIdAtIndex(uint256 _index) external view returns (uint256) -``` - - - -*Returns the id for the batch of tokens the given tokenId belongs to.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getDefaultRoyaltyInfo - -```solidity -function getDefaultRoyaltyInfo() external view returns (address, uint16) -``` - - - -*Returns the default royalty recipient and bps.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### getRoyaltyInfoForToken - -```solidity -function getRoyaltyInfoForToken(uint256 _tokenId) external view returns (address, uint16) -``` - - - -*Returns the royalty recipient and bps for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address owner, address operator) external view returns (bool) -``` - - - -*See {IERC721-isApprovedForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### mintTo - -```solidity -function mintTo(address _to, string _tokenURI) external nonpayable -``` - -Lets an authorized address mint an NFT to a recipient. - -*The logic in the `_canMint` function determines whether the caller is authorized to mint NFTs.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _to | address | The recipient of the NFT to mint. -| _tokenURI | string | The full metadata URI for the NFT minted. - -### mintWithSignature - -```solidity -function mintWithSignature(ISignatureMintERC721.MintRequest _req, bytes _signature) external payable returns (address signer) -``` - -Mints tokens according to the provided mint request. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _req | ISignatureMintERC721.MintRequest | The payload / mint request. -| _signature | bytes | The signature produced by an account signing the mint request. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| signer | address | undefined - -### multicall - -```solidity -function multicall(bytes[] data) external nonpayable returns (bytes[] results) -``` - - - -*Receives and executes a batch of function calls on this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| results | bytes[] | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*See {IERC721Metadata-name}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### nextTokenIdToMint - -```solidity -function nextTokenIdToMint() external view returns (uint256) -``` - -The tokenId assigned to the next new NFT to be minted. - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### owner - -```solidity -function owner() external view returns (address) -``` - - - -*Returns the owner of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### ownerOf - -```solidity -function ownerOf(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-ownerOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### primarySaleRecipient - -```solidity -function primarySaleRecipient() external view returns (address) -``` - - - -*The adress that receives all primary sales value.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### royaltyInfo - -```solidity -function royaltyInfo(uint256 tokenId, uint256 salePrice) external view returns (address receiver, uint256 royaltyAmount) -``` - - - -*Returns the royalty recipient and amount, given a tokenId and sale price.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined -| salePrice | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| receiver | address | undefined -| royaltyAmount | uint256 | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 tokenId, bytes _data) external nonpayable -``` - - - -*See {IERC721-safeTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined -| _data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool approved) external nonpayable -``` - - - -*See {IERC721-setApprovalForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| approved | bool | undefined - -### setContractURI - -```solidity -function setContractURI(string _uri) external nonpayable -``` - - - -*Lets a contract admin set the URI for contract-level metadata.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _uri | string | undefined - -### setDefaultRoyaltyInfo - -```solidity -function setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) external nonpayable -``` - - - -*Lets a contract admin update the default royalty recipient and bps.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _royaltyRecipient | address | undefined -| _royaltyBps | uint256 | undefined - -### setOwner - -```solidity -function setOwner(address _newOwner) external nonpayable -``` - - - -*Lets a contract admin set a new owner for the contract. The new owner must be a contract admin.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _newOwner | address | undefined - -### setPrimarySaleRecipient - -```solidity -function setPrimarySaleRecipient(address _saleRecipient) external nonpayable -``` - - - -*Lets a contract admin set the recipient for all primary sales.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _saleRecipient | address | undefined - -### setRoyaltyInfoForToken - -```solidity -function setRoyaltyInfoForToken(uint256 _tokenId, address _recipient, uint256 _bps) external nonpayable -``` - - - -*Lets a contract admin set the royalty recipient and bps for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined -| _recipient | address | undefined -| _bps | uint256 | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See ERC165: https://eips.ethereum.org/EIPS/eip-165* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - -*See {IERC721Metadata-symbol}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### tokenURI - -```solidity -function tokenURI(uint256 _tokenId) external view returns (string) -``` - -Returns the metadata URI for an NFT. - -*See `BatchMintMetadata` for handling of metadata in this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | The tokenId of an NFT. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - -*Burned tokens are calculated here, use _totalMinted() if you want to count just minted tokens.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-transferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined - -### verify - -```solidity -function verify(ISignatureMintERC721.MintRequest _req, bytes _signature) external view returns (bool success, address signer) -``` - - - -*Verifies that a mint request is signed by an authorized account.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _req | ISignatureMintERC721.MintRequest | undefined -| _signature | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| success | bool | undefined -| signer | address | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| approved `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed owner, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### ContractURIUpdated - -```solidity -event ContractURIUpdated(string prevURI, string newURI) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| prevURI | string | undefined | -| newURI | string | undefined | - -### DefaultRoyalty - -```solidity -event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| newRoyaltyRecipient `indexed` | address | undefined | -| newRoyaltyBps | uint256 | undefined | - -### OwnerUpdated - -```solidity -event OwnerUpdated(address indexed prevOwner, address indexed newOwner) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| prevOwner `indexed` | address | undefined | -| newOwner `indexed` | address | undefined | - -### PrimarySaleRecipientUpdated - -```solidity -event PrimarySaleRecipientUpdated(address indexed recipient) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| recipient `indexed` | address | undefined | - -### RoyaltyForToken - -```solidity -event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, uint256 royaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId `indexed` | uint256 | undefined | -| royaltyRecipient `indexed` | address | undefined | -| royaltyBps | uint256 | undefined | - -### TokensMintedWithSignature - -```solidity -event TokensMintedWithSignature(address indexed signer, address indexed mintedTo, uint256 indexed tokenIdMinted, ISignatureMintERC721.MintRequest mintRequest) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| signer `indexed` | address | undefined | -| mintedTo `indexed` | address | undefined | -| tokenIdMinted `indexed` | uint256 | undefined | -| mintRequest | ISignatureMintERC721.MintRequest | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - - - -## Errors - -### ApprovalCallerNotOwnerNorApproved - -```solidity -error ApprovalCallerNotOwnerNorApproved() -``` - -The caller must own the token or be an approved operator. - - - - -### ApprovalQueryForNonexistentToken - -```solidity -error ApprovalQueryForNonexistentToken() -``` - -The token does not exist. - - - - -### ApprovalToCurrentOwner - -```solidity -error ApprovalToCurrentOwner() -``` - -The caller cannot approve to the current owner. - - - - -### ApproveToCaller - -```solidity -error ApproveToCaller() -``` - -The caller cannot approve to their own address. - - - - -### BalanceQueryForZeroAddress - -```solidity -error BalanceQueryForZeroAddress() -``` - -Cannot query the balance for the zero address. - - - - -### MintToZeroAddress - -```solidity -error MintToZeroAddress() -``` - -Cannot mint to the zero address. - - - - -### MintZeroQuantity - -```solidity -error MintZeroQuantity() -``` - -The quantity of tokens minted must be more than zero. - - - - -### OwnerQueryForNonexistentToken - -```solidity -error OwnerQueryForNonexistentToken() -``` - -The token does not exist. - - - - -### TransferCallerNotOwnerNorApproved - -```solidity -error TransferCallerNotOwnerNorApproved() -``` - -The caller must own the token or be an approved operator. - - - - -### TransferFromIncorrectOwner - -```solidity -error TransferFromIncorrectOwner() -``` - -The token must be owned by `from`. - - - - -### TransferToNonERC721ReceiverImplementer - -```solidity -error TransferToNonERC721ReceiverImplementer() -``` - -Cannot safely transfer to a contract that does not implement the ERC721Receiver interface. - - - - -### TransferToZeroAddress - -```solidity -error TransferToZeroAddress() -``` - -Cannot transfer to the zero address. - - - - -### URIQueryForNonexistentToken - -```solidity -error URIQueryForNonexistentToken() -``` - -The token does not exist. - - - - - diff --git a/docs/ERC721Upgradeable.md b/docs/ERC721Upgradeable.md deleted file mode 100644 index 2d45595a3..000000000 --- a/docs/ERC721Upgradeable.md +++ /dev/null @@ -1,310 +0,0 @@ -# ERC721Upgradeable - - - - - - - -*Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including the Metadata extension, but not including the Enumerable extension, which is available separately as {ERC721Enumerable}.* - -## Methods - -### approve - -```solidity -function approve(address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-approve}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| tokenId | uint256 | undefined - -### balanceOf - -```solidity -function balanceOf(address owner) external view returns (uint256) -``` - - - -*See {IERC721-balanceOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getApproved - -```solidity -function getApproved(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-getApproved}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address owner, address operator) external view returns (bool) -``` - - - -*See {IERC721-isApprovedForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*See {IERC721Metadata-name}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### ownerOf - -```solidity -function ownerOf(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-ownerOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 tokenId, bytes _data) external nonpayable -``` - - - -*See {IERC721-safeTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined -| _data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool approved) external nonpayable -``` - - - -*See {IERC721-setApprovalForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| approved | bool | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - -*See {IERC721Metadata-symbol}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### tokenURI - -```solidity -function tokenURI(uint256 tokenId) external view returns (string) -``` - - - -*See {IERC721Metadata-tokenURI}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-transferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| approved `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed owner, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - - - diff --git a/docs/EnumerableSet.md b/docs/EnumerableSet.md deleted file mode 100644 index e5b07f61a..000000000 --- a/docs/EnumerableSet.md +++ /dev/null @@ -1,12 +0,0 @@ -# EnumerableSet - - - - - - - -*Library for managing https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive types. Sets have the following properties: - Elements are added, removed, and checked for existence in constant time (O(1)). - Elements are enumerated in O(n). No guarantees are made on the ordering. ``` contract Example { // Add the library methods using EnumerableSet for EnumerableSet.AddressSet; // Declare a set state variable EnumerableSet.AddressSet private mySet; } ``` As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`) and `uint256` (`UintSet`) are supported.* - - - diff --git a/docs/EnumerableSetUpgradeable.md b/docs/EnumerableSetUpgradeable.md deleted file mode 100644 index 1a169e579..000000000 --- a/docs/EnumerableSetUpgradeable.md +++ /dev/null @@ -1,12 +0,0 @@ -# EnumerableSetUpgradeable - - - - - - - -*Library for managing https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive types. Sets have the following properties: - Elements are added, removed, and checked for existence in constant time (O(1)). - Elements are enumerated in O(n). No guarantees are made on the ordering. ``` contract Example { // Add the library methods using EnumerableSet for EnumerableSet.AddressSet; // Declare a set state variable EnumerableSet.AddressSet private mySet; } ``` As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`) and `uint256` (`UintSet`) are supported.* - - - diff --git a/docs/FeeType.md b/docs/FeeType.md deleted file mode 100644 index 6950fdc30..000000000 --- a/docs/FeeType.md +++ /dev/null @@ -1,12 +0,0 @@ -# FeeType - - - - - - - - - - - diff --git a/docs/Forwarder.md b/docs/Forwarder.md deleted file mode 100644 index 234a6e406..000000000 --- a/docs/Forwarder.md +++ /dev/null @@ -1,84 +0,0 @@ -# Forwarder - - - - - - - - - -## Methods - -### execute - -```solidity -function execute(MinimalForwarder.ForwardRequest req, bytes signature) external payable returns (bool, bytes) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| req | MinimalForwarder.ForwardRequest | undefined -| signature | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined -| _1 | bytes | undefined - -### getNonce - -```solidity -function getNonce(address from) external view returns (uint256) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### verify - -```solidity -function verify(MinimalForwarder.ForwardRequest req, bytes signature) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| req | MinimalForwarder.ForwardRequest | undefined -| signature | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - - diff --git a/docs/GovernorCountingSimpleUpgradeable.md b/docs/GovernorCountingSimpleUpgradeable.md deleted file mode 100644 index fcb1bdbf4..000000000 --- a/docs/GovernorCountingSimpleUpgradeable.md +++ /dev/null @@ -1,559 +0,0 @@ -# GovernorCountingSimpleUpgradeable - - - - - - - -*Extension of {Governor} for simple, 3 options, vote counting. _Available since v4.3._* - -## Methods - -### BALLOT_TYPEHASH - -```solidity -function BALLOT_TYPEHASH() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### COUNTING_MODE - -```solidity -function COUNTING_MODE() external pure returns (string) -``` - - - -*See {IGovernor-COUNTING_MODE}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### castVote - -```solidity -function castVote(uint256 proposalId, uint8 support) external nonpayable returns (uint256) -``` - - - -*See {IGovernor-castVote}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| support | uint8 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### castVoteBySig - -```solidity -function castVoteBySig(uint256 proposalId, uint8 support, uint8 v, bytes32 r, bytes32 s) external nonpayable returns (uint256) -``` - - - -*See {IGovernor-castVoteBySig}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| support | uint8 | undefined -| v | uint8 | undefined -| r | bytes32 | undefined -| s | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### castVoteWithReason - -```solidity -function castVoteWithReason(uint256 proposalId, uint8 support, string reason) external nonpayable returns (uint256) -``` - - - -*See {IGovernor-castVoteWithReason}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| support | uint8 | undefined -| reason | string | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### execute - -```solidity -function execute(address[] targets, uint256[] values, bytes[] calldatas, bytes32 descriptionHash) external payable returns (uint256) -``` - - - -*See {IGovernor-execute}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| targets | address[] | undefined -| values | uint256[] | undefined -| calldatas | bytes[] | undefined -| descriptionHash | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getVotes - -```solidity -function getVotes(address account, uint256 blockNumber) external view returns (uint256) -``` - -module:reputation - -*Voting power of an `account` at a specific `blockNumber`. Note: this can be implemented in a number of ways, for example by reading the delegated balance from one (or multiple), {ERC20Votes} tokens.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| blockNumber | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### hasVoted - -```solidity -function hasVoted(uint256 proposalId, address account) external view returns (bool) -``` - - - -*See {IGovernor-hasVoted}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### hashProposal - -```solidity -function hashProposal(address[] targets, uint256[] values, bytes[] calldatas, bytes32 descriptionHash) external pure returns (uint256) -``` - - - -*See {IGovernor-hashProposal}. The proposal id is produced by hashing the RLC encoded `targets` array, the `values` array, the `calldatas` array and the descriptionHash (bytes32 which itself is the keccak256 hash of the description string). This proposal id can be produced from the proposal data which is part of the {ProposalCreated} event. It can even be computed in advance, before the proposal is submitted. Note that the chainId and the governor address are not part of the proposal id computation. Consequently, the same proposal (with same operation and same description) will have the same id if submitted on multiple governors accross multiple networks. This also means that in order to execute the same operation twice (on the same governor) the proposer will have to change the description in order to avoid proposal id conflicts.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| targets | address[] | undefined -| values | uint256[] | undefined -| calldatas | bytes[] | undefined -| descriptionHash | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*See {IGovernor-name}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### proposalDeadline - -```solidity -function proposalDeadline(uint256 proposalId) external view returns (uint256) -``` - - - -*See {IGovernor-proposalDeadline}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### proposalSnapshot - -```solidity -function proposalSnapshot(uint256 proposalId) external view returns (uint256) -``` - - - -*See {IGovernor-proposalSnapshot}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### proposalThreshold - -```solidity -function proposalThreshold() external view returns (uint256) -``` - - - -*Part of the Governor Bravo's interface: _"The number of votes required in order for a voter to become a proposer"_.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### proposalVotes - -```solidity -function proposalVotes(uint256 proposalId) external view returns (uint256 againstVotes, uint256 forVotes, uint256 abstainVotes) -``` - - - -*Accessor to the internal vote counts.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| againstVotes | uint256 | undefined -| forVotes | uint256 | undefined -| abstainVotes | uint256 | undefined - -### propose - -```solidity -function propose(address[] targets, uint256[] values, bytes[] calldatas, string description) external nonpayable returns (uint256) -``` - - - -*See {IGovernor-propose}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| targets | address[] | undefined -| values | uint256[] | undefined -| calldatas | bytes[] | undefined -| description | string | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### quorum - -```solidity -function quorum(uint256 blockNumber) external view returns (uint256) -``` - -module:user-config - -*Minimum number of cast voted required for a proposal to be successful. Note: The `blockNumber` parameter corresponds to the snaphot used for counting vote. This allows to scale the quroum depending on values such as the totalSupply of a token at this block (see {ERC20Votes}).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| blockNumber | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### relay - -```solidity -function relay(address target, uint256 value, bytes data) external nonpayable -``` - - - -*Relays a transaction or function call to an arbitrary target. In cases where the governance executor is some contract other than the governor itself, like when using a timelock, this function can be invoked in a governance proposal to recover tokens or Ether that was sent to the governor contract by mistake. Note that if the executor is simply the governor itself, use of `relay` is redundant.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| target | address | undefined -| value | uint256 | undefined -| data | bytes | undefined - -### state - -```solidity -function state(uint256 proposalId) external view returns (enum IGovernorUpgradeable.ProposalState) -``` - - - -*See {IGovernor-state}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | enum IGovernorUpgradeable.ProposalState | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### version - -```solidity -function version() external view returns (string) -``` - - - -*See {IGovernor-version}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### votingDelay - -```solidity -function votingDelay() external view returns (uint256) -``` - -module:user-config - -*Delay, in number of block, between the proposal is created and the vote starts. This can be increassed to leave time for users to buy voting power, of delegate it, before the voting of a proposal starts.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### votingPeriod - -```solidity -function votingPeriod() external view returns (uint256) -``` - -module:user-config - -*Delay, in number of blocks, between the vote start and vote ends. NOTE: The {votingDelay} can delay the start of the vote. This must be considered when setting the voting duration compared to the voting delay.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - - - -## Events - -### ProposalCanceled - -```solidity -event ProposalCanceled(uint256 proposalId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined | - -### ProposalCreated - -```solidity -event ProposalCreated(uint256 proposalId, address proposer, address[] targets, uint256[] values, string[] signatures, bytes[] calldatas, uint256 startBlock, uint256 endBlock, string description) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined | -| proposer | address | undefined | -| targets | address[] | undefined | -| values | uint256[] | undefined | -| signatures | string[] | undefined | -| calldatas | bytes[] | undefined | -| startBlock | uint256 | undefined | -| endBlock | uint256 | undefined | -| description | string | undefined | - -### ProposalExecuted - -```solidity -event ProposalExecuted(uint256 proposalId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined | - -### VoteCast - -```solidity -event VoteCast(address indexed voter, uint256 proposalId, uint8 support, uint256 weight, string reason) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| voter `indexed` | address | undefined | -| proposalId | uint256 | undefined | -| support | uint8 | undefined | -| weight | uint256 | undefined | -| reason | string | undefined | - - - diff --git a/docs/GovernorSettingsUpgradeable.md b/docs/GovernorSettingsUpgradeable.md deleted file mode 100644 index 4dbf2d3a0..000000000 --- a/docs/GovernorSettingsUpgradeable.md +++ /dev/null @@ -1,634 +0,0 @@ -# GovernorSettingsUpgradeable - - - - - - - -*Extension of {Governor} for settings updatable through governance. _Available since v4.4._* - -## Methods - -### BALLOT_TYPEHASH - -```solidity -function BALLOT_TYPEHASH() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### COUNTING_MODE - -```solidity -function COUNTING_MODE() external pure returns (string) -``` - -module:voting - -*A description of the possible `support` values for {castVote} and the way these votes are counted, meant to be consumed by UIs to show correct vote options and interpret the results. The string is a URL-encoded sequence of key-value pairs that each describe one aspect, for example `support=bravo&quorum=for,abstain`. There are 2 standard keys: `support` and `quorum`. - `support=bravo` refers to the vote options 0 = Against, 1 = For, 2 = Abstain, as in `GovernorBravo`. - `quorum=bravo` means that only For votes are counted towards quorum. - `quorum=for,abstain` means that both For and Abstain votes are counted towards quorum. NOTE: The string can be decoded by the standard https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams[`URLSearchParams`] JavaScript class.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### castVote - -```solidity -function castVote(uint256 proposalId, uint8 support) external nonpayable returns (uint256) -``` - - - -*See {IGovernor-castVote}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| support | uint8 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### castVoteBySig - -```solidity -function castVoteBySig(uint256 proposalId, uint8 support, uint8 v, bytes32 r, bytes32 s) external nonpayable returns (uint256) -``` - - - -*See {IGovernor-castVoteBySig}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| support | uint8 | undefined -| v | uint8 | undefined -| r | bytes32 | undefined -| s | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### castVoteWithReason - -```solidity -function castVoteWithReason(uint256 proposalId, uint8 support, string reason) external nonpayable returns (uint256) -``` - - - -*See {IGovernor-castVoteWithReason}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| support | uint8 | undefined -| reason | string | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### execute - -```solidity -function execute(address[] targets, uint256[] values, bytes[] calldatas, bytes32 descriptionHash) external payable returns (uint256) -``` - - - -*See {IGovernor-execute}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| targets | address[] | undefined -| values | uint256[] | undefined -| calldatas | bytes[] | undefined -| descriptionHash | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getVotes - -```solidity -function getVotes(address account, uint256 blockNumber) external view returns (uint256) -``` - -module:reputation - -*Voting power of an `account` at a specific `blockNumber`. Note: this can be implemented in a number of ways, for example by reading the delegated balance from one (or multiple), {ERC20Votes} tokens.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| blockNumber | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### hasVoted - -```solidity -function hasVoted(uint256 proposalId, address account) external view returns (bool) -``` - -module:voting - -*Returns weither `account` has cast a vote on `proposalId`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### hashProposal - -```solidity -function hashProposal(address[] targets, uint256[] values, bytes[] calldatas, bytes32 descriptionHash) external pure returns (uint256) -``` - - - -*See {IGovernor-hashProposal}. The proposal id is produced by hashing the RLC encoded `targets` array, the `values` array, the `calldatas` array and the descriptionHash (bytes32 which itself is the keccak256 hash of the description string). This proposal id can be produced from the proposal data which is part of the {ProposalCreated} event. It can even be computed in advance, before the proposal is submitted. Note that the chainId and the governor address are not part of the proposal id computation. Consequently, the same proposal (with same operation and same description) will have the same id if submitted on multiple governors accross multiple networks. This also means that in order to execute the same operation twice (on the same governor) the proposer will have to change the description in order to avoid proposal id conflicts.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| targets | address[] | undefined -| values | uint256[] | undefined -| calldatas | bytes[] | undefined -| descriptionHash | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*See {IGovernor-name}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### proposalDeadline - -```solidity -function proposalDeadline(uint256 proposalId) external view returns (uint256) -``` - - - -*See {IGovernor-proposalDeadline}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### proposalSnapshot - -```solidity -function proposalSnapshot(uint256 proposalId) external view returns (uint256) -``` - - - -*See {IGovernor-proposalSnapshot}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### proposalThreshold - -```solidity -function proposalThreshold() external view returns (uint256) -``` - - - -*See {Governor-proposalThreshold}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### propose - -```solidity -function propose(address[] targets, uint256[] values, bytes[] calldatas, string description) external nonpayable returns (uint256) -``` - - - -*See {IGovernor-propose}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| targets | address[] | undefined -| values | uint256[] | undefined -| calldatas | bytes[] | undefined -| description | string | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### quorum - -```solidity -function quorum(uint256 blockNumber) external view returns (uint256) -``` - -module:user-config - -*Minimum number of cast voted required for a proposal to be successful. Note: The `blockNumber` parameter corresponds to the snaphot used for counting vote. This allows to scale the quroum depending on values such as the totalSupply of a token at this block (see {ERC20Votes}).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| blockNumber | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### relay - -```solidity -function relay(address target, uint256 value, bytes data) external nonpayable -``` - - - -*Relays a transaction or function call to an arbitrary target. In cases where the governance executor is some contract other than the governor itself, like when using a timelock, this function can be invoked in a governance proposal to recover tokens or Ether that was sent to the governor contract by mistake. Note that if the executor is simply the governor itself, use of `relay` is redundant.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| target | address | undefined -| value | uint256 | undefined -| data | bytes | undefined - -### setProposalThreshold - -```solidity -function setProposalThreshold(uint256 newProposalThreshold) external nonpayable -``` - - - -*Update the proposal threshold. This operation can only be performed through a governance proposal. Emits a {ProposalThresholdSet} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| newProposalThreshold | uint256 | undefined - -### setVotingDelay - -```solidity -function setVotingDelay(uint256 newVotingDelay) external nonpayable -``` - - - -*Update the voting delay. This operation can only be performed through a governance proposal. Emits a {VotingDelaySet} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| newVotingDelay | uint256 | undefined - -### setVotingPeriod - -```solidity -function setVotingPeriod(uint256 newVotingPeriod) external nonpayable -``` - - - -*Update the voting period. This operation can only be performed through a governance proposal. Emits a {VotingPeriodSet} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| newVotingPeriod | uint256 | undefined - -### state - -```solidity -function state(uint256 proposalId) external view returns (enum IGovernorUpgradeable.ProposalState) -``` - - - -*See {IGovernor-state}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | enum IGovernorUpgradeable.ProposalState | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### version - -```solidity -function version() external view returns (string) -``` - - - -*See {IGovernor-version}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### votingDelay - -```solidity -function votingDelay() external view returns (uint256) -``` - - - -*See {IGovernor-votingDelay}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### votingPeriod - -```solidity -function votingPeriod() external view returns (uint256) -``` - - - -*See {IGovernor-votingPeriod}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - - - -## Events - -### ProposalCanceled - -```solidity -event ProposalCanceled(uint256 proposalId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined | - -### ProposalCreated - -```solidity -event ProposalCreated(uint256 proposalId, address proposer, address[] targets, uint256[] values, string[] signatures, bytes[] calldatas, uint256 startBlock, uint256 endBlock, string description) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined | -| proposer | address | undefined | -| targets | address[] | undefined | -| values | uint256[] | undefined | -| signatures | string[] | undefined | -| calldatas | bytes[] | undefined | -| startBlock | uint256 | undefined | -| endBlock | uint256 | undefined | -| description | string | undefined | - -### ProposalExecuted - -```solidity -event ProposalExecuted(uint256 proposalId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined | - -### ProposalThresholdSet - -```solidity -event ProposalThresholdSet(uint256 oldProposalThreshold, uint256 newProposalThreshold) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| oldProposalThreshold | uint256 | undefined | -| newProposalThreshold | uint256 | undefined | - -### VoteCast - -```solidity -event VoteCast(address indexed voter, uint256 proposalId, uint8 support, uint256 weight, string reason) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| voter `indexed` | address | undefined | -| proposalId | uint256 | undefined | -| support | uint8 | undefined | -| weight | uint256 | undefined | -| reason | string | undefined | - -### VotingDelaySet - -```solidity -event VotingDelaySet(uint256 oldVotingDelay, uint256 newVotingDelay) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| oldVotingDelay | uint256 | undefined | -| newVotingDelay | uint256 | undefined | - -### VotingPeriodSet - -```solidity -event VotingPeriodSet(uint256 oldVotingPeriod, uint256 newVotingPeriod) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| oldVotingPeriod | uint256 | undefined | -| newVotingPeriod | uint256 | undefined | - - - diff --git a/docs/GovernorUpgradeable.md b/docs/GovernorUpgradeable.md deleted file mode 100644 index 7b27d92a1..000000000 --- a/docs/GovernorUpgradeable.md +++ /dev/null @@ -1,535 +0,0 @@ -# GovernorUpgradeable - - - - - - - -*Core of the governance system, designed to be extended though various modules. This contract is abstract and requires several function to be implemented in various modules: - A counting module must implement {quorum}, {_quorumReached}, {_voteSucceeded} and {_countVote} - A voting module must implement {getVotes} - Additionanly, the {votingPeriod} must also be implemented _Available since v4.3._* - -## Methods - -### BALLOT_TYPEHASH - -```solidity -function BALLOT_TYPEHASH() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### COUNTING_MODE - -```solidity -function COUNTING_MODE() external pure returns (string) -``` - -module:voting - -*A description of the possible `support` values for {castVote} and the way these votes are counted, meant to be consumed by UIs to show correct vote options and interpret the results. The string is a URL-encoded sequence of key-value pairs that each describe one aspect, for example `support=bravo&quorum=for,abstain`. There are 2 standard keys: `support` and `quorum`. - `support=bravo` refers to the vote options 0 = Against, 1 = For, 2 = Abstain, as in `GovernorBravo`. - `quorum=bravo` means that only For votes are counted towards quorum. - `quorum=for,abstain` means that both For and Abstain votes are counted towards quorum. NOTE: The string can be decoded by the standard https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams[`URLSearchParams`] JavaScript class.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### castVote - -```solidity -function castVote(uint256 proposalId, uint8 support) external nonpayable returns (uint256) -``` - - - -*See {IGovernor-castVote}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| support | uint8 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### castVoteBySig - -```solidity -function castVoteBySig(uint256 proposalId, uint8 support, uint8 v, bytes32 r, bytes32 s) external nonpayable returns (uint256) -``` - - - -*See {IGovernor-castVoteBySig}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| support | uint8 | undefined -| v | uint8 | undefined -| r | bytes32 | undefined -| s | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### castVoteWithReason - -```solidity -function castVoteWithReason(uint256 proposalId, uint8 support, string reason) external nonpayable returns (uint256) -``` - - - -*See {IGovernor-castVoteWithReason}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| support | uint8 | undefined -| reason | string | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### execute - -```solidity -function execute(address[] targets, uint256[] values, bytes[] calldatas, bytes32 descriptionHash) external payable returns (uint256) -``` - - - -*See {IGovernor-execute}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| targets | address[] | undefined -| values | uint256[] | undefined -| calldatas | bytes[] | undefined -| descriptionHash | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getVotes - -```solidity -function getVotes(address account, uint256 blockNumber) external view returns (uint256) -``` - -module:reputation - -*Voting power of an `account` at a specific `blockNumber`. Note: this can be implemented in a number of ways, for example by reading the delegated balance from one (or multiple), {ERC20Votes} tokens.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| blockNumber | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### hasVoted - -```solidity -function hasVoted(uint256 proposalId, address account) external view returns (bool) -``` - -module:voting - -*Returns weither `account` has cast a vote on `proposalId`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### hashProposal - -```solidity -function hashProposal(address[] targets, uint256[] values, bytes[] calldatas, bytes32 descriptionHash) external pure returns (uint256) -``` - - - -*See {IGovernor-hashProposal}. The proposal id is produced by hashing the RLC encoded `targets` array, the `values` array, the `calldatas` array and the descriptionHash (bytes32 which itself is the keccak256 hash of the description string). This proposal id can be produced from the proposal data which is part of the {ProposalCreated} event. It can even be computed in advance, before the proposal is submitted. Note that the chainId and the governor address are not part of the proposal id computation. Consequently, the same proposal (with same operation and same description) will have the same id if submitted on multiple governors accross multiple networks. This also means that in order to execute the same operation twice (on the same governor) the proposer will have to change the description in order to avoid proposal id conflicts.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| targets | address[] | undefined -| values | uint256[] | undefined -| calldatas | bytes[] | undefined -| descriptionHash | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*See {IGovernor-name}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### proposalDeadline - -```solidity -function proposalDeadline(uint256 proposalId) external view returns (uint256) -``` - - - -*See {IGovernor-proposalDeadline}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### proposalSnapshot - -```solidity -function proposalSnapshot(uint256 proposalId) external view returns (uint256) -``` - - - -*See {IGovernor-proposalSnapshot}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### proposalThreshold - -```solidity -function proposalThreshold() external view returns (uint256) -``` - - - -*Part of the Governor Bravo's interface: _"The number of votes required in order for a voter to become a proposer"_.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### propose - -```solidity -function propose(address[] targets, uint256[] values, bytes[] calldatas, string description) external nonpayable returns (uint256) -``` - - - -*See {IGovernor-propose}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| targets | address[] | undefined -| values | uint256[] | undefined -| calldatas | bytes[] | undefined -| description | string | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### quorum - -```solidity -function quorum(uint256 blockNumber) external view returns (uint256) -``` - -module:user-config - -*Minimum number of cast voted required for a proposal to be successful. Note: The `blockNumber` parameter corresponds to the snaphot used for counting vote. This allows to scale the quroum depending on values such as the totalSupply of a token at this block (see {ERC20Votes}).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| blockNumber | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### relay - -```solidity -function relay(address target, uint256 value, bytes data) external nonpayable -``` - - - -*Relays a transaction or function call to an arbitrary target. In cases where the governance executor is some contract other than the governor itself, like when using a timelock, this function can be invoked in a governance proposal to recover tokens or Ether that was sent to the governor contract by mistake. Note that if the executor is simply the governor itself, use of `relay` is redundant.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| target | address | undefined -| value | uint256 | undefined -| data | bytes | undefined - -### state - -```solidity -function state(uint256 proposalId) external view returns (enum IGovernorUpgradeable.ProposalState) -``` - - - -*See {IGovernor-state}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | enum IGovernorUpgradeable.ProposalState | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### version - -```solidity -function version() external view returns (string) -``` - - - -*See {IGovernor-version}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### votingDelay - -```solidity -function votingDelay() external view returns (uint256) -``` - -module:user-config - -*Delay, in number of block, between the proposal is created and the vote starts. This can be increassed to leave time for users to buy voting power, of delegate it, before the voting of a proposal starts.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### votingPeriod - -```solidity -function votingPeriod() external view returns (uint256) -``` - -module:user-config - -*Delay, in number of blocks, between the vote start and vote ends. NOTE: The {votingDelay} can delay the start of the vote. This must be considered when setting the voting duration compared to the voting delay.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - - - -## Events - -### ProposalCanceled - -```solidity -event ProposalCanceled(uint256 proposalId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined | - -### ProposalCreated - -```solidity -event ProposalCreated(uint256 proposalId, address proposer, address[] targets, uint256[] values, string[] signatures, bytes[] calldatas, uint256 startBlock, uint256 endBlock, string description) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined | -| proposer | address | undefined | -| targets | address[] | undefined | -| values | uint256[] | undefined | -| signatures | string[] | undefined | -| calldatas | bytes[] | undefined | -| startBlock | uint256 | undefined | -| endBlock | uint256 | undefined | -| description | string | undefined | - -### ProposalExecuted - -```solidity -event ProposalExecuted(uint256 proposalId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined | - -### VoteCast - -```solidity -event VoteCast(address indexed voter, uint256 proposalId, uint8 support, uint256 weight, string reason) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| voter `indexed` | address | undefined | -| proposalId | uint256 | undefined | -| support | uint8 | undefined | -| weight | uint256 | undefined | -| reason | string | undefined | - - - diff --git a/docs/GovernorVotesQuorumFractionUpgradeable.md b/docs/GovernorVotesQuorumFractionUpgradeable.md deleted file mode 100644 index 84846624b..000000000 --- a/docs/GovernorVotesQuorumFractionUpgradeable.md +++ /dev/null @@ -1,619 +0,0 @@ -# GovernorVotesQuorumFractionUpgradeable - - - - - - - -*Extension of {Governor} for voting weight extraction from an {ERC20Votes} token and a quorum expressed as a fraction of the total supply. _Available since v4.3._* - -## Methods - -### BALLOT_TYPEHASH - -```solidity -function BALLOT_TYPEHASH() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### COUNTING_MODE - -```solidity -function COUNTING_MODE() external pure returns (string) -``` - -module:voting - -*A description of the possible `support` values for {castVote} and the way these votes are counted, meant to be consumed by UIs to show correct vote options and interpret the results. The string is a URL-encoded sequence of key-value pairs that each describe one aspect, for example `support=bravo&quorum=for,abstain`. There are 2 standard keys: `support` and `quorum`. - `support=bravo` refers to the vote options 0 = Against, 1 = For, 2 = Abstain, as in `GovernorBravo`. - `quorum=bravo` means that only For votes are counted towards quorum. - `quorum=for,abstain` means that both For and Abstain votes are counted towards quorum. NOTE: The string can be decoded by the standard https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams[`URLSearchParams`] JavaScript class.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### castVote - -```solidity -function castVote(uint256 proposalId, uint8 support) external nonpayable returns (uint256) -``` - - - -*See {IGovernor-castVote}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| support | uint8 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### castVoteBySig - -```solidity -function castVoteBySig(uint256 proposalId, uint8 support, uint8 v, bytes32 r, bytes32 s) external nonpayable returns (uint256) -``` - - - -*See {IGovernor-castVoteBySig}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| support | uint8 | undefined -| v | uint8 | undefined -| r | bytes32 | undefined -| s | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### castVoteWithReason - -```solidity -function castVoteWithReason(uint256 proposalId, uint8 support, string reason) external nonpayable returns (uint256) -``` - - - -*See {IGovernor-castVoteWithReason}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| support | uint8 | undefined -| reason | string | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### execute - -```solidity -function execute(address[] targets, uint256[] values, bytes[] calldatas, bytes32 descriptionHash) external payable returns (uint256) -``` - - - -*See {IGovernor-execute}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| targets | address[] | undefined -| values | uint256[] | undefined -| calldatas | bytes[] | undefined -| descriptionHash | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getVotes - -```solidity -function getVotes(address account, uint256 blockNumber) external view returns (uint256) -``` - -Read the voting weight from the token's built in snapshot mechanism (see {IGovernor-getVotes}). - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| blockNumber | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### hasVoted - -```solidity -function hasVoted(uint256 proposalId, address account) external view returns (bool) -``` - -module:voting - -*Returns weither `account` has cast a vote on `proposalId`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### hashProposal - -```solidity -function hashProposal(address[] targets, uint256[] values, bytes[] calldatas, bytes32 descriptionHash) external pure returns (uint256) -``` - - - -*See {IGovernor-hashProposal}. The proposal id is produced by hashing the RLC encoded `targets` array, the `values` array, the `calldatas` array and the descriptionHash (bytes32 which itself is the keccak256 hash of the description string). This proposal id can be produced from the proposal data which is part of the {ProposalCreated} event. It can even be computed in advance, before the proposal is submitted. Note that the chainId and the governor address are not part of the proposal id computation. Consequently, the same proposal (with same operation and same description) will have the same id if submitted on multiple governors accross multiple networks. This also means that in order to execute the same operation twice (on the same governor) the proposer will have to change the description in order to avoid proposal id conflicts.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| targets | address[] | undefined -| values | uint256[] | undefined -| calldatas | bytes[] | undefined -| descriptionHash | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*See {IGovernor-name}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### proposalDeadline - -```solidity -function proposalDeadline(uint256 proposalId) external view returns (uint256) -``` - - - -*See {IGovernor-proposalDeadline}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### proposalSnapshot - -```solidity -function proposalSnapshot(uint256 proposalId) external view returns (uint256) -``` - - - -*See {IGovernor-proposalSnapshot}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### proposalThreshold - -```solidity -function proposalThreshold() external view returns (uint256) -``` - - - -*Part of the Governor Bravo's interface: _"The number of votes required in order for a voter to become a proposer"_.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### propose - -```solidity -function propose(address[] targets, uint256[] values, bytes[] calldatas, string description) external nonpayable returns (uint256) -``` - - - -*See {IGovernor-propose}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| targets | address[] | undefined -| values | uint256[] | undefined -| calldatas | bytes[] | undefined -| description | string | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### quorum - -```solidity -function quorum(uint256 blockNumber) external view returns (uint256) -``` - - - -*Returns the quorum for a block number, in terms of number of votes: `supply * numerator / denominator`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| blockNumber | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### quorumDenominator - -```solidity -function quorumDenominator() external view returns (uint256) -``` - - - -*Returns the quorum denominator. Defaults to 100, but may be overridden.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### quorumNumerator - -```solidity -function quorumNumerator() external view returns (uint256) -``` - - - -*Returns the current quorum numerator. See {quorumDenominator}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### relay - -```solidity -function relay(address target, uint256 value, bytes data) external nonpayable -``` - - - -*Relays a transaction or function call to an arbitrary target. In cases where the governance executor is some contract other than the governor itself, like when using a timelock, this function can be invoked in a governance proposal to recover tokens or Ether that was sent to the governor contract by mistake. Note that if the executor is simply the governor itself, use of `relay` is redundant.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| target | address | undefined -| value | uint256 | undefined -| data | bytes | undefined - -### state - -```solidity -function state(uint256 proposalId) external view returns (enum IGovernorUpgradeable.ProposalState) -``` - - - -*See {IGovernor-state}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | enum IGovernorUpgradeable.ProposalState | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### token - -```solidity -function token() external view returns (contract IVotesUpgradeable) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | contract IVotesUpgradeable | undefined - -### updateQuorumNumerator - -```solidity -function updateQuorumNumerator(uint256 newQuorumNumerator) external nonpayable -``` - - - -*Changes the quorum numerator. Emits a {QuorumNumeratorUpdated} event. Requirements: - Must be called through a governance proposal. - New numerator must be smaller or equal to the denominator.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| newQuorumNumerator | uint256 | undefined - -### version - -```solidity -function version() external view returns (string) -``` - - - -*See {IGovernor-version}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### votingDelay - -```solidity -function votingDelay() external view returns (uint256) -``` - -module:user-config - -*Delay, in number of block, between the proposal is created and the vote starts. This can be increassed to leave time for users to buy voting power, of delegate it, before the voting of a proposal starts.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### votingPeriod - -```solidity -function votingPeriod() external view returns (uint256) -``` - -module:user-config - -*Delay, in number of blocks, between the vote start and vote ends. NOTE: The {votingDelay} can delay the start of the vote. This must be considered when setting the voting duration compared to the voting delay.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - - - -## Events - -### ProposalCanceled - -```solidity -event ProposalCanceled(uint256 proposalId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined | - -### ProposalCreated - -```solidity -event ProposalCreated(uint256 proposalId, address proposer, address[] targets, uint256[] values, string[] signatures, bytes[] calldatas, uint256 startBlock, uint256 endBlock, string description) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined | -| proposer | address | undefined | -| targets | address[] | undefined | -| values | uint256[] | undefined | -| signatures | string[] | undefined | -| calldatas | bytes[] | undefined | -| startBlock | uint256 | undefined | -| endBlock | uint256 | undefined | -| description | string | undefined | - -### ProposalExecuted - -```solidity -event ProposalExecuted(uint256 proposalId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined | - -### QuorumNumeratorUpdated - -```solidity -event QuorumNumeratorUpdated(uint256 oldQuorumNumerator, uint256 newQuorumNumerator) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| oldQuorumNumerator | uint256 | undefined | -| newQuorumNumerator | uint256 | undefined | - -### VoteCast - -```solidity -event VoteCast(address indexed voter, uint256 proposalId, uint8 support, uint256 weight, string reason) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| voter `indexed` | address | undefined | -| proposalId | uint256 | undefined | -| support | uint8 | undefined | -| weight | uint256 | undefined | -| reason | string | undefined | - - - diff --git a/docs/GovernorVotesUpgradeable.md b/docs/GovernorVotesUpgradeable.md deleted file mode 100644 index 5b7ac6e33..000000000 --- a/docs/GovernorVotesUpgradeable.md +++ /dev/null @@ -1,552 +0,0 @@ -# GovernorVotesUpgradeable - - - - - - - -*Extension of {Governor} for voting weight extraction from an {ERC20Votes} token, or since v4.5 an {ERC721Votes} token. _Available since v4.3._* - -## Methods - -### BALLOT_TYPEHASH - -```solidity -function BALLOT_TYPEHASH() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### COUNTING_MODE - -```solidity -function COUNTING_MODE() external pure returns (string) -``` - -module:voting - -*A description of the possible `support` values for {castVote} and the way these votes are counted, meant to be consumed by UIs to show correct vote options and interpret the results. The string is a URL-encoded sequence of key-value pairs that each describe one aspect, for example `support=bravo&quorum=for,abstain`. There are 2 standard keys: `support` and `quorum`. - `support=bravo` refers to the vote options 0 = Against, 1 = For, 2 = Abstain, as in `GovernorBravo`. - `quorum=bravo` means that only For votes are counted towards quorum. - `quorum=for,abstain` means that both For and Abstain votes are counted towards quorum. NOTE: The string can be decoded by the standard https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams[`URLSearchParams`] JavaScript class.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### castVote - -```solidity -function castVote(uint256 proposalId, uint8 support) external nonpayable returns (uint256) -``` - - - -*See {IGovernor-castVote}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| support | uint8 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### castVoteBySig - -```solidity -function castVoteBySig(uint256 proposalId, uint8 support, uint8 v, bytes32 r, bytes32 s) external nonpayable returns (uint256) -``` - - - -*See {IGovernor-castVoteBySig}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| support | uint8 | undefined -| v | uint8 | undefined -| r | bytes32 | undefined -| s | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### castVoteWithReason - -```solidity -function castVoteWithReason(uint256 proposalId, uint8 support, string reason) external nonpayable returns (uint256) -``` - - - -*See {IGovernor-castVoteWithReason}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| support | uint8 | undefined -| reason | string | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### execute - -```solidity -function execute(address[] targets, uint256[] values, bytes[] calldatas, bytes32 descriptionHash) external payable returns (uint256) -``` - - - -*See {IGovernor-execute}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| targets | address[] | undefined -| values | uint256[] | undefined -| calldatas | bytes[] | undefined -| descriptionHash | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getVotes - -```solidity -function getVotes(address account, uint256 blockNumber) external view returns (uint256) -``` - -Read the voting weight from the token's built in snapshot mechanism (see {IGovernor-getVotes}). - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| blockNumber | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### hasVoted - -```solidity -function hasVoted(uint256 proposalId, address account) external view returns (bool) -``` - -module:voting - -*Returns weither `account` has cast a vote on `proposalId`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### hashProposal - -```solidity -function hashProposal(address[] targets, uint256[] values, bytes[] calldatas, bytes32 descriptionHash) external pure returns (uint256) -``` - - - -*See {IGovernor-hashProposal}. The proposal id is produced by hashing the RLC encoded `targets` array, the `values` array, the `calldatas` array and the descriptionHash (bytes32 which itself is the keccak256 hash of the description string). This proposal id can be produced from the proposal data which is part of the {ProposalCreated} event. It can even be computed in advance, before the proposal is submitted. Note that the chainId and the governor address are not part of the proposal id computation. Consequently, the same proposal (with same operation and same description) will have the same id if submitted on multiple governors accross multiple networks. This also means that in order to execute the same operation twice (on the same governor) the proposer will have to change the description in order to avoid proposal id conflicts.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| targets | address[] | undefined -| values | uint256[] | undefined -| calldatas | bytes[] | undefined -| descriptionHash | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*See {IGovernor-name}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### proposalDeadline - -```solidity -function proposalDeadline(uint256 proposalId) external view returns (uint256) -``` - - - -*See {IGovernor-proposalDeadline}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### proposalSnapshot - -```solidity -function proposalSnapshot(uint256 proposalId) external view returns (uint256) -``` - - - -*See {IGovernor-proposalSnapshot}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### proposalThreshold - -```solidity -function proposalThreshold() external view returns (uint256) -``` - - - -*Part of the Governor Bravo's interface: _"The number of votes required in order for a voter to become a proposer"_.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### propose - -```solidity -function propose(address[] targets, uint256[] values, bytes[] calldatas, string description) external nonpayable returns (uint256) -``` - - - -*See {IGovernor-propose}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| targets | address[] | undefined -| values | uint256[] | undefined -| calldatas | bytes[] | undefined -| description | string | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### quorum - -```solidity -function quorum(uint256 blockNumber) external view returns (uint256) -``` - -module:user-config - -*Minimum number of cast voted required for a proposal to be successful. Note: The `blockNumber` parameter corresponds to the snaphot used for counting vote. This allows to scale the quroum depending on values such as the totalSupply of a token at this block (see {ERC20Votes}).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| blockNumber | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### relay - -```solidity -function relay(address target, uint256 value, bytes data) external nonpayable -``` - - - -*Relays a transaction or function call to an arbitrary target. In cases where the governance executor is some contract other than the governor itself, like when using a timelock, this function can be invoked in a governance proposal to recover tokens or Ether that was sent to the governor contract by mistake. Note that if the executor is simply the governor itself, use of `relay` is redundant.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| target | address | undefined -| value | uint256 | undefined -| data | bytes | undefined - -### state - -```solidity -function state(uint256 proposalId) external view returns (enum IGovernorUpgradeable.ProposalState) -``` - - - -*See {IGovernor-state}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | enum IGovernorUpgradeable.ProposalState | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### token - -```solidity -function token() external view returns (contract IVotesUpgradeable) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | contract IVotesUpgradeable | undefined - -### version - -```solidity -function version() external view returns (string) -``` - - - -*See {IGovernor-version}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### votingDelay - -```solidity -function votingDelay() external view returns (uint256) -``` - -module:user-config - -*Delay, in number of block, between the proposal is created and the vote starts. This can be increassed to leave time for users to buy voting power, of delegate it, before the voting of a proposal starts.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### votingPeriod - -```solidity -function votingPeriod() external view returns (uint256) -``` - -module:user-config - -*Delay, in number of blocks, between the vote start and vote ends. NOTE: The {votingDelay} can delay the start of the vote. This must be considered when setting the voting duration compared to the voting delay.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - - - -## Events - -### ProposalCanceled - -```solidity -event ProposalCanceled(uint256 proposalId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined | - -### ProposalCreated - -```solidity -event ProposalCreated(uint256 proposalId, address proposer, address[] targets, uint256[] values, string[] signatures, bytes[] calldatas, uint256 startBlock, uint256 endBlock, string description) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined | -| proposer | address | undefined | -| targets | address[] | undefined | -| values | uint256[] | undefined | -| signatures | string[] | undefined | -| calldatas | bytes[] | undefined | -| startBlock | uint256 | undefined | -| endBlock | uint256 | undefined | -| description | string | undefined | - -### ProposalExecuted - -```solidity -event ProposalExecuted(uint256 proposalId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined | - -### VoteCast - -```solidity -event VoteCast(address indexed voter, uint256 proposalId, uint8 support, uint256 weight, string reason) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| voter `indexed` | address | undefined | -| proposalId | uint256 | undefined | -| support | uint8 | undefined | -| weight | uint256 | undefined | -| reason | string | undefined | - - - diff --git a/docs/IAccessControl.md b/docs/IAccessControl.md deleted file mode 100644 index 323726480..000000000 --- a/docs/IAccessControl.md +++ /dev/null @@ -1,168 +0,0 @@ -# IAccessControl - - - - - - - -*External interface of AccessControl declared to support ERC165 detection.* - -## Methods - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {AccessControl-_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - - - -## Events - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - -*Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole` `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite {RoleAdminChanged} not being emitted signaling this. _Available since v3.1._* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - -*Emitted when `account` is granted `role`. `sender` is the account that originated the contract call, an admin role bearer except when using {AccessControl-_setupRole}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - -*Emitted when `account` is revoked `role`. `sender` is the account that originated the contract call: - if using `revokeRole`, it is the admin role bearer - if using `renounceRole`, it is the role bearer (i.e. `account`)* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - - - diff --git a/docs/IAccessControlEnumerable.md b/docs/IAccessControlEnumerable.md deleted file mode 100644 index 8030a017e..000000000 --- a/docs/IAccessControlEnumerable.md +++ /dev/null @@ -1,213 +0,0 @@ -# IAccessControlEnumerable - - - - - - - -*External interface of AccessControlEnumerable declared to support ERC165 detection.* - -## Methods - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {AccessControl-_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleMember - -```solidity -function getRoleMember(bytes32 role, uint256 index) external view returns (address) -``` - - - -*Returns one of the accounts that have `role`. `index` must be a value between 0 and {getRoleMemberCount}, non-inclusive. Role bearers are not sorted in any particular way, and their ordering may change at any point. WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure you perform all queries on the same block. See the following https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] for more information.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getRoleMemberCount - -```solidity -function getRoleMemberCount(bytes32 role) external view returns (uint256) -``` - - - -*Returns the number of accounts that have `role`. Can be used together with {getRoleMember} to enumerate all bearers of a role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - - - -## Events - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - - - diff --git a/docs/IAccessControlEnumerableUpgradeable.md b/docs/IAccessControlEnumerableUpgradeable.md deleted file mode 100644 index 3ae10cc63..000000000 --- a/docs/IAccessControlEnumerableUpgradeable.md +++ /dev/null @@ -1,213 +0,0 @@ -# IAccessControlEnumerableUpgradeable - - - - - - - -*External interface of AccessControlEnumerable declared to support ERC165 detection.* - -## Methods - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {AccessControl-_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleMember - -```solidity -function getRoleMember(bytes32 role, uint256 index) external view returns (address) -``` - - - -*Returns one of the accounts that have `role`. `index` must be a value between 0 and {getRoleMemberCount}, non-inclusive. Role bearers are not sorted in any particular way, and their ordering may change at any point. WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure you perform all queries on the same block. See the following https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] for more information.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getRoleMemberCount - -```solidity -function getRoleMemberCount(bytes32 role) external view returns (uint256) -``` - - - -*Returns the number of accounts that have `role`. Can be used together with {getRoleMember} to enumerate all bearers of a role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - - - -## Events - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - - - diff --git a/docs/IAccessControlUpgradeable.md b/docs/IAccessControlUpgradeable.md deleted file mode 100644 index 1ca85f45d..000000000 --- a/docs/IAccessControlUpgradeable.md +++ /dev/null @@ -1,168 +0,0 @@ -# IAccessControlUpgradeable - - - - - - - -*External interface of AccessControl declared to support ERC165 detection.* - -## Methods - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {AccessControl-_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - - - -## Events - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - -*Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole` `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite {RoleAdminChanged} not being emitted signaling this. _Available since v3.1._* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - -*Emitted when `account` is granted `role`. `sender` is the account that originated the contract call, an admin role bearer except when using {AccessControl-_setupRole}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - -*Emitted when `account` is revoked `role`. `sender` is the account that originated the contract call: - if using `revokeRole`, it is the admin role bearer - if using `renounceRole`, it is the role bearer (i.e. `account`)* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - - - diff --git a/docs/IBurnableERC1155.md b/docs/IBurnableERC1155.md deleted file mode 100644 index 8a5e5ffc6..000000000 --- a/docs/IBurnableERC1155.md +++ /dev/null @@ -1,51 +0,0 @@ -# IBurnableERC1155 - - - - - -`SignatureMint1155` is an ERC 1155 contract. It lets anyone mint NFTs by producing a mint request and a signature (produced by an account with MINTER_ROLE, signing the mint request). - - - -## Methods - -### burn - -```solidity -function burn(address account, uint256 id, uint256 value) external nonpayable -``` - - - -*Lets a token owner burn the tokens they own (i.e. destroy for good)* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| id | uint256 | undefined -| value | uint256 | undefined - -### burnBatch - -```solidity -function burnBatch(address account, uint256[] ids, uint256[] values) external nonpayable -``` - - - -*Lets a token owner burn multiple tokens they own at once (i.e. destroy for good)* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| ids | uint256[] | undefined -| values | uint256[] | undefined - - - - diff --git a/docs/IBurnableERC20.md b/docs/IBurnableERC20.md deleted file mode 100644 index bf87194c4..000000000 --- a/docs/IBurnableERC20.md +++ /dev/null @@ -1,48 +0,0 @@ -# IBurnableERC20 - - - - - - - - - -## Methods - -### burn - -```solidity -function burn(uint256 amount) external nonpayable -``` - - - -*Destroys `amount` tokens from the caller. See {ERC20-_burn}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| amount | uint256 | undefined - -### burnFrom - -```solidity -function burnFrom(address account, uint256 amount) external nonpayable -``` - - - -*Destroys `amount` tokens from `account`, deducting from the caller's allowance. See {ERC20-_burn} and {ERC20-allowance}. Requirements: - the caller must have allowance for ``accounts``'s tokens of at least `amount`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| amount | uint256 | undefined - - - - diff --git a/docs/IBurnableERC721.md b/docs/IBurnableERC721.md deleted file mode 100644 index c7224c29b..000000000 --- a/docs/IBurnableERC721.md +++ /dev/null @@ -1,31 +0,0 @@ -# IBurnableERC721 - - - - - - - - - -## Methods - -### burn - -```solidity -function burn(uint256 tokenId) external nonpayable -``` - - - -*Burns `tokenId`. See {ERC721-_burn}. Requirements: - The caller must own `tokenId` or be an approved operator.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - - - - diff --git a/docs/IClaimCondition.md b/docs/IClaimCondition.md deleted file mode 100644 index 9806fbe80..000000000 --- a/docs/IClaimCondition.md +++ /dev/null @@ -1,12 +0,0 @@ -# IClaimCondition - - - - - -Thirdweb's 'Drop' contracts are distribution mechanisms for tokens. A contract admin (i.e. a holder of `DEFAULT_ADMIN_ROLE`) can set a series of claim conditions, ordered by their respective `startTimestamp`. A claim condition defines criteria under which accounts can mint tokens. Claim conditions can be overwritten or added to by the contract admin. At any moment, there is only one active claim condition. - - - - - diff --git a/docs/IClaimConditionMultiPhase.md b/docs/IClaimConditionMultiPhase.md deleted file mode 100644 index 418bcfe29..000000000 --- a/docs/IClaimConditionMultiPhase.md +++ /dev/null @@ -1,12 +0,0 @@ -# IClaimConditionMultiPhase - - - - - -Thirdweb's 'Drop' contracts are distribution mechanisms for tokens. A contract admin (i.e. a holder of `DEFAULT_ADMIN_ROLE`) can set a series of claim conditions, ordered by their respective `startTimestamp`. A claim condition defines criteria under which accounts can mint tokens. Claim conditions can be overwritten or added to by the contract admin. At any moment, there is only one active claim condition. - - - - - diff --git a/docs/IClaimConditionsSinglePhase.md b/docs/IClaimConditionsSinglePhase.md deleted file mode 100644 index 0c4b8dbd6..000000000 --- a/docs/IClaimConditionsSinglePhase.md +++ /dev/null @@ -1,52 +0,0 @@ -# IClaimConditionsSinglePhase - - - - - -Thirdweb's 'Drop' contracts are distribution mechanisms for tokens. A contract admin (i.e. a holder of `DEFAULT_ADMIN_ROLE`) can set a series of claim conditions, ordered by their respective `startTimestamp`. A claim condition defines criteria under which accounts can mint tokens. Claim conditions can be overwritten or added to by the contract admin. At any moment, there is only one active claim condition. - - - -## Methods - -### setClaimConditions - -```solidity -function setClaimConditions(IClaimCondition.ClaimCondition phase, bool resetClaimEligibility) external nonpayable -``` - -Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| phase | IClaimCondition.ClaimCondition | Claim conditions in ascending order by `startTimestamp`. -| resetClaimEligibility | bool | Whether to reset `limitLastClaimTimestamp` and `limitMerkleProofClaim` values when setting new claim conditions. - - - -## Events - -### ClaimConditionUpdated - -```solidity -event ClaimConditionUpdated(IClaimCondition.ClaimCondition claimConditions, bool resetClaimEligibility) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| claimConditions | IClaimCondition.ClaimCondition | undefined | -| resetClaimEligibility | bool | undefined | - - - diff --git a/docs/IContractDeployer.md b/docs/IContractDeployer.md deleted file mode 100644 index 35ac7ff53..000000000 --- a/docs/IContractDeployer.md +++ /dev/null @@ -1,128 +0,0 @@ -# IContractDeployer - - - - - - - - - -## Methods - -### deployInstance - -```solidity -function deployInstance(address publisher, bytes contractBytecode, bytes constructorArgs, bytes32 salt, uint256 value, string publishMetadataUri) external nonpayable returns (address deployedAddress) -``` - -Deploys an instance of a published contract directly. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| publisher | address | The address of the publisher. -| contractBytecode | bytes | The bytecode of the contract to deploy. -| constructorArgs | bytes | The encoded constructor args to deploy the contract with. -| salt | bytes32 | The salt to use in the CREATE2 contract deployment. -| value | uint256 | The native token value to pass to the contract on deployment. -| publishMetadataUri | string | The publish metadata URI for the contract to deploy. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| deployedAddress | address | The address of the contract deployed. - -### deployInstanceProxy - -```solidity -function deployInstanceProxy(address publisher, address implementation, bytes initializeData, bytes32 salt, uint256 value, string publishMetadataUri) external nonpayable returns (address deployedAddress) -``` - -Deploys a clone pointing to an implementation of a published contract. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| publisher | address | The address of the publisher. -| implementation | address | The contract implementation for the clone to point to. -| initializeData | bytes | The encoded function call to initialize the contract with. -| salt | bytes32 | The salt to use in the CREATE2 contract deployment. -| value | uint256 | The native token value to pass to the contract on deployment. -| publishMetadataUri | string | The publish metadata URI and for the contract to deploy. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| deployedAddress | address | The address of the contract deployed. - -### getContractDeployer - -```solidity -function getContractDeployer(address _contract) external view returns (address) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _contract | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - - - -## Events - -### ContractDeployed - -```solidity -event ContractDeployed(address indexed deployer, address indexed publisher, address deployedContract) -``` - - - -*Emitted when a contract is deployed.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| deployer `indexed` | address | undefined | -| publisher `indexed` | address | undefined | -| deployedContract | address | undefined | - -### Paused - -```solidity -event Paused(bool isPaused) -``` - - - -*Emitted when the registry is paused.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| isPaused | bool | undefined | - - - diff --git a/docs/IContractMetadata.md b/docs/IContractMetadata.md deleted file mode 100644 index 1165fc5a7..000000000 --- a/docs/IContractMetadata.md +++ /dev/null @@ -1,68 +0,0 @@ -# IContractMetadata - - - - - -Thirdweb's `ContractMetadata` is a contract extension for any base contracts. It lets you set a metadata URI for you contract. Additionally, `ContractMetadata` is necessary for NFT contracts that want royalties to get distributed on OpenSea. - - - -## Methods - -### contractURI - -```solidity -function contractURI() external view returns (string) -``` - - - -*Returns the metadata URI of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### setContractURI - -```solidity -function setContractURI(string _uri) external nonpayable -``` - - - -*Sets contract URI for the storefront-level metadata of the contract. Only module admin can call this function.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _uri | string | undefined - - - -## Events - -### ContractURIUpdated - -```solidity -event ContractURIUpdated(string prevURI, string newURI) -``` - - - -*Emitted when the contract URI is updated.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| prevURI | string | undefined | -| newURI | string | undefined | - - - diff --git a/docs/IContractMetadataRegistry.md b/docs/IContractMetadataRegistry.md deleted file mode 100644 index 524c0cf56..000000000 --- a/docs/IContractMetadataRegistry.md +++ /dev/null @@ -1,52 +0,0 @@ -# IContractMetadataRegistry - - - - - - - - - -## Methods - -### registerMetadata - -```solidity -function registerMetadata(address contractAddress, string metadataUri) external nonpayable -``` - - - -*Records `metadataUri` as metadata for the contract at `contractAddress`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| contractAddress | address | undefined -| metadataUri | string | undefined - - - -## Events - -### MetadataRegistered - -```solidity -event MetadataRegistered(address indexed contractAddress, string metadataUri) -``` - - - -*Emitted when a contract metadata is registered* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| contractAddress `indexed` | address | undefined | -| metadataUri | string | undefined | - - - diff --git a/docs/IContractPublisher.md b/docs/IContractPublisher.md deleted file mode 100644 index 8c174b009..000000000 --- a/docs/IContractPublisher.md +++ /dev/null @@ -1,237 +0,0 @@ -# IContractPublisher - - - - - - - - - -## Methods - -### getAllPublishedContracts - -```solidity -function getAllPublishedContracts(address publisher) external view returns (struct IContractPublisher.CustomContractInstance[] published) -``` - -Returns the latest version of all contracts published by a publisher. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| publisher | address | The address of the publisher. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| published | IContractPublisher.CustomContractInstance[] | An array of all contracts published by the publisher. - -### getPublishedContract - -```solidity -function getPublishedContract(address publisher, string contractId) external view returns (struct IContractPublisher.CustomContractInstance published) -``` - -Returns the latest version of a contract published by a publisher. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| publisher | address | The address of the publisher. -| contractId | string | The identifier for a published contract (that can have multiple verisons). - -#### Returns - -| Name | Type | Description | -|---|---|---| -| published | IContractPublisher.CustomContractInstance | The desired contract published by the publisher. - -### getPublishedContractVersions - -```solidity -function getPublishedContractVersions(address publisher, string contractId) external view returns (struct IContractPublisher.CustomContractInstance[] published) -``` - -Returns all versions of a published contract. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| publisher | address | The address of the publisher. -| contractId | string | The identifier for a published contract (that can have multiple verisons). - -#### Returns - -| Name | Type | Description | -|---|---|---| -| published | IContractPublisher.CustomContractInstance[] | The desired contracts published by the publisher. - -### getPublishedUriFromCompilerUri - -```solidity -function getPublishedUriFromCompilerUri(string compilerMetadataUri) external view returns (string[] publishedMetadataUris) -``` - -Retrieve the published metadata URI from a compiler metadata URI - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| compilerMetadataUri | string | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| publishedMetadataUris | string[] | undefined - -### getPublisherProfileUri - -```solidity -function getPublisherProfileUri(address publisher) external view returns (string uri) -``` - -get the publisher profile uri - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| publisher | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| uri | string | undefined - -### publishContract - -```solidity -function publishContract(address publisher, string contractId, string publishMetadataUri, string compilerMetadataUri, bytes32 bytecodeHash, address implementation) external nonpayable -``` - -Let's an account publish a contract. The account must be approved by the publisher, or be the publisher. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| publisher | address | The address of the publisher. -| contractId | string | The identifier for a published contract (that can have multiple verisons). -| publishMetadataUri | string | The IPFS URI of the publish metadata. -| compilerMetadataUri | string | The IPFS URI of the compiler metadata. -| bytecodeHash | bytes32 | The keccak256 hash of the contract bytecode. -| implementation | address | (Optional) An implementation address that proxy contracts / clones can point to. Default value if such an implementation does not exist - address(0); - -### setPublisherProfileUri - -```solidity -function setPublisherProfileUri(address publisher, string uri) external nonpayable -``` - -Lets an account set its publisher profile uri - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| publisher | address | undefined -| uri | string | undefined - -### unpublishContract - -```solidity -function unpublishContract(address publisher, string contractId) external nonpayable -``` - -Lets an account unpublish a contract and all its versions. The account must be approved by the publisher, or be the publisher. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| publisher | address | The address of the publisher. -| contractId | string | The identifier for a published contract (that can have multiple verisons). - - - -## Events - -### ContractPublished - -```solidity -event ContractPublished(address indexed operator, address indexed publisher, IContractPublisher.CustomContractInstance publishedContract) -``` - - - -*Emitted when a contract is published.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| publisher `indexed` | address | undefined | -| publishedContract | IContractPublisher.CustomContractInstance | undefined | - -### ContractUnpublished - -```solidity -event ContractUnpublished(address indexed operator, address indexed publisher, string indexed contractId) -``` - - - -*Emitted when a contract is unpublished.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| publisher `indexed` | address | undefined | -| contractId `indexed` | string | undefined | - -### Paused - -```solidity -event Paused(bool isPaused) -``` - - - -*Emitted when the registry is paused.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| isPaused | bool | undefined | - - - diff --git a/docs/IDelayedReveal.md b/docs/IDelayedReveal.md deleted file mode 100644 index be7dbb4e4..000000000 --- a/docs/IDelayedReveal.md +++ /dev/null @@ -1,81 +0,0 @@ -# IDelayedReveal - - - - - -Thirdweb's `DelayedReveal` is a contract extension for base NFT contracts. It lets you create batches of 'delayed-reveal' NFTs. You can learn more about the usage of delayed reveal NFTs here - https://blog.thirdweb.com/delayed-reveal-nfts - - - -## Methods - -### encryptDecrypt - -```solidity -function encryptDecrypt(bytes data, bytes key) external pure returns (bytes result) -``` - -Performs XOR encryption/decryption. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes | The data to encrypt. In the case of delayed-reveal NFTs, this is the "revealed" state base URI of the relevant batch of NFTs. -| key | bytes | The key with which to encrypt data - -#### Returns - -| Name | Type | Description | -|---|---|---| -| result | bytes | undefined - -### reveal - -```solidity -function reveal(uint256 identifier, bytes key) external nonpayable returns (string revealedURI) -``` - -Reveals a batch of delayed reveal NFTs. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| identifier | uint256 | The ID for the batch of delayed-reveal NFTs to reveal. -| key | bytes | The key with which the base URI for the relevant batch of NFTs was encrypted. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| revealedURI | string | undefined - - - -## Events - -### TokenURIRevealed - -```solidity -event TokenURIRevealed(uint256 indexed index, string revealedURI) -``` - - - -*Emitted when tokens are revealed.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| index `indexed` | uint256 | undefined | -| revealedURI | string | undefined | - - - diff --git a/docs/IDrop.md b/docs/IDrop.md deleted file mode 100644 index 3cdd031b4..000000000 --- a/docs/IDrop.md +++ /dev/null @@ -1,227 +0,0 @@ -# IDrop - - - - - - - - - -## Methods - -### claim - -```solidity -function claim(address receiver, uint256 quantity, address currency, uint256 pricePerToken, IDrop.AllowlistProof allowlistProof, bytes data) external payable -``` - -Lets an account claim a given quantity of NFTs. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| receiver | address | The receiver of the NFTs to claim. -| quantity | uint256 | The quantity of NFTs to claim. -| currency | address | The currency in which to pay for the claim. -| pricePerToken | uint256 | The price per token to pay for the claim. -| allowlistProof | IDrop.AllowlistProof | The proof of the claimer's inclusion in the merkle root allowlist of the claim conditions that apply. -| data | bytes | Arbitrary bytes data that can be leveraged in the implementation of this interface. - -### setClaimConditions - -```solidity -function setClaimConditions(IClaimCondition.ClaimCondition[] phases, bool resetClaimEligibility) external nonpayable -``` - -Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| phases | IClaimCondition.ClaimCondition[] | Claim conditions in ascending order by `startTimestamp`. -| resetClaimEligibility | bool | Whether to reset `limitLastClaimTimestamp` and `limitMerkleProofClaim` values when setting new claim conditions. - - - -## Events - -### ClaimConditionsUpdated - -```solidity -event ClaimConditionsUpdated(IClaimCondition.ClaimCondition[] claimConditions, bool resetEligibility) -``` - - - -*Emitted when the contract's claim conditions are updated.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| claimConditions | IClaimCondition.ClaimCondition[] | undefined | -| resetEligibility | bool | undefined | - -### TokensClaimed - -```solidity -event TokensClaimed(uint256 indexed claimConditionIndex, address indexed claimer, address indexed receiver, uint256 startTokenId, uint256 quantityClaimed) -``` - - - -*Emitted when tokens are claimed via `claim`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| claimConditionIndex `indexed` | uint256 | undefined | -| claimer `indexed` | address | undefined | -| receiver `indexed` | address | undefined | -| startTokenId | uint256 | undefined | -| quantityClaimed | uint256 | undefined | - - - -## Errors - -### Drop__CannotClaimYet - -```solidity -error Drop__CannotClaimYet(uint256 blockTimestamp, uint256 startTimestamp, uint256 lastClaimedAt, uint256 nextValidClaimTimestamp) -``` - -Emitted when the current timestamp is invalid for claim. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| blockTimestamp | uint256 | undefined | -| startTimestamp | uint256 | undefined | -| lastClaimedAt | uint256 | undefined | -| nextValidClaimTimestamp | uint256 | undefined | - -### Drop__ExceedMaxClaimableSupply - -```solidity -error Drop__ExceedMaxClaimableSupply(uint256 supplyClaimed, uint256 maxClaimableSupply) -``` - -Emitted when claiming given quantity will exceed max claimable supply. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| supplyClaimed | uint256 | undefined | -| maxClaimableSupply | uint256 | undefined | - -### Drop__InvalidCurrencyOrPrice - -```solidity -error Drop__InvalidCurrencyOrPrice(address givenCurrency, address requiredCurrency, uint256 givenPricePerToken, uint256 requiredPricePerToken) -``` - -Emitted when given currency or price is invalid. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| givenCurrency | address | undefined | -| requiredCurrency | address | undefined | -| givenPricePerToken | uint256 | undefined | -| requiredPricePerToken | uint256 | undefined | - -### Drop__InvalidQuantity - -```solidity -error Drop__InvalidQuantity() -``` - -Emitted when claiming invalid quantity of tokens. - - - - -### Drop__InvalidQuantityProof - -```solidity -error Drop__InvalidQuantityProof(uint256 maxQuantityInAllowlist) -``` - -Emitted when claiming more than allowed quantity in allowlist. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| maxQuantityInAllowlist | uint256 | undefined | - -### Drop__MaxSupplyClaimedAlready - -```solidity -error Drop__MaxSupplyClaimedAlready(uint256 supplyClaimedAlready) -``` - -Emitted when max claimable supply in given condition is less than supply claimed already. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| supplyClaimedAlready | uint256 | undefined | - -### Drop__NotAuthorized - -```solidity -error Drop__NotAuthorized() -``` - - - -*Emitted when an unauthorized caller tries to set claim conditions.* - - -### Drop__NotInWhitelist - -```solidity -error Drop__NotInWhitelist() -``` - -Emitted when given allowlist proof is invalid. - - - - -### Drop__ProofClaimed - -```solidity -error Drop__ProofClaimed() -``` - -Emitted when allowlist spot is already used. - - - - - diff --git a/docs/IDropClaimCondition.md b/docs/IDropClaimCondition.md deleted file mode 100644 index c746e6521..000000000 --- a/docs/IDropClaimCondition.md +++ /dev/null @@ -1,12 +0,0 @@ -# IDropClaimCondition - - - - - -Thirdweb's 'Drop' contracts are distribution mechanisms for tokens. A contract admin (i.e. a holder of `DEFAULT_ADMIN_ROLE`) can set a series of claim conditions, ordered by their respective `startTimestamp`. A claim condition defines criteria under which accounts can mint tokens. Claim conditions can be overwritten or added to by the contract admin. At any moment, there is only one active claim condition. - - - - - diff --git a/docs/IDropERC1155.md b/docs/IDropERC1155.md deleted file mode 100644 index 0d19f09f4..000000000 --- a/docs/IDropERC1155.md +++ /dev/null @@ -1,422 +0,0 @@ -# IDropERC1155 - - - - - -Thirdweb's 'Drop' contracts are distribution mechanisms for tokens. The `DropERC721` contract is a distribution mechanism for ERC721 tokens. A minter wallet (i.e. holder of `MINTER_ROLE`) can (lazy)mint 'n' tokens at once by providing a single base URI for all tokens being lazy minted. The URI for each of the 'n' tokens lazy minted is the provided base URI + `{tokenId}` of the respective token. (e.g. "ipsf://Qmece.../1"). A minter can choose to lazy mint 'delayed-reveal' tokens. More on 'delayed-reveal' tokens in [this article](https://blog.thirdweb.com/delayed-reveal-nfts). A contract admin (i.e. holder of `DEFAULT_ADMIN_ROLE`) can create claim conditions with non-overlapping time windows, and accounts can claim the tokens according to restrictions defined in the claim condition that is active at the time of the transaction. - - - -## Methods - -### balanceOf - -```solidity -function balanceOf(address account, uint256 id) external view returns (uint256) -``` - - - -*Returns the amount of tokens of token type `id` owned by `account`. Requirements: - `account` cannot be the zero address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| id | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### balanceOfBatch - -```solidity -function balanceOfBatch(address[] accounts, uint256[] ids) external view returns (uint256[]) -``` - - - -*xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {balanceOf}. Requirements: - `accounts` and `ids` must have the same length.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| accounts | address[] | undefined -| ids | uint256[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256[] | undefined - -### claim - -```solidity -function claim(address receiver, uint256 tokenId, uint256 quantity, address currency, uint256 pricePerToken, bytes32[] proofs, uint256 proofMaxQuantityPerTransaction) external payable -``` - -Lets an account claim a given quantity of NFTs. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| receiver | address | The receiver of the NFTs to claim. -| tokenId | uint256 | The unique ID of the token to claim. -| quantity | uint256 | The quantity of NFTs to claim. -| currency | address | The currency in which to pay for the claim. -| pricePerToken | uint256 | The price per token to pay for the claim. -| proofs | bytes32[] | The proof of the claimer's inclusion in the merkle root allowlist of the claim conditions that apply. -| proofMaxQuantityPerTransaction | uint256 | (Optional) The maximum number of NFTs an address included in an allowlist can claim. - -### isApprovedForAll - -```solidity -function isApprovedForAll(address account, address operator) external view returns (bool) -``` - - - -*Returns true if `operator` is approved to transfer ``account``'s tokens. See {setApprovalForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### lazyMint - -```solidity -function lazyMint(uint256 amount, string baseURIForTokens) external nonpayable -``` - -Lets an account with `MINTER_ROLE` lazy mint 'n' NFTs. The URIs for each token is the provided `_baseURIForTokens` + `{tokenId}`. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| amount | uint256 | The amount of NFTs to lazy mint. -| baseURIForTokens | string | The URI for the NFTs to lazy mint. - -### safeBatchTransferFrom - -```solidity -function safeBatchTransferFrom(address from, address to, uint256[] ids, uint256[] amounts, bytes data) external nonpayable -``` - - - -*xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {safeTransferFrom}. Emits a {TransferBatch} event. Requirements: - `ids` and `amounts` must have the same length. - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the acceptance magic value.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| ids | uint256[] | undefined -| amounts | uint256[] | undefined -| data | bytes | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data) external nonpayable -``` - - - -*Transfers `amount` tokens of token type `id` from `from` to `to`. Emits a {TransferSingle} event. Requirements: - `to` cannot be the zero address. - If the caller is not `from`, it must be have been approved to spend ``from``'s tokens via {setApprovalForAll}. - `from` must have a balance of tokens of type `id` of at least `amount`. - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the acceptance magic value.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| id | uint256 | undefined -| amount | uint256 | undefined -| data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool approved) external nonpayable -``` - - - -*Grants or revokes permission to `operator` to transfer the caller's tokens, according to `approved`, Emits an {ApprovalForAll} event. Requirements: - `operator` cannot be the caller.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| approved | bool | undefined - -### setClaimConditions - -```solidity -function setClaimConditions(uint256 tokenId, IDropClaimCondition.ClaimCondition[] phases, bool resetClaimEligibility) external nonpayable -``` - -Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | The token ID for which to set mint conditions. -| phases | IDropClaimCondition.ClaimCondition[] | Claim conditions in ascending order by `startTimestamp`. -| resetClaimEligibility | bool | Whether to reset `limitLastClaimTimestamp` and `limitMerkleProofClaim` values when setting new claim conditions. - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - -## Events - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed account, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### ClaimConditionsUpdated - -```solidity -event ClaimConditionsUpdated(uint256 indexed tokenId, IDropClaimCondition.ClaimCondition[] claimConditions) -``` - - - -*Emitted when new claim conditions are set for a token.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId `indexed` | uint256 | undefined | -| claimConditions | IDropClaimCondition.ClaimCondition[] | undefined | - -### MaxTotalSupplyUpdated - -```solidity -event MaxTotalSupplyUpdated(uint256 tokenId, uint256 maxTotalSupply) -``` - - - -*Emitted when the global max supply of a token is updated.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined | -| maxTotalSupply | uint256 | undefined | - -### MaxWalletClaimCountUpdated - -```solidity -event MaxWalletClaimCountUpdated(uint256 tokenId, uint256 count) -``` - - - -*Emitted when the max wallet claim count for a given tokenId is updated.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined | -| count | uint256 | undefined | - -### SaleRecipientForTokenUpdated - -```solidity -event SaleRecipientForTokenUpdated(uint256 indexed tokenId, address saleRecipient) -``` - - - -*Emitted when the sale recipient for a particular tokenId is updated.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId `indexed` | uint256 | undefined | -| saleRecipient | address | undefined | - -### TokensClaimed - -```solidity -event TokensClaimed(uint256 indexed claimConditionIndex, uint256 indexed tokenId, address indexed claimer, address receiver, uint256 quantityClaimed) -``` - - - -*Emitted when tokens are claimed.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| claimConditionIndex `indexed` | uint256 | undefined | -| tokenId `indexed` | uint256 | undefined | -| claimer `indexed` | address | undefined | -| receiver | address | undefined | -| quantityClaimed | uint256 | undefined | - -### TokensLazyMinted - -```solidity -event TokensLazyMinted(uint256 startTokenId, uint256 endTokenId, string baseURI) -``` - - - -*Emitted when tokens are lazy minted.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| startTokenId | uint256 | undefined | -| endTokenId | uint256 | undefined | -| baseURI | string | undefined | - -### TransferBatch - -```solidity -event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| ids | uint256[] | undefined | -| values | uint256[] | undefined | - -### TransferSingle - -```solidity -event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| id | uint256 | undefined | -| value | uint256 | undefined | - -### URI - -```solidity -event URI(string value, uint256 indexed id) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| value | string | undefined | -| id `indexed` | uint256 | undefined | - -### WalletClaimCountUpdated - -```solidity -event WalletClaimCountUpdated(uint256 tokenId, address indexed wallet, uint256 count) -``` - - - -*Emitted when the wallet claim count for a given tokenId and address is updated.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined | -| wallet `indexed` | address | undefined | -| count | uint256 | undefined | - - - diff --git a/docs/IDropERC20.md b/docs/IDropERC20.md deleted file mode 100644 index c98e084f8..000000000 --- a/docs/IDropERC20.md +++ /dev/null @@ -1,308 +0,0 @@ -# IDropERC20 - - - - - -Thirdweb's 'Drop' contracts are distribution mechanisms for tokens. The `DropERC20` contract is a distribution mechanism for ERC20 tokens. A contract admin (i.e. holder of `DEFAULT_ADMIN_ROLE`) can create claim conditions with non-overlapping time windows, and accounts can claim the tokens according to restrictions defined in the claim condition that is active at the time of the transaction. - - - -## Methods - -### allowance - -```solidity -function allowance(address owner, address spender) external view returns (uint256) -``` - - - -*Returns the remaining number of tokens that `spender` will be allowed to spend on behalf of `owner` through {transferFrom}. This is zero by default. This value changes when {approve} or {transferFrom} are called.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| spender | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### approve - -```solidity -function approve(address spender, uint256 amount) external nonpayable returns (bool) -``` - - - -*Sets `amount` as the allowance of `spender` over the caller's tokens. Returns a boolean value indicating whether the operation succeeded. IMPORTANT: Beware that changing an allowance with this method brings the risk that someone may use both the old and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 Emits an {Approval} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### balanceOf - -```solidity -function balanceOf(address account) external view returns (uint256) -``` - - - -*Returns the amount of tokens owned by `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### claim - -```solidity -function claim(address receiver, uint256 quantity, address currency, uint256 pricePerToken, bytes32[] proofs, uint256 proofMaxQuantityPerTransaction) external payable -``` - -Lets an account claim a given quantity of tokens. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| receiver | address | The receiver of the tokens to claim. -| quantity | uint256 | The quantity of tokens to claim. -| currency | address | The currency in which to pay for the claim. -| pricePerToken | uint256 | The price per token (i.e. price per 1 ether unit of the token) to pay for the claim. -| proofs | bytes32[] | The proof of the claimer's inclusion in the merkle root allowlist of the claim conditions that apply. -| proofMaxQuantityPerTransaction | uint256 | (Optional) The maximum number of tokens an address included in an allowlist can claim. - -### setClaimConditions - -```solidity -function setClaimConditions(IDropClaimCondition.ClaimCondition[] phases, bool resetClaimEligibility) external nonpayable -``` - -Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| phases | IDropClaimCondition.ClaimCondition[] | Claim conditions in ascending order by `startTimestamp`. -| resetClaimEligibility | bool | Whether to reset `limitLastClaimTimestamp` and `limitMerkleProofClaim` values when setting new claim conditions. - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - -*Returns the amount of tokens in existence.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transfer - -```solidity -function transfer(address to, uint256 amount) external nonpayable returns (bool) -``` - - - -*Moves `amount` tokens from the caller's account to `to`. Returns a boolean value indicating whether the operation succeeded. Emits a {Transfer} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 amount) external nonpayable returns (bool) -``` - - - -*Moves `amount` tokens from `from` to `to` using the allowance mechanism. `amount` is then deducted from the caller's allowance. Returns a boolean value indicating whether the operation succeeded. Emits a {Transfer} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed spender, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| spender `indexed` | address | undefined | -| value | uint256 | undefined | - -### ClaimConditionsUpdated - -```solidity -event ClaimConditionsUpdated(IDropClaimCondition.ClaimCondition[] claimConditions) -``` - - - -*Emitted when new claim conditions are set.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| claimConditions | IDropClaimCondition.ClaimCondition[] | undefined | - -### MaxTotalSupplyUpdated - -```solidity -event MaxTotalSupplyUpdated(uint256 maxTotalSupply) -``` - - - -*Emitted when the global max supply of tokens is updated.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| maxTotalSupply | uint256 | undefined | - -### MaxWalletClaimCountUpdated - -```solidity -event MaxWalletClaimCountUpdated(uint256 count) -``` - - - -*Emitted when the global max wallet claim count is updated.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| count | uint256 | undefined | - -### TokensClaimed - -```solidity -event TokensClaimed(uint256 indexed claimConditionIndex, address indexed claimer, address indexed receiver, uint256 quantityClaimed) -``` - - - -*Emitted when tokens are claimed.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| claimConditionIndex `indexed` | uint256 | undefined | -| claimer `indexed` | address | undefined | -| receiver `indexed` | address | undefined | -| quantityClaimed | uint256 | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| value | uint256 | undefined | - -### WalletClaimCountUpdated - -```solidity -event WalletClaimCountUpdated(address indexed wallet, uint256 count) -``` - - - -*Emitted when the wallet claim count for an address is updated.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| wallet `indexed` | address | undefined | -| count | uint256 | undefined | - - - diff --git a/docs/IDropERC721.md b/docs/IDropERC721.md deleted file mode 100644 index 4f79c4fd4..000000000 --- a/docs/IDropERC721.md +++ /dev/null @@ -1,431 +0,0 @@ -# IDropERC721 - - - - - -Thirdweb's 'Drop' contracts are distribution mechanisms for tokens. The `DropERC721` contract is a distribution mechanism for ERC721 tokens. A minter wallet (i.e. holder of `MINTER_ROLE`) can (lazy)mint 'n' tokens at once by providing a single base URI for all tokens being lazy minted. The URI for each of the 'n' tokens lazy minted is the provided base URI + `{tokenId}` of the respective token. (e.g. "ipsf://Qmece.../1"). A minter can choose to lazy mint 'delayed-reveal' tokens. More on 'delayed-reveal' tokens in [this article](https://blog.thirdweb.com/delayed-reveal-nfts). A contract admin (i.e. holder of `DEFAULT_ADMIN_ROLE`) can create claim conditions with non-overlapping time windows, and accounts can claim the tokens according to restrictions defined in the claim condition that is active at the time of the transaction. - - - -## Methods - -### approve - -```solidity -function approve(address to, uint256 tokenId) external nonpayable -``` - - - -*Gives permission to `to` to transfer `tokenId` token to another account. The approval is cleared when the token is transferred. Only a single account can be approved at a time, so approving the zero address clears previous approvals. Requirements: - The caller must own the token or be an approved operator. - `tokenId` must exist. Emits an {Approval} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| tokenId | uint256 | undefined - -### balanceOf - -```solidity -function balanceOf(address owner) external view returns (uint256 balance) -``` - - - -*Returns the number of tokens in ``owner``'s account.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| balance | uint256 | undefined - -### claim - -```solidity -function claim(address receiver, uint256 quantity, address currency, uint256 pricePerToken, bytes32[] proofs, uint256 proofMaxQuantityPerTransaction) external payable -``` - -Lets an account claim a given quantity of NFTs. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| receiver | address | The receiver of the NFTs to claim. -| quantity | uint256 | The quantity of NFTs to claim. -| currency | address | The currency in which to pay for the claim. -| pricePerToken | uint256 | The price per token to pay for the claim. -| proofs | bytes32[] | The proof of the claimer's inclusion in the merkle root allowlist of the claim conditions that apply. -| proofMaxQuantityPerTransaction | uint256 | (Optional) The maximum number of NFTs an address included in an allowlist can claim. - -### getApproved - -```solidity -function getApproved(uint256 tokenId) external view returns (address operator) -``` - - - -*Returns the account approved for `tokenId` token. Requirements: - `tokenId` must exist.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address owner, address operator) external view returns (bool) -``` - - - -*Returns if the `operator` is allowed to manage all of the assets of `owner`. See {setApprovalForAll}* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### lazyMint - -```solidity -function lazyMint(uint256 amount, string baseURIForTokens, bytes encryptedBaseURI) external nonpayable -``` - -Lets an account with `MINTER_ROLE` lazy mint 'n' NFTs. The URIs for each token is the provided `_baseURIForTokens` + `{tokenId}`. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| amount | uint256 | The amount of NFTs to lazy mint. -| baseURIForTokens | string | The URI for the NFTs to lazy mint. If lazy minting 'delayed-reveal' NFTs, the is a URI for NFTs in the un-revealed state. -| encryptedBaseURI | bytes | If lazy minting 'delayed-reveal' NFTs, this is the result of encrypting the URI of the NFTs in the revealed state. - -### ownerOf - -```solidity -function ownerOf(uint256 tokenId) external view returns (address owner) -``` - - - -*Returns the owner of the `tokenId` token. Requirements: - `tokenId` must exist.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 tokenId, bytes data) external nonpayable -``` - - - -*Safely transfers `tokenId` token from `from` to `to`. Requirements: - `from` cannot be the zero address. - `to` cannot be the zero address. - `tokenId` token must exist and be owned by `from`. - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. Emits a {Transfer} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined -| data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool _approved) external nonpayable -``` - - - -*Approve or remove `operator` as an operator for the caller. Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller. Requirements: - The `operator` cannot be the caller. Emits an {ApprovalForAll} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| _approved | bool | undefined - -### setClaimConditions - -```solidity -function setClaimConditions(IDropClaimCondition.ClaimCondition[] phases, bool resetClaimEligibility) external nonpayable -``` - -Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| phases | IDropClaimCondition.ClaimCondition[] | Claim conditions in ascending order by `startTimestamp`. -| resetClaimEligibility | bool | Whether to reset `limitLastClaimTimestamp` and `limitMerkleProofClaim` values when setting new claim conditions. - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 tokenId) external nonpayable -``` - - - -*Transfers `tokenId` token from `from` to `to`. WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible. Requirements: - `from` cannot be the zero address. - `to` cannot be the zero address. - `tokenId` token must be owned by `from`. - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. Emits a {Transfer} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| approved `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed owner, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### ClaimConditionsUpdated - -```solidity -event ClaimConditionsUpdated(IDropClaimCondition.ClaimCondition[] claimConditions) -``` - - - -*Emitted when new claim conditions are set.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| claimConditions | IDropClaimCondition.ClaimCondition[] | undefined | - -### MaxTotalSupplyUpdated - -```solidity -event MaxTotalSupplyUpdated(uint256 maxTotalSupply) -``` - - - -*Emitted when the global max supply of tokens is updated.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| maxTotalSupply | uint256 | undefined | - -### MaxWalletClaimCountUpdated - -```solidity -event MaxWalletClaimCountUpdated(uint256 count) -``` - - - -*Emitted when the global max wallet claim count is updated.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| count | uint256 | undefined | - -### NFTRevealed - -```solidity -event NFTRevealed(uint256 endTokenId, string revealedURI) -``` - - - -*Emitted when the URI for a batch of 'delayed-reveal' NFTs is revealed.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| endTokenId | uint256 | undefined | -| revealedURI | string | undefined | - -### TokensClaimed - -```solidity -event TokensClaimed(uint256 indexed claimConditionIndex, address indexed claimer, address indexed receiver, uint256 startTokenId, uint256 quantityClaimed) -``` - - - -*Emitted when tokens are claimed.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| claimConditionIndex `indexed` | uint256 | undefined | -| claimer `indexed` | address | undefined | -| receiver `indexed` | address | undefined | -| startTokenId | uint256 | undefined | -| quantityClaimed | uint256 | undefined | - -### TokensLazyMinted - -```solidity -event TokensLazyMinted(uint256 startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI) -``` - - - -*Emitted when tokens are lazy minted.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| startTokenId | uint256 | undefined | -| endTokenId | uint256 | undefined | -| baseURI | string | undefined | -| encryptedBaseURI | bytes | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - -### WalletClaimCountUpdated - -```solidity -event WalletClaimCountUpdated(address indexed wallet, uint256 count) -``` - - - -*Emitted when the wallet claim count for an address is updated.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| wallet `indexed` | address | undefined | -| count | uint256 | undefined | - - - diff --git a/docs/IDropSinglePhase.md b/docs/IDropSinglePhase.md deleted file mode 100644 index fe4c82132..000000000 --- a/docs/IDropSinglePhase.md +++ /dev/null @@ -1,92 +0,0 @@ -# IDropSinglePhase - - - - - - - - - -## Methods - -### claim - -```solidity -function claim(address receiver, uint256 quantity, address currency, uint256 pricePerToken, IDropSinglePhase.AllowlistProof allowlistProof, bytes data) external payable -``` - -Lets an account claim a given quantity of NFTs. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| receiver | address | The receiver of the NFTs to claim. -| quantity | uint256 | The quantity of NFTs to claim. -| currency | address | The currency in which to pay for the claim. -| pricePerToken | uint256 | The price per token to pay for the claim. -| allowlistProof | IDropSinglePhase.AllowlistProof | The proof of the claimer's inclusion in the merkle root allowlist of the claim conditions that apply. -| data | bytes | Arbitrary bytes data that can be leveraged in the implementation of this interface. - -### setClaimConditions - -```solidity -function setClaimConditions(IClaimCondition.ClaimCondition phase, bool resetClaimEligibility) external nonpayable -``` - -Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| phase | IClaimCondition.ClaimCondition | Claim condition to set. -| resetClaimEligibility | bool | Whether to reset `limitLastClaimTimestamp` and `limitMerkleProofClaim` values when setting new claim conditions. - - - -## Events - -### ClaimConditionUpdated - -```solidity -event ClaimConditionUpdated(IClaimCondition.ClaimCondition condition, bool resetEligibility) -``` - - - -*Emitted when the contract's claim conditions are updated.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| condition | IClaimCondition.ClaimCondition | undefined | -| resetEligibility | bool | undefined | - -### TokensClaimed - -```solidity -event TokensClaimed(address indexed claimer, address indexed receiver, uint256 indexed startTokenId, uint256 quantityClaimed) -``` - - - -*Emitted when tokens are claimed via `claim`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| claimer `indexed` | address | undefined | -| receiver `indexed` | address | undefined | -| startTokenId `indexed` | uint256 | undefined | -| quantityClaimed | uint256 | undefined | - - - diff --git a/docs/IERC1155.md b/docs/IERC1155.md deleted file mode 100644 index 32ca592ae..000000000 --- a/docs/IERC1155.md +++ /dev/null @@ -1,219 +0,0 @@ -# IERC1155 - - - -> ERC-1155 Multi Token Standard - - - -*See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1155.md Note: The ERC-165 identifier for this interface is 0xd9b67a26.* - -## Methods - -### balanceOf - -```solidity -function balanceOf(address _owner, uint256 _id) external view returns (uint256) -``` - -Get the balance of an account's Tokens. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _owner | address | The address of the token holder -| _id | uint256 | ID of the Token - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | The _owner's balance of the Token type requested - -### balanceOfBatch - -```solidity -function balanceOfBatch(address[] _owners, uint256[] _ids) external view returns (uint256[]) -``` - -Get the balance of multiple account/token pairs - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _owners | address[] | The addresses of the token holders -| _ids | uint256[] | ID of the Tokens - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256[] | The _owner's balance of the Token types requested (i.e. balance for each (owner, id) pair) - -### isApprovedForAll - -```solidity -function isApprovedForAll(address _owner, address _operator) external view returns (bool) -``` - -Queries the approval status of an operator for a given owner. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _owner | address | The owner of the Tokens -| _operator | address | Address of authorized operator - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | True if the operator is approved, false if not - -### safeBatchTransferFrom - -```solidity -function safeBatchTransferFrom(address _from, address _to, uint256[] _ids, uint256[] _values, bytes _data) external nonpayable -``` - -Transfers `_values` amount(s) of `_ids` from the `_from` address to the `_to` address specified (with safety call). - -*Caller must be approved to manage the tokens being transferred out of the `_from` account (see "Approval" section of the standard). MUST revert if `_to` is the zero address. MUST revert if length of `_ids` is not the same as length of `_values`. MUST revert if any of the balance(s) of the holder(s) for token(s) in `_ids` is lower than the respective amount(s) in `_values` sent to the recipient. MUST revert on any other error. MUST emit `TransferSingle` or `TransferBatch` event(s) such that all the balance changes are reflected (see "Safe Transfer Rules" section of the standard). Balance changes and events MUST follow the ordering of the arrays (_ids[0]/_values[0] before _ids[1]/_values[1], etc). After the above conditions for the transfer(s) in the batch are met, this function MUST check if `_to` is a smart contract (e.g. code size > 0). If so, it MUST call the relevant `ERC1155TokenReceiver` hook(s) on `_to` and act appropriately (see "Safe Transfer Rules" section of the standard).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _from | address | Source address -| _to | address | Target address -| _ids | uint256[] | IDs of each token type (order and length must match _values array) -| _values | uint256[] | Transfer amounts per token type (order and length must match _ids array) -| _data | bytes | Additional data with no specified format, MUST be sent unaltered in call to the `ERC1155TokenReceiver` hook(s) on `_to` - -### safeTransferFrom - -```solidity -function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes _data) external nonpayable -``` - -Transfers `_value` amount of an `_id` from the `_from` address to the `_to` address specified (with safety call). - -*Caller must be approved to manage the tokens being transferred out of the `_from` account (see "Approval" section of the standard). MUST revert if `_to` is the zero address. MUST revert if balance of holder for token `_id` is lower than the `_value` sent. MUST revert on any other error. MUST emit the `TransferSingle` event to reflect the balance change (see "Safe Transfer Rules" section of the standard). After the above conditions are met, this function MUST check if `_to` is a smart contract (e.g. code size > 0). If so, it MUST call `onERC1155Received` on `_to` and act appropriately (see "Safe Transfer Rules" section of the standard).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _from | address | Source address -| _to | address | Target address -| _id | uint256 | ID of the token type -| _value | uint256 | Transfer amount -| _data | bytes | Additional data with no specified format, MUST be sent unaltered in call to `onERC1155Received` on `_to` - -### setApprovalForAll - -```solidity -function setApprovalForAll(address _operator, bool _approved) external nonpayable -``` - -Enable or disable approval for a third party ("operator") to manage all of the caller's tokens. - -*MUST emit the ApprovalForAll event on success.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _operator | address | Address to add to the set of authorized operators -| _approved | bool | True if the operator is approved, false to revoke approval - - - -## Events - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved) -``` - - - -*MUST emit when approval for a second party/operator address to manage all tokens for an owner address is enabled or disabled (absense of an event assumes disabled).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _owner `indexed` | address | undefined | -| _operator `indexed` | address | undefined | -| _approved | bool | undefined | - -### TransferBatch - -```solidity -event TransferBatch(address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values) -``` - - - -*Either `TransferSingle` or `TransferBatch` MUST emit when tokens are transferred, including zero value transfers as well as minting or burning (see "Safe Transfer Rules" section of the standard). The `_operator` argument MUST be msg.sender. The `_from` argument MUST be the address of the holder whose balance is decreased. The `_to` argument MUST be the address of the recipient whose balance is increased. The `_ids` argument MUST be the list of tokens being transferred. The `_values` argument MUST be the list of number of tokens (matching the list and order of tokens specified in _ids) the holder balance is decreased by and match what the recipient balance is increased by. When minting/creating tokens, the `_from` argument MUST be set to `0x0` (i.e. zero address). When burning/destroying tokens, the `_to` argument MUST be set to `0x0` (i.e. zero address).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _operator `indexed` | address | undefined | -| _from `indexed` | address | undefined | -| _to `indexed` | address | undefined | -| _ids | uint256[] | undefined | -| _values | uint256[] | undefined | - -### TransferSingle - -```solidity -event TransferSingle(address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value) -``` - - - -*Either `TransferSingle` or `TransferBatch` MUST emit when tokens are transferred, including zero value transfers as well as minting or burning (see "Safe Transfer Rules" section of the standard). The `_operator` argument MUST be msg.sender. The `_from` argument MUST be the address of the holder whose balance is decreased. The `_to` argument MUST be the address of the recipient whose balance is increased. The `_id` argument MUST be the token type being transferred. The `_value` argument MUST be the number of tokens the holder balance is decreased by and match what the recipient balance is increased by. When minting/creating tokens, the `_from` argument MUST be set to `0x0` (i.e. zero address). When burning/destroying tokens, the `_to` argument MUST be set to `0x0` (i.e. zero address).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _operator `indexed` | address | undefined | -| _from `indexed` | address | undefined | -| _to `indexed` | address | undefined | -| _id | uint256 | undefined | -| _value | uint256 | undefined | - -### URI - -```solidity -event URI(string _value, uint256 indexed _id) -``` - - - -*MUST emit when the URI is updated for a token ID. URIs are defined in RFC 3986. The URI MUST point a JSON file that conforms to the "ERC-1155 Metadata URI JSON Schema".* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _value | string | undefined | -| _id `indexed` | uint256 | undefined | - - - diff --git a/docs/IERC1155Enumerable.md b/docs/IERC1155Enumerable.md deleted file mode 100644 index a48752751..000000000 --- a/docs/IERC1155Enumerable.md +++ /dev/null @@ -1,32 +0,0 @@ -# IERC1155Enumerable - - - -> ERC1155 Non-Fungible Token Standard, optional enumeration extension - - - -*See https://eips.ethereum.org/EIPS/eip-1155* - -## Methods - -### nextTokenIdToMint - -```solidity -function nextTokenIdToMint() external view returns (uint256) -``` - -Returns the next token ID available for minting - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | The token identifier for the `_index`th NFT, (sort order not specified) - - - - diff --git a/docs/IERC1155Metadata.md b/docs/IERC1155Metadata.md deleted file mode 100644 index de9161954..000000000 --- a/docs/IERC1155Metadata.md +++ /dev/null @@ -1,37 +0,0 @@ -# IERC1155Metadata - - - - - -Note: The ERC-165 identifier for this interface is 0x0e89341c. - - - -## Methods - -### uri - -```solidity -function uri(uint256 _id) external view returns (string) -``` - -A distinct Uniform Resource Identifier (URI) for a given token. - -*URIs are defined in RFC 3986. The URI may point to a JSON file that conforms to the "ERC-1155 Metadata URI JSON Schema".* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _id | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | URI string - - - - diff --git a/docs/IERC1155MetadataURIUpgradeable.md b/docs/IERC1155MetadataURIUpgradeable.md deleted file mode 100644 index 71bb57029..000000000 --- a/docs/IERC1155MetadataURIUpgradeable.md +++ /dev/null @@ -1,263 +0,0 @@ -# IERC1155MetadataURIUpgradeable - - - - - - - -*Interface of the optional ERC1155MetadataExtension interface, as defined in the https://eips.ethereum.org/EIPS/eip-1155#metadata-extensions[EIP]. _Available since v3.1._* - -## Methods - -### balanceOf - -```solidity -function balanceOf(address account, uint256 id) external view returns (uint256) -``` - - - -*Returns the amount of tokens of token type `id` owned by `account`. Requirements: - `account` cannot be the zero address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| id | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### balanceOfBatch - -```solidity -function balanceOfBatch(address[] accounts, uint256[] ids) external view returns (uint256[]) -``` - - - -*xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {balanceOf}. Requirements: - `accounts` and `ids` must have the same length.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| accounts | address[] | undefined -| ids | uint256[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256[] | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address account, address operator) external view returns (bool) -``` - - - -*Returns true if `operator` is approved to transfer ``account``'s tokens. See {setApprovalForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### safeBatchTransferFrom - -```solidity -function safeBatchTransferFrom(address from, address to, uint256[] ids, uint256[] amounts, bytes data) external nonpayable -``` - - - -*xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {safeTransferFrom}. Emits a {TransferBatch} event. Requirements: - `ids` and `amounts` must have the same length. - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the acceptance magic value.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| ids | uint256[] | undefined -| amounts | uint256[] | undefined -| data | bytes | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data) external nonpayable -``` - - - -*Transfers `amount` tokens of token type `id` from `from` to `to`. Emits a {TransferSingle} event. Requirements: - `to` cannot be the zero address. - If the caller is not `from`, it must be have been approved to spend ``from``'s tokens via {setApprovalForAll}. - `from` must have a balance of tokens of type `id` of at least `amount`. - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the acceptance magic value.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| id | uint256 | undefined -| amount | uint256 | undefined -| data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool approved) external nonpayable -``` - - - -*Grants or revokes permission to `operator` to transfer the caller's tokens, according to `approved`, Emits an {ApprovalForAll} event. Requirements: - `operator` cannot be the caller.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| approved | bool | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### uri - -```solidity -function uri(uint256 id) external view returns (string) -``` - - - -*Returns the URI for token type `id`. If the `\{id\}` substring is present in the URI, it must be replaced by clients with the actual token type ID.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| id | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - - - -## Events - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed account, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### TransferBatch - -```solidity -event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| ids | uint256[] | undefined | -| values | uint256[] | undefined | - -### TransferSingle - -```solidity -event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| id | uint256 | undefined | -| value | uint256 | undefined | - -### URI - -```solidity -event URI(string value, uint256 indexed id) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| value | string | undefined | -| id `indexed` | uint256 | undefined | - - - diff --git a/docs/IERC1155Receiver.md b/docs/IERC1155Receiver.md deleted file mode 100644 index 5701c5057..000000000 --- a/docs/IERC1155Receiver.md +++ /dev/null @@ -1,89 +0,0 @@ -# IERC1155Receiver - - - - - - - -*_Available since v3.1._* - -## Methods - -### onERC1155BatchReceived - -```solidity -function onERC1155BatchReceived(address operator, address from, uint256[] ids, uint256[] values, bytes data) external nonpayable returns (bytes4) -``` - - - -*Handles the receipt of a multiple ERC1155 token types. This function is called at the end of a `safeBatchTransferFrom` after the balances have been updated. NOTE: To accept the transfer(s), this must return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` (i.e. 0xbc197c81, or its own function selector).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | The address which initiated the batch transfer (i.e. msg.sender) -| from | address | The address which previously owned the token -| ids | uint256[] | An array containing ids of each token being transferred (order and length must match values array) -| values | uint256[] | An array containing amounts of each token being transferred (order and length must match ids array) -| data | bytes | Additional data with no specified format - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed - -### onERC1155Received - -```solidity -function onERC1155Received(address operator, address from, uint256 id, uint256 value, bytes data) external nonpayable returns (bytes4) -``` - - - -*Handles the receipt of a single ERC1155 token type. This function is called at the end of a `safeTransferFrom` after the balance has been updated. NOTE: To accept the transfer, this must return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` (i.e. 0xf23a6e61, or its own function selector).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | The address which initiated the transfer (i.e. msg.sender) -| from | address | The address which previously owned the token -| id | uint256 | The ID of the token being transferred -| value | uint256 | The amount of tokens being transferred -| data | bytes | Additional data with no specified format - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - - diff --git a/docs/IERC1155ReceiverUpgradeable.md b/docs/IERC1155ReceiverUpgradeable.md deleted file mode 100644 index fdd7faab9..000000000 --- a/docs/IERC1155ReceiverUpgradeable.md +++ /dev/null @@ -1,89 +0,0 @@ -# IERC1155ReceiverUpgradeable - - - - - - - -*_Available since v3.1._* - -## Methods - -### onERC1155BatchReceived - -```solidity -function onERC1155BatchReceived(address operator, address from, uint256[] ids, uint256[] values, bytes data) external nonpayable returns (bytes4) -``` - - - -*Handles the receipt of a multiple ERC1155 token types. This function is called at the end of a `safeBatchTransferFrom` after the balances have been updated. NOTE: To accept the transfer(s), this must return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` (i.e. 0xbc197c81, or its own function selector).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | The address which initiated the batch transfer (i.e. msg.sender) -| from | address | The address which previously owned the token -| ids | uint256[] | An array containing ids of each token being transferred (order and length must match values array) -| values | uint256[] | An array containing amounts of each token being transferred (order and length must match ids array) -| data | bytes | Additional data with no specified format - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed - -### onERC1155Received - -```solidity -function onERC1155Received(address operator, address from, uint256 id, uint256 value, bytes data) external nonpayable returns (bytes4) -``` - - - -*Handles the receipt of a single ERC1155 token type. This function is called at the end of a `safeTransferFrom` after the balance has been updated. NOTE: To accept the transfer, this must return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` (i.e. 0xf23a6e61, or its own function selector).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | The address which initiated the transfer (i.e. msg.sender) -| from | address | The address which previously owned the token -| id | uint256 | The ID of the token being transferred -| value | uint256 | The amount of tokens being transferred -| data | bytes | Additional data with no specified format - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - - diff --git a/docs/IERC1155Supply.md b/docs/IERC1155Supply.md deleted file mode 100644 index ebd2b27d3..000000000 --- a/docs/IERC1155Supply.md +++ /dev/null @@ -1,37 +0,0 @@ -# IERC1155Supply - - - -> ERC1155S Non-Fungible Token Standard, optional supply extension - - - -*See https://eips.ethereum.org/EIPS/eip-1155* - -## Methods - -### totalSupply - -```solidity -function totalSupply(uint256 id) external view returns (uint256) -``` - -Count NFTs tracked by this contract - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| id | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | A count of valid NFTs tracked by this contract, where each one of them has an assigned and queryable owner not equal to the zero address - - - - diff --git a/docs/IERC1155Upgradeable.md b/docs/IERC1155Upgradeable.md deleted file mode 100644 index 525bd2b8e..000000000 --- a/docs/IERC1155Upgradeable.md +++ /dev/null @@ -1,241 +0,0 @@ -# IERC1155Upgradeable - - - - - - - -*Required interface of an ERC1155 compliant contract, as defined in the https://eips.ethereum.org/EIPS/eip-1155[EIP]. _Available since v3.1._* - -## Methods - -### balanceOf - -```solidity -function balanceOf(address account, uint256 id) external view returns (uint256) -``` - - - -*Returns the amount of tokens of token type `id` owned by `account`. Requirements: - `account` cannot be the zero address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| id | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### balanceOfBatch - -```solidity -function balanceOfBatch(address[] accounts, uint256[] ids) external view returns (uint256[]) -``` - - - -*xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {balanceOf}. Requirements: - `accounts` and `ids` must have the same length.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| accounts | address[] | undefined -| ids | uint256[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256[] | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address account, address operator) external view returns (bool) -``` - - - -*Returns true if `operator` is approved to transfer ``account``'s tokens. See {setApprovalForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### safeBatchTransferFrom - -```solidity -function safeBatchTransferFrom(address from, address to, uint256[] ids, uint256[] amounts, bytes data) external nonpayable -``` - - - -*xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {safeTransferFrom}. Emits a {TransferBatch} event. Requirements: - `ids` and `amounts` must have the same length. - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the acceptance magic value.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| ids | uint256[] | undefined -| amounts | uint256[] | undefined -| data | bytes | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data) external nonpayable -``` - - - -*Transfers `amount` tokens of token type `id` from `from` to `to`. Emits a {TransferSingle} event. Requirements: - `to` cannot be the zero address. - If the caller is not `from`, it must be have been approved to spend ``from``'s tokens via {setApprovalForAll}. - `from` must have a balance of tokens of type `id` of at least `amount`. - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the acceptance magic value.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| id | uint256 | undefined -| amount | uint256 | undefined -| data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool approved) external nonpayable -``` - - - -*Grants or revokes permission to `operator` to transfer the caller's tokens, according to `approved`, Emits an {ApprovalForAll} event. Requirements: - `operator` cannot be the caller.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| approved | bool | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - -## Events - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed account, address indexed operator, bool approved) -``` - - - -*Emitted when `account` grants or revokes permission to `operator` to transfer their tokens, according to `approved`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### TransferBatch - -```solidity -event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values) -``` - - - -*Equivalent to multiple {TransferSingle} events, where `operator`, `from` and `to` are the same for all transfers.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| ids | uint256[] | undefined | -| values | uint256[] | undefined | - -### TransferSingle - -```solidity -event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value) -``` - - - -*Emitted when `value` tokens of token type `id` are transferred from `from` to `to` by `operator`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| id | uint256 | undefined | -| value | uint256 | undefined | - -### URI - -```solidity -event URI(string value, uint256 indexed id) -``` - - - -*Emitted when the URI for token type `id` changes to `value`, if it is a non-programmatic URI. If an {URI} event was emitted for `id`, the standard https://eips.ethereum.org/EIPS/eip-1155#metadata-extensions[guarantees] that `value` will equal the value returned by {IERC1155MetadataURI-uri}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| value | string | undefined | -| id `indexed` | uint256 | undefined | - - - diff --git a/docs/IERC165.md b/docs/IERC165.md deleted file mode 100644 index 30bb32277..000000000 --- a/docs/IERC165.md +++ /dev/null @@ -1,37 +0,0 @@ -# IERC165 - - - - - - - -*Interface of the ERC165 standard, as defined in the https://eips.ethereum.org/EIPS/eip-165[EIP]. Implementers can declare support of contract interfaces, which can then be queried by others ({ERC165Checker}). For an implementation, see {ERC165}.* - -## Methods - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - - diff --git a/docs/IERC165Upgradeable.md b/docs/IERC165Upgradeable.md deleted file mode 100644 index 6368722dc..000000000 --- a/docs/IERC165Upgradeable.md +++ /dev/null @@ -1,37 +0,0 @@ -# IERC165Upgradeable - - - - - - - -*Interface of the ERC165 standard, as defined in the https://eips.ethereum.org/EIPS/eip-165[EIP]. Implementers can declare support of contract interfaces, which can then be queried by others ({ERC165Checker}). For an implementation, see {ERC165}.* - -## Methods - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - - diff --git a/docs/IERC20.md b/docs/IERC20.md deleted file mode 100644 index da676f34b..000000000 --- a/docs/IERC20.md +++ /dev/null @@ -1,186 +0,0 @@ -# IERC20 - - - -> ERC20 interface - - - -*see https://github.com/ethereum/EIPs/issues/20* - -## Methods - -### allowance - -```solidity -function allowance(address owner, address spender) external view returns (uint256) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| spender | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### approve - -```solidity -function approve(address spender, uint256 value) external nonpayable returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| value | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### balanceOf - -```solidity -function balanceOf(address who) external view returns (uint256) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| who | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transfer - -```solidity -function transfer(address to, uint256 value) external nonpayable returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| value | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 value) external nonpayable returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| value | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed spender, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| spender `indexed` | address | undefined | -| value | uint256 | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| value | uint256 | undefined | - - - diff --git a/docs/IERC20Metadata.md b/docs/IERC20Metadata.md deleted file mode 100644 index bc6b97bb9..000000000 --- a/docs/IERC20Metadata.md +++ /dev/null @@ -1,66 +0,0 @@ -# IERC20Metadata - - - -> ERC20Metadata interface - - - -*see https://github.com/ethereum/EIPs/issues/20* - -## Methods - -### decimals - -```solidity -function decimals() external view returns (uint8) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint8 | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - - - - diff --git a/docs/IERC20MetadataUpgradeable.md b/docs/IERC20MetadataUpgradeable.md deleted file mode 100644 index 5ec5bf580..000000000 --- a/docs/IERC20MetadataUpgradeable.md +++ /dev/null @@ -1,237 +0,0 @@ -# IERC20MetadataUpgradeable - - - - - - - -*Interface for the optional metadata functions from the ERC20 standard. _Available since v4.1._* - -## Methods - -### allowance - -```solidity -function allowance(address owner, address spender) external view returns (uint256) -``` - - - -*Returns the remaining number of tokens that `spender` will be allowed to spend on behalf of `owner` through {transferFrom}. This is zero by default. This value changes when {approve} or {transferFrom} are called.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| spender | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### approve - -```solidity -function approve(address spender, uint256 amount) external nonpayable returns (bool) -``` - - - -*Sets `amount` as the allowance of `spender` over the caller's tokens. Returns a boolean value indicating whether the operation succeeded. IMPORTANT: Beware that changing an allowance with this method brings the risk that someone may use both the old and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 Emits an {Approval} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### balanceOf - -```solidity -function balanceOf(address account) external view returns (uint256) -``` - - - -*Returns the amount of tokens owned by `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### decimals - -```solidity -function decimals() external view returns (uint8) -``` - - - -*Returns the decimals places of the token.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint8 | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*Returns the name of the token.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - -*Returns the symbol of the token.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - -*Returns the amount of tokens in existence.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transfer - -```solidity -function transfer(address to, uint256 amount) external nonpayable returns (bool) -``` - - - -*Moves `amount` tokens from the caller's account to `to`. Returns a boolean value indicating whether the operation succeeded. Emits a {Transfer} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 amount) external nonpayable returns (bool) -``` - - - -*Moves `amount` tokens from `from` to `to` using the allowance mechanism. `amount` is then deducted from the caller's allowance. Returns a boolean value indicating whether the operation succeeded. Emits a {Transfer} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed spender, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| spender `indexed` | address | undefined | -| value | uint256 | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| value | uint256 | undefined | - - - diff --git a/docs/IERC20PermitUpgradeable.md b/docs/IERC20PermitUpgradeable.md deleted file mode 100644 index 2c3e2e6d1..000000000 --- a/docs/IERC20PermitUpgradeable.md +++ /dev/null @@ -1,76 +0,0 @@ -# IERC20PermitUpgradeable - - - - - - - -*Interface of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't need to send a transaction, and thus is not required to hold Ether at all.* - -## Methods - -### DOMAIN_SEPARATOR - -```solidity -function DOMAIN_SEPARATOR() external view returns (bytes32) -``` - - - -*Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### nonces - -```solidity -function nonces(address owner) external view returns (uint256) -``` - - - -*Returns the current nonce for `owner`. This value must be included whenever a signature is generated for {permit}. Every successful call to {permit} increases ``owner``'s nonce by one. This prevents a signature from being used multiple times.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### permit - -```solidity -function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external nonpayable -``` - - - -*Sets `value` as the allowance of `spender` over ``owner``'s tokens, given ``owner``'s signed approval. IMPORTANT: The same issues {IERC20-approve} has related to transaction ordering also apply here. Emits an {Approval} event. Requirements: - `spender` cannot be the zero address. - `deadline` must be a timestamp in the future. - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` over the EIP712-formatted function arguments. - the signature must use ``owner``'s current nonce (see {nonces}). For more information on the signature format, see the https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP section].* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| spender | address | undefined -| value | uint256 | undefined -| deadline | uint256 | undefined -| v | uint8 | undefined -| r | bytes32 | undefined -| s | bytes32 | undefined - - - - diff --git a/docs/IERC20Upgradeable.md b/docs/IERC20Upgradeable.md deleted file mode 100644 index f5bac1678..000000000 --- a/docs/IERC20Upgradeable.md +++ /dev/null @@ -1,186 +0,0 @@ -# IERC20Upgradeable - - - - - - - -*Interface of the ERC20 standard as defined in the EIP.* - -## Methods - -### allowance - -```solidity -function allowance(address owner, address spender) external view returns (uint256) -``` - - - -*Returns the remaining number of tokens that `spender` will be allowed to spend on behalf of `owner` through {transferFrom}. This is zero by default. This value changes when {approve} or {transferFrom} are called.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| spender | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### approve - -```solidity -function approve(address spender, uint256 amount) external nonpayable returns (bool) -``` - - - -*Sets `amount` as the allowance of `spender` over the caller's tokens. Returns a boolean value indicating whether the operation succeeded. IMPORTANT: Beware that changing an allowance with this method brings the risk that someone may use both the old and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 Emits an {Approval} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### balanceOf - -```solidity -function balanceOf(address account) external view returns (uint256) -``` - - - -*Returns the amount of tokens owned by `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - -*Returns the amount of tokens in existence.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transfer - -```solidity -function transfer(address to, uint256 amount) external nonpayable returns (bool) -``` - - - -*Moves `amount` tokens from the caller's account to `to`. Returns a boolean value indicating whether the operation succeeded. Emits a {Transfer} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 amount) external nonpayable returns (bool) -``` - - - -*Moves `amount` tokens from `from` to `to` using the allowance mechanism. `amount` is then deducted from the caller's allowance. Returns a boolean value indicating whether the operation succeeded. Emits a {Transfer} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed spender, uint256 value) -``` - - - -*Emitted when the allowance of a `spender` for an `owner` is set by a call to {approve}. `value` is the new allowance.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| spender `indexed` | address | undefined | -| value | uint256 | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 value) -``` - - - -*Emitted when `value` tokens are moved from one account (`from`) to another (`to`). Note that `value` may be zero.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| value | uint256 | undefined | - - - diff --git a/docs/IERC2981.md b/docs/IERC2981.md deleted file mode 100644 index 53de67843..000000000 --- a/docs/IERC2981.md +++ /dev/null @@ -1,61 +0,0 @@ -# IERC2981 - - - - - - - -*Interface for the NFT Royalty Standard. A standardized way to retrieve royalty payment information for non-fungible tokens (NFTs) to enable universal support for royalty payments across all NFT marketplaces and ecosystem participants. _Available since v4.5._* - -## Methods - -### royaltyInfo - -```solidity -function royaltyInfo(uint256 tokenId, uint256 salePrice) external view returns (address receiver, uint256 royaltyAmount) -``` - - - -*Returns how much royalty is owed and to whom, based on a sale price that may be denominated in any unit of exchange. The royalty amount is denominated and should be payed in that same unit of exchange.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined -| salePrice | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| receiver | address | undefined -| royaltyAmount | uint256 | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - - diff --git a/docs/IERC2981Upgradeable.md b/docs/IERC2981Upgradeable.md deleted file mode 100644 index a63c9ac65..000000000 --- a/docs/IERC2981Upgradeable.md +++ /dev/null @@ -1,61 +0,0 @@ -# IERC2981Upgradeable - - - - - - - -*Interface for the NFT Royalty Standard. A standardized way to retrieve royalty payment information for non-fungible tokens (NFTs) to enable universal support for royalty payments across all NFT marketplaces and ecosystem participants. _Available since v4.5._* - -## Methods - -### royaltyInfo - -```solidity -function royaltyInfo(uint256 tokenId, uint256 salePrice) external view returns (address receiver, uint256 royaltyAmount) -``` - - - -*Returns how much royalty is owed and to whom, based on a sale price that may be denominated in any unit of exchange. The royalty amount is denominated and should be payed in that same unit of exchange.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined -| salePrice | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| receiver | address | undefined -| royaltyAmount | uint256 | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - - diff --git a/docs/IERC721.md b/docs/IERC721.md deleted file mode 100644 index 7613ad9f7..000000000 --- a/docs/IERC721.md +++ /dev/null @@ -1,232 +0,0 @@ -# IERC721 - - - - - - - -*Required interface of an ERC721 compliant contract.* - -## Methods - -### approve - -```solidity -function approve(address to, uint256 tokenId) external nonpayable -``` - - - -*Gives permission to `to` to transfer `tokenId` token to another account. The approval is cleared when the token is transferred. Only a single account can be approved at a time, so approving the zero address clears previous approvals. Requirements: - The caller must own the token or be an approved operator. - `tokenId` must exist. Emits an {Approval} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| tokenId | uint256 | undefined - -### balanceOf - -```solidity -function balanceOf(address owner) external view returns (uint256) -``` - - - -*Returns the number of tokens in ``owner``'s account.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getApproved - -```solidity -function getApproved(uint256 tokenId) external view returns (address) -``` - - - -*Returns the account approved for `tokenId` token. Requirements: - `tokenId` must exist.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address owner, address operator) external view returns (bool) -``` - - - -*Returns if the `operator` is allowed to manage all of the assets of `owner`. See {setApprovalForAll}* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### ownerOf - -```solidity -function ownerOf(uint256 tokenId) external view returns (address) -``` - - - -*Returns the owner of the `tokenId` token. Requirements: - `tokenId` must exist.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 tokenId, bytes data) external nonpayable -``` - - - -*Safely transfers `tokenId` token from `from` to `to`. Requirements: - `from` cannot be the zero address. - `to` cannot be the zero address. - `tokenId` token must exist and be owned by `from`. - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. Emits a {Transfer} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined -| data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool _approved) external nonpayable -``` - - - -*Approve or remove `operator` as an operator for the caller. Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller. Requirements: - The `operator` cannot be the caller. Emits an {ApprovalForAll} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| _approved | bool | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 tokenId) external nonpayable -``` - - - -*Transfers `tokenId` token from `from` to `to`. WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible. Requirements: - `from` cannot be the zero address. - `to` cannot be the zero address. - `tokenId` token must be owned by `from`. - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. Emits a {Transfer} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) -``` - - - -*Emitted when `owner` enables `approved` to manage the `tokenId` token.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| approved `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed owner, address indexed operator, bool approved) -``` - - - -*Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) -``` - - - -*Emitted when `tokenId` token is transferred from `from` to `to`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - - - diff --git a/docs/IERC721A.md b/docs/IERC721A.md deleted file mode 100644 index ae7001cc1..000000000 --- a/docs/IERC721A.md +++ /dev/null @@ -1,451 +0,0 @@ -# IERC721A - - - - - - - -*Interface of an ERC721A compliant contract.* - -## Methods - -### approve - -```solidity -function approve(address to, uint256 tokenId) external nonpayable -``` - - - -*Gives permission to `to` to transfer `tokenId` token to another account. The approval is cleared when the token is transferred. Only a single account can be approved at a time, so approving the zero address clears previous approvals. Requirements: - The caller must own the token or be an approved operator. - `tokenId` must exist. Emits an {Approval} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| tokenId | uint256 | undefined - -### balanceOf - -```solidity -function balanceOf(address owner) external view returns (uint256) -``` - - - -*Returns the number of tokens in ``owner``'s account.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getApproved - -```solidity -function getApproved(uint256 tokenId) external view returns (address) -``` - - - -*Returns the account approved for `tokenId` token. Requirements: - `tokenId` must exist.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address owner, address operator) external view returns (bool) -``` - - - -*Returns if the `operator` is allowed to manage all of the assets of `owner`. See {setApprovalForAll}* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### name - -```solidity -function name() external view returns (string) -``` - -A descriptive name for a collection of NFTs in this contract - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### ownerOf - -```solidity -function ownerOf(uint256 tokenId) external view returns (address) -``` - - - -*Returns the owner of the `tokenId` token. Requirements: - `tokenId` must exist.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 tokenId, bytes data) external nonpayable -``` - - - -*Safely transfers `tokenId` token from `from` to `to`. Requirements: - `from` cannot be the zero address. - `to` cannot be the zero address. - `tokenId` token must exist and be owned by `from`. - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. Emits a {Transfer} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined -| data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool _approved) external nonpayable -``` - - - -*Approve or remove `operator` as an operator for the caller. Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller. Requirements: - The `operator` cannot be the caller. Emits an {ApprovalForAll} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| _approved | bool | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - -An abbreviated name for NFTs in this contract - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### tokenURI - -```solidity -function tokenURI(uint256 _tokenId) external view returns (string) -``` - -A distinct Uniform Resource Identifier (URI) for a given asset. - -*Throws if `_tokenId` is not a valid NFT. URIs are defined in RFC 3986. The URI may point to a JSON file that conforms to the "ERC721 Metadata JSON Schema".* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - -*Returns the total amount of tokens stored by the contract. Burned tokens are calculated here, use `_totalMinted()` if you want to count just minted tokens.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 tokenId) external nonpayable -``` - - - -*Transfers `tokenId` token from `from` to `to`. WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible. Requirements: - `from` cannot be the zero address. - `to` cannot be the zero address. - `tokenId` token must be owned by `from`. - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. Emits a {Transfer} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| approved `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed owner, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - - - -## Errors - -### ApprovalCallerNotOwnerNorApproved - -```solidity -error ApprovalCallerNotOwnerNorApproved() -``` - -The caller must own the token or be an approved operator. - - - - -### ApprovalQueryForNonexistentToken - -```solidity -error ApprovalQueryForNonexistentToken() -``` - -The token does not exist. - - - - -### ApprovalToCurrentOwner - -```solidity -error ApprovalToCurrentOwner() -``` - -The caller cannot approve to the current owner. - - - - -### ApproveToCaller - -```solidity -error ApproveToCaller() -``` - -The caller cannot approve to their own address. - - - - -### BalanceQueryForZeroAddress - -```solidity -error BalanceQueryForZeroAddress() -``` - -Cannot query the balance for the zero address. - - - - -### MintToZeroAddress - -```solidity -error MintToZeroAddress() -``` - -Cannot mint to the zero address. - - - - -### MintZeroQuantity - -```solidity -error MintZeroQuantity() -``` - -The quantity of tokens minted must be more than zero. - - - - -### OwnerQueryForNonexistentToken - -```solidity -error OwnerQueryForNonexistentToken() -``` - -The token does not exist. - - - - -### TransferCallerNotOwnerNorApproved - -```solidity -error TransferCallerNotOwnerNorApproved() -``` - -The caller must own the token or be an approved operator. - - - - -### TransferFromIncorrectOwner - -```solidity -error TransferFromIncorrectOwner() -``` - -The token must be owned by `from`. - - - - -### TransferToNonERC721ReceiverImplementer - -```solidity -error TransferToNonERC721ReceiverImplementer() -``` - -Cannot safely transfer to a contract that does not implement the ERC721Receiver interface. - - - - -### TransferToZeroAddress - -```solidity -error TransferToZeroAddress() -``` - -Cannot transfer to the zero address. - - - - -### URIQueryForNonexistentToken - -```solidity -error URIQueryForNonexistentToken() -``` - -The token does not exist. - - - - - diff --git a/docs/IERC721AUpgradeable.md b/docs/IERC721AUpgradeable.md deleted file mode 100644 index 15d4ef44a..000000000 --- a/docs/IERC721AUpgradeable.md +++ /dev/null @@ -1,473 +0,0 @@ -# IERC721AUpgradeable - - - - - - - -*Interface of an ERC721A compliant contract.* - -## Methods - -### approve - -```solidity -function approve(address to, uint256 tokenId) external nonpayable -``` - - - -*Gives permission to `to` to transfer `tokenId` token to another account. The approval is cleared when the token is transferred. Only a single account can be approved at a time, so approving the zero address clears previous approvals. Requirements: - The caller must own the token or be an approved operator. - `tokenId` must exist. Emits an {Approval} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| tokenId | uint256 | undefined - -### balanceOf - -```solidity -function balanceOf(address owner) external view returns (uint256 balance) -``` - - - -*Returns the number of tokens in ``owner``'s account.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| balance | uint256 | undefined - -### getApproved - -```solidity -function getApproved(uint256 tokenId) external view returns (address operator) -``` - - - -*Returns the account approved for `tokenId` token. Requirements: - `tokenId` must exist.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address owner, address operator) external view returns (bool) -``` - - - -*Returns if the `operator` is allowed to manage all of the assets of `owner`. See {setApprovalForAll}* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*Returns the token collection name.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### ownerOf - -```solidity -function ownerOf(uint256 tokenId) external view returns (address owner) -``` - - - -*Returns the owner of the `tokenId` token. Requirements: - `tokenId` must exist.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 tokenId, bytes data) external nonpayable -``` - - - -*Safely transfers `tokenId` token from `from` to `to`. Requirements: - `from` cannot be the zero address. - `to` cannot be the zero address. - `tokenId` token must exist and be owned by `from`. - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. Emits a {Transfer} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined -| data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool _approved) external nonpayable -``` - - - -*Approve or remove `operator` as an operator for the caller. Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller. Requirements: - The `operator` cannot be the caller. Emits an {ApprovalForAll} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| _approved | bool | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - -*Returns the token collection symbol.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### tokenURI - -```solidity -function tokenURI(uint256 tokenId) external view returns (string) -``` - - - -*Returns the Uniform Resource Identifier (URI) for `tokenId` token.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - -*Returns the total amount of tokens stored by the contract. Burned tokens are calculated here, use `_totalMinted()` if you want to count just minted tokens.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 tokenId) external nonpayable -``` - - - -*Transfers `tokenId` token from `from` to `to`. WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible. Requirements: - `from` cannot be the zero address. - `to` cannot be the zero address. - `tokenId` token must be owned by `from`. - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. Emits a {Transfer} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| approved `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed owner, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - - - -## Errors - -### ApprovalCallerNotOwnerNorApproved - -```solidity -error ApprovalCallerNotOwnerNorApproved() -``` - -The caller must own the token or be an approved operator. - - - - -### ApprovalQueryForNonexistentToken - -```solidity -error ApprovalQueryForNonexistentToken() -``` - -The token does not exist. - - - - -### ApprovalToCurrentOwner - -```solidity -error ApprovalToCurrentOwner() -``` - -The caller cannot approve to the current owner. - - - - -### ApproveToCaller - -```solidity -error ApproveToCaller() -``` - -The caller cannot approve to their own address. - - - - -### BalanceQueryForZeroAddress - -```solidity -error BalanceQueryForZeroAddress() -``` - -Cannot query the balance for the zero address. - - - - -### MintToZeroAddress - -```solidity -error MintToZeroAddress() -``` - -Cannot mint to the zero address. - - - - -### MintZeroQuantity - -```solidity -error MintZeroQuantity() -``` - -The quantity of tokens minted must be more than zero. - - - - -### OwnerQueryForNonexistentToken - -```solidity -error OwnerQueryForNonexistentToken() -``` - -The token does not exist. - - - - -### TransferCallerNotOwnerNorApproved - -```solidity -error TransferCallerNotOwnerNorApproved() -``` - -The caller must own the token or be an approved operator. - - - - -### TransferFromIncorrectOwner - -```solidity -error TransferFromIncorrectOwner() -``` - -The token must be owned by `from`. - - - - -### TransferToNonERC721ReceiverImplementer - -```solidity -error TransferToNonERC721ReceiverImplementer() -``` - -Cannot safely transfer to a contract that does not implement the ERC721Receiver interface. - - - - -### TransferToZeroAddress - -```solidity -error TransferToZeroAddress() -``` - -Cannot transfer to the zero address. - - - - -### URIQueryForNonexistentToken - -```solidity -error URIQueryForNonexistentToken() -``` - -The token does not exist. - - - - - diff --git a/docs/IERC721Enumerable.md b/docs/IERC721Enumerable.md deleted file mode 100644 index 6c6e47751..000000000 --- a/docs/IERC721Enumerable.md +++ /dev/null @@ -1,60 +0,0 @@ -# IERC721Enumerable - - - -> ERC-721 Non-Fungible Token Standard, optional enumeration extension - - - -*See https://eips.ethereum.org/EIPS/eip-721 Note: the ERC-165 identifier for this interface is 0x780e9d63.* - -## Methods - -### tokenByIndex - -```solidity -function tokenByIndex(uint256 _index) external view returns (uint256) -``` - -Enumerate valid NFTs - -*Throws if `_index` >= `totalSupply()`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _index | uint256 | A counter less than `totalSupply()` - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | The token identifier for the `_index`th NFT, (sort order not specified) - -### tokenOfOwnerByIndex - -```solidity -function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256) -``` - -Enumerate NFTs assigned to an owner - -*Throws if `_index` >= `balanceOf(_owner)` or if `_owner` is the zero address, representing invalid NFTs.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _owner | address | An address where we are interested in NFTs owned by them -| _index | uint256 | A counter less than `balanceOf(_owner)` - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | The token identifier for the `_index`th NFT assigned to `_owner`, (sort order not specified) - - - - diff --git a/docs/IERC721EnumerableUpgradeable.md b/docs/IERC721EnumerableUpgradeable.md deleted file mode 100644 index af546dc22..000000000 --- a/docs/IERC721EnumerableUpgradeable.md +++ /dev/null @@ -1,316 +0,0 @@ -# IERC721EnumerableUpgradeable - - - -> ERC-721 Non-Fungible Token Standard, optional enumeration extension - - - -*See https://eips.ethereum.org/EIPS/eip-721* - -## Methods - -### approve - -```solidity -function approve(address to, uint256 tokenId) external nonpayable -``` - - - -*Gives permission to `to` to transfer `tokenId` token to another account. The approval is cleared when the token is transferred. Only a single account can be approved at a time, so approving the zero address clears previous approvals. Requirements: - The caller must own the token or be an approved operator. - `tokenId` must exist. Emits an {Approval} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| tokenId | uint256 | undefined - -### balanceOf - -```solidity -function balanceOf(address owner) external view returns (uint256 balance) -``` - - - -*Returns the number of tokens in ``owner``'s account.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| balance | uint256 | undefined - -### getApproved - -```solidity -function getApproved(uint256 tokenId) external view returns (address operator) -``` - - - -*Returns the account approved for `tokenId` token. Requirements: - `tokenId` must exist.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address owner, address operator) external view returns (bool) -``` - - - -*Returns if the `operator` is allowed to manage all of the assets of `owner`. See {setApprovalForAll}* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### ownerOf - -```solidity -function ownerOf(uint256 tokenId) external view returns (address owner) -``` - - - -*Returns the owner of the `tokenId` token. Requirements: - `tokenId` must exist.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 tokenId, bytes data) external nonpayable -``` - - - -*Safely transfers `tokenId` token from `from` to `to`. Requirements: - `from` cannot be the zero address. - `to` cannot be the zero address. - `tokenId` token must exist and be owned by `from`. - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. Emits a {Transfer} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined -| data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool _approved) external nonpayable -``` - - - -*Approve or remove `operator` as an operator for the caller. Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller. Requirements: - The `operator` cannot be the caller. Emits an {ApprovalForAll} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| _approved | bool | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### tokenByIndex - -```solidity -function tokenByIndex(uint256 index) external view returns (uint256) -``` - - - -*Returns a token ID at a given `index` of all the tokens stored by the contract. Use along with {totalSupply} to enumerate all tokens.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### tokenOfOwnerByIndex - -```solidity -function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256) -``` - - - -*Returns a token ID owned by `owner` at a given `index` of its token list. Use along with {balanceOf} to enumerate all of ``owner``'s tokens.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - -*Returns the total amount of tokens stored by the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 tokenId) external nonpayable -``` - - - -*Transfers `tokenId` token from `from` to `to`. WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible. Requirements: - `from` cannot be the zero address. - `to` cannot be the zero address. - `tokenId` token must be owned by `from`. - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. Emits a {Transfer} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| approved `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed owner, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - - - diff --git a/docs/IERC721Metadata.md b/docs/IERC721Metadata.md deleted file mode 100644 index cd2ebf355..000000000 --- a/docs/IERC721Metadata.md +++ /dev/null @@ -1,71 +0,0 @@ -# IERC721Metadata - - - -> ERC-721 Non-Fungible Token Standard, optional metadata extension - - - -*See https://eips.ethereum.org/EIPS/eip-721 Note: the ERC-165 identifier for this interface is 0x5b5e139f.* - -## Methods - -### name - -```solidity -function name() external view returns (string) -``` - -A descriptive name for a collection of NFTs in this contract - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - -An abbreviated name for NFTs in this contract - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### tokenURI - -```solidity -function tokenURI(uint256 _tokenId) external view returns (string) -``` - -A distinct Uniform Resource Identifier (URI) for a given asset. - -*Throws if `_tokenId` is not a valid NFT. URIs are defined in RFC 3986. The URI may point to a JSON file that conforms to the "ERC721 Metadata JSON Schema".* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - - - - diff --git a/docs/IERC721MetadataUpgradeable.md b/docs/IERC721MetadataUpgradeable.md deleted file mode 100644 index 4837a51d8..000000000 --- a/docs/IERC721MetadataUpgradeable.md +++ /dev/null @@ -1,310 +0,0 @@ -# IERC721MetadataUpgradeable - - - -> ERC-721 Non-Fungible Token Standard, optional metadata extension - - - -*See https://eips.ethereum.org/EIPS/eip-721* - -## Methods - -### approve - -```solidity -function approve(address to, uint256 tokenId) external nonpayable -``` - - - -*Gives permission to `to` to transfer `tokenId` token to another account. The approval is cleared when the token is transferred. Only a single account can be approved at a time, so approving the zero address clears previous approvals. Requirements: - The caller must own the token or be an approved operator. - `tokenId` must exist. Emits an {Approval} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| tokenId | uint256 | undefined - -### balanceOf - -```solidity -function balanceOf(address owner) external view returns (uint256 balance) -``` - - - -*Returns the number of tokens in ``owner``'s account.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| balance | uint256 | undefined - -### getApproved - -```solidity -function getApproved(uint256 tokenId) external view returns (address operator) -``` - - - -*Returns the account approved for `tokenId` token. Requirements: - `tokenId` must exist.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address owner, address operator) external view returns (bool) -``` - - - -*Returns if the `operator` is allowed to manage all of the assets of `owner`. See {setApprovalForAll}* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*Returns the token collection name.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### ownerOf - -```solidity -function ownerOf(uint256 tokenId) external view returns (address owner) -``` - - - -*Returns the owner of the `tokenId` token. Requirements: - `tokenId` must exist.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 tokenId, bytes data) external nonpayable -``` - - - -*Safely transfers `tokenId` token from `from` to `to`. Requirements: - `from` cannot be the zero address. - `to` cannot be the zero address. - `tokenId` token must exist and be owned by `from`. - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. Emits a {Transfer} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined -| data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool _approved) external nonpayable -``` - - - -*Approve or remove `operator` as an operator for the caller. Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller. Requirements: - The `operator` cannot be the caller. Emits an {ApprovalForAll} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| _approved | bool | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - -*Returns the token collection symbol.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### tokenURI - -```solidity -function tokenURI(uint256 tokenId) external view returns (string) -``` - - - -*Returns the Uniform Resource Identifier (URI) for `tokenId` token.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 tokenId) external nonpayable -``` - - - -*Transfers `tokenId` token from `from` to `to`. WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible. Requirements: - `from` cannot be the zero address. - `to` cannot be the zero address. - `tokenId` token must be owned by `from`. - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. Emits a {Transfer} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| approved `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed owner, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - - - diff --git a/docs/IERC721Receiver.md b/docs/IERC721Receiver.md deleted file mode 100644 index bdf02e6b4..000000000 --- a/docs/IERC721Receiver.md +++ /dev/null @@ -1,40 +0,0 @@ -# IERC721Receiver - - - -> ERC721 token receiver interface - - - -*Interface for any contract that wants to support safeTransfers from ERC721 asset contracts.* - -## Methods - -### onERC721Received - -```solidity -function onERC721Received(address operator, address from, uint256 tokenId, bytes data) external nonpayable returns (bytes4) -``` - - - -*Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom} by `operator` from `from`, this function is called. It must return its Solidity selector to confirm the token transfer. If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted. The selector can be obtained in Solidity with `IERC721.onERC721Received.selector`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| from | address | undefined -| tokenId | uint256 | undefined -| data | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - - - - diff --git a/docs/IERC721ReceiverUpgradeable.md b/docs/IERC721ReceiverUpgradeable.md deleted file mode 100644 index 54b165a6b..000000000 --- a/docs/IERC721ReceiverUpgradeable.md +++ /dev/null @@ -1,40 +0,0 @@ -# IERC721ReceiverUpgradeable - - - -> ERC721 token receiver interface - - - -*Interface for any contract that wants to support safeTransfers from ERC721 asset contracts.* - -## Methods - -### onERC721Received - -```solidity -function onERC721Received(address operator, address from, uint256 tokenId, bytes data) external nonpayable returns (bytes4) -``` - - - -*Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom} by `operator` from `from`, this function is called. It must return its Solidity selector to confirm the token transfer. If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted. The selector can be obtained in Solidity with `IERC721.onERC721Received.selector`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| from | address | undefined -| tokenId | uint256 | undefined -| data | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - - - - diff --git a/docs/IERC721Supply.md b/docs/IERC721Supply.md deleted file mode 100644 index 7a893bff4..000000000 --- a/docs/IERC721Supply.md +++ /dev/null @@ -1,32 +0,0 @@ -# IERC721Supply - - - -> ERC-721 Non-Fungible Token Standard, optional supplu extension - - - -*See https://eips.ethereum.org/EIPS/eip-721 Note: the ERC-165 identifier for this interface is 0x780e9d63.* - -## Methods - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - -Count NFTs tracked by this contract - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | A count of valid NFTs tracked by this contract, where each one of them has an assigned and queryable owner not equal to the zero address - - - - diff --git a/docs/IERC721Upgradeable.md b/docs/IERC721Upgradeable.md deleted file mode 100644 index dec636351..000000000 --- a/docs/IERC721Upgradeable.md +++ /dev/null @@ -1,254 +0,0 @@ -# IERC721Upgradeable - - - - - - - -*Required interface of an ERC721 compliant contract.* - -## Methods - -### approve - -```solidity -function approve(address to, uint256 tokenId) external nonpayable -``` - - - -*Gives permission to `to` to transfer `tokenId` token to another account. The approval is cleared when the token is transferred. Only a single account can be approved at a time, so approving the zero address clears previous approvals. Requirements: - The caller must own the token or be an approved operator. - `tokenId` must exist. Emits an {Approval} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| tokenId | uint256 | undefined - -### balanceOf - -```solidity -function balanceOf(address owner) external view returns (uint256 balance) -``` - - - -*Returns the number of tokens in ``owner``'s account.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| balance | uint256 | undefined - -### getApproved - -```solidity -function getApproved(uint256 tokenId) external view returns (address operator) -``` - - - -*Returns the account approved for `tokenId` token. Requirements: - `tokenId` must exist.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address owner, address operator) external view returns (bool) -``` - - - -*Returns if the `operator` is allowed to manage all of the assets of `owner`. See {setApprovalForAll}* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### ownerOf - -```solidity -function ownerOf(uint256 tokenId) external view returns (address owner) -``` - - - -*Returns the owner of the `tokenId` token. Requirements: - `tokenId` must exist.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 tokenId, bytes data) external nonpayable -``` - - - -*Safely transfers `tokenId` token from `from` to `to`. Requirements: - `from` cannot be the zero address. - `to` cannot be the zero address. - `tokenId` token must exist and be owned by `from`. - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. Emits a {Transfer} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined -| data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool _approved) external nonpayable -``` - - - -*Approve or remove `operator` as an operator for the caller. Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller. Requirements: - The `operator` cannot be the caller. Emits an {ApprovalForAll} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| _approved | bool | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 tokenId) external nonpayable -``` - - - -*Transfers `tokenId` token from `from` to `to`. WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible. Requirements: - `from` cannot be the zero address. - `to` cannot be the zero address. - `tokenId` token must be owned by `from`. - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. Emits a {Transfer} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) -``` - - - -*Emitted when `owner` enables `approved` to manage the `tokenId` token.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| approved `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed owner, address indexed operator, bool approved) -``` - - - -*Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) -``` - - - -*Emitted when `tokenId` token is transferred from `from` to `to`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - - - diff --git a/docs/IFeeTierPlacementExtension.md b/docs/IFeeTierPlacementExtension.md deleted file mode 100644 index 94230ed3e..000000000 --- a/docs/IFeeTierPlacementExtension.md +++ /dev/null @@ -1,39 +0,0 @@ -# IFeeTierPlacementExtension - - - - - - - - - -## Methods - -### getFeeTier - -```solidity -function getFeeTier(address deployer, address proxy) external view returns (uint128 tierId, uint128 validUntilTimestamp) -``` - - - -*Returns the fee tier for a given proxy contract address and proxy deployer address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| deployer | address | undefined -| proxy | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| tierId | uint128 | undefined -| validUntilTimestamp | uint128 | undefined - - - - diff --git a/docs/IGovernorUpgradeable.md b/docs/IGovernorUpgradeable.md deleted file mode 100644 index 124df055d..000000000 --- a/docs/IGovernorUpgradeable.md +++ /dev/null @@ -1,483 +0,0 @@ -# IGovernorUpgradeable - - - - - - - -*Interface of the {Governor} core. _Available since v4.3._* - -## Methods - -### COUNTING_MODE - -```solidity -function COUNTING_MODE() external pure returns (string) -``` - -module:voting - -*A description of the possible `support` values for {castVote} and the way these votes are counted, meant to be consumed by UIs to show correct vote options and interpret the results. The string is a URL-encoded sequence of key-value pairs that each describe one aspect, for example `support=bravo&quorum=for,abstain`. There are 2 standard keys: `support` and `quorum`. - `support=bravo` refers to the vote options 0 = Against, 1 = For, 2 = Abstain, as in `GovernorBravo`. - `quorum=bravo` means that only For votes are counted towards quorum. - `quorum=for,abstain` means that both For and Abstain votes are counted towards quorum. NOTE: The string can be decoded by the standard https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams[`URLSearchParams`] JavaScript class.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### castVote - -```solidity -function castVote(uint256 proposalId, uint8 support) external nonpayable returns (uint256 balance) -``` - - - -*Cast a vote Emits a {VoteCast} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| support | uint8 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| balance | uint256 | undefined - -### castVoteBySig - -```solidity -function castVoteBySig(uint256 proposalId, uint8 support, uint8 v, bytes32 r, bytes32 s) external nonpayable returns (uint256 balance) -``` - - - -*Cast a vote using the user cryptographic signature. Emits a {VoteCast} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| support | uint8 | undefined -| v | uint8 | undefined -| r | bytes32 | undefined -| s | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| balance | uint256 | undefined - -### castVoteWithReason - -```solidity -function castVoteWithReason(uint256 proposalId, uint8 support, string reason) external nonpayable returns (uint256 balance) -``` - - - -*Cast a vote with a reason Emits a {VoteCast} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| support | uint8 | undefined -| reason | string | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| balance | uint256 | undefined - -### execute - -```solidity -function execute(address[] targets, uint256[] values, bytes[] calldatas, bytes32 descriptionHash) external payable returns (uint256 proposalId) -``` - - - -*Execute a successful proposal. This requires the quorum to be reached, the vote to be successful, and the deadline to be reached. Emits a {ProposalExecuted} event. Note: some module can modify the requirements for execution, for example by adding an additional timelock.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| targets | address[] | undefined -| values | uint256[] | undefined -| calldatas | bytes[] | undefined -| descriptionHash | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -### getVotes - -```solidity -function getVotes(address account, uint256 blockNumber) external view returns (uint256) -``` - -module:reputation - -*Voting power of an `account` at a specific `blockNumber`. Note: this can be implemented in a number of ways, for example by reading the delegated balance from one (or multiple), {ERC20Votes} tokens.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| blockNumber | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### hasVoted - -```solidity -function hasVoted(uint256 proposalId, address account) external view returns (bool) -``` - -module:voting - -*Returns weither `account` has cast a vote on `proposalId`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### hashProposal - -```solidity -function hashProposal(address[] targets, uint256[] values, bytes[] calldatas, bytes32 descriptionHash) external pure returns (uint256) -``` - -module:core - -*Hashing function used to (re)build the proposal id from the proposal details..* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| targets | address[] | undefined -| values | uint256[] | undefined -| calldatas | bytes[] | undefined -| descriptionHash | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### name - -```solidity -function name() external view returns (string) -``` - -module:core - -*Name of the governor instance (used in building the ERC712 domain separator).* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### proposalDeadline - -```solidity -function proposalDeadline(uint256 proposalId) external view returns (uint256) -``` - -module:core - -*Block number at which votes close. Votes close at the end of this block, so it is possible to cast a vote during this block.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### proposalSnapshot - -```solidity -function proposalSnapshot(uint256 proposalId) external view returns (uint256) -``` - -module:core - -*Block number used to retrieve user's votes and quorum. As per Compound's Comp and OpenZeppelin's ERC20Votes, the snapshot is performed at the end of this block. Hence, voting for this proposal starts at the beginning of the following block.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### propose - -```solidity -function propose(address[] targets, uint256[] values, bytes[] calldatas, string description) external nonpayable returns (uint256 proposalId) -``` - - - -*Create a new proposal. Vote start {IGovernor-votingDelay} blocks after the proposal is created and ends {IGovernor-votingPeriod} blocks after the voting starts. Emits a {ProposalCreated} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| targets | address[] | undefined -| values | uint256[] | undefined -| calldatas | bytes[] | undefined -| description | string | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -### quorum - -```solidity -function quorum(uint256 blockNumber) external view returns (uint256) -``` - -module:user-config - -*Minimum number of cast voted required for a proposal to be successful. Note: The `blockNumber` parameter corresponds to the snaphot used for counting vote. This allows to scale the quroum depending on values such as the totalSupply of a token at this block (see {ERC20Votes}).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| blockNumber | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### state - -```solidity -function state(uint256 proposalId) external view returns (enum IGovernorUpgradeable.ProposalState) -``` - -module:core - -*Current state of a proposal, following Compound's convention* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | enum IGovernorUpgradeable.ProposalState | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### version - -```solidity -function version() external view returns (string) -``` - -module:core - -*Version of the governor instance (used in building the ERC712 domain separator). Default: "1"* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### votingDelay - -```solidity -function votingDelay() external view returns (uint256) -``` - -module:user-config - -*Delay, in number of block, between the proposal is created and the vote starts. This can be increassed to leave time for users to buy voting power, of delegate it, before the voting of a proposal starts.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### votingPeriod - -```solidity -function votingPeriod() external view returns (uint256) -``` - -module:user-config - -*Delay, in number of blocks, between the vote start and vote ends. NOTE: The {votingDelay} can delay the start of the vote. This must be considered when setting the voting duration compared to the voting delay.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - - - -## Events - -### ProposalCanceled - -```solidity -event ProposalCanceled(uint256 proposalId) -``` - - - -*Emitted when a proposal is canceled.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined | - -### ProposalCreated - -```solidity -event ProposalCreated(uint256 proposalId, address proposer, address[] targets, uint256[] values, string[] signatures, bytes[] calldatas, uint256 startBlock, uint256 endBlock, string description) -``` - - - -*Emitted when a proposal is created.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined | -| proposer | address | undefined | -| targets | address[] | undefined | -| values | uint256[] | undefined | -| signatures | string[] | undefined | -| calldatas | bytes[] | undefined | -| startBlock | uint256 | undefined | -| endBlock | uint256 | undefined | -| description | string | undefined | - -### ProposalExecuted - -```solidity -event ProposalExecuted(uint256 proposalId) -``` - - - -*Emitted when a proposal is executed.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined | - -### VoteCast - -```solidity -event VoteCast(address indexed voter, uint256 proposalId, uint8 support, uint256 weight, string reason) -``` - - - -*Emitted when a vote is cast. Note: `support` values should be seen as buckets. There interpretation depends on the voting module used.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| voter `indexed` | address | undefined | -| proposalId | uint256 | undefined | -| support | uint8 | undefined | -| weight | uint256 | undefined | -| reason | string | undefined | - - - diff --git a/docs/ILazyMint.md b/docs/ILazyMint.md deleted file mode 100644 index e30aa062a..000000000 --- a/docs/ILazyMint.md +++ /dev/null @@ -1,61 +0,0 @@ -# ILazyMint - - - - - -Thirdweb's `LazyMint` is a contract extension for any base NFT contract. It lets you 'lazy mint' any number of NFTs at once. Here, 'lazy mint' means defining the metadata for particular tokenIds of your NFT contract, without actually minting a non-zero balance of NFTs of those tokenIds. - - - -## Methods - -### lazyMint - -```solidity -function lazyMint(uint256 amount, string baseURIForTokens, bytes extraData) external nonpayable returns (uint256 batchId) -``` - -Lazy mints a given amount of NFTs. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| amount | uint256 | The number of NFTs to lazy mint. -| baseURIForTokens | string | The base URI for the 'n' number of NFTs being lazy minted, where the metadata for each of those NFTs is `${baseURIForTokens}/${tokenId}`. -| extraData | bytes | Additional bytes data to be used at the discretion of the consumer of the contract. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| batchId | uint256 | A unique integer identifier for the batch of NFTs lazy minted together. - - - -## Events - -### TokensLazyMinted - -```solidity -event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI) -``` - - - -*Emitted when tokens are lazy minted.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| startTokenId `indexed` | uint256 | undefined | -| endTokenId | uint256 | undefined | -| baseURI | string | undefined | -| encryptedBaseURI | bytes | undefined | - - - diff --git a/docs/IMarketplace.md b/docs/IMarketplace.md deleted file mode 100644 index d5c535caf..000000000 --- a/docs/IMarketplace.md +++ /dev/null @@ -1,399 +0,0 @@ -# IMarketplace - - - - - - - - - -## Methods - -### acceptOffer - -```solidity -function acceptOffer(uint256 _listingId, address _offeror, address _currency, uint256 _totalPrice) external nonpayable -``` - -Lets a listing's creator accept an offer to their direct listing. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _listingId | uint256 | The unique ID of the listing for which to accept the offer. -| _offeror | address | The address of the buyer whose offer is to be accepted. -| _currency | address | The currency of the offer that is to be accepted. -| _totalPrice | uint256 | The total price of the offer that is to be accepted. - -### buy - -```solidity -function buy(uint256 _listingId, address _buyFor, uint256 _quantity, address _currency, uint256 _totalPrice) external payable -``` - -Lets someone buy a given quantity of tokens from a direct listing by paying the fixed price. - -*A sale will fail to execute if either: (1) buyer does not own or has not approved Marketplace to transfer the appropriate amount of currency (or hasn't sent the appropriate amount of native tokens) (2) the lister does not own or has removed Markeplace's approval to transfer the tokens listed for sale.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _listingId | uint256 | The uid of the direct lisitng to buy from. -| _buyFor | address | The receiver of the NFT being bought. -| _quantity | uint256 | The amount of NFTs to buy from the direct listing. -| _currency | address | The currency to pay the price in. -| _totalPrice | uint256 | The total price to pay for the tokens being bought. - -### cancelDirectListing - -```solidity -function cancelDirectListing(uint256 _listingId) external nonpayable -``` - -Lets a direct listing creator cancel their listing. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _listingId | uint256 | The unique Id of the lisitng to cancel. - -### closeAuction - -```solidity -function closeAuction(uint256 _listingId, address _closeFor) external nonpayable -``` - -Lets any account close an auction on behalf of either the (1) auction's creator, or (2) winning bidder. For (1): The auction creator is sent the the winning bid amount. For (2): The winning bidder is sent the auctioned NFTs. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _listingId | uint256 | The uid of the listing (the auction to close). -| _closeFor | address | For whom the auction is being closed - the auction creator or winning bidder. - -### contractType - -```solidity -function contractType() external pure returns (bytes32) -``` - - - -*Returns the module type of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### contractURI - -```solidity -function contractURI() external view returns (string) -``` - - - -*Returns the metadata URI of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### contractVersion - -```solidity -function contractVersion() external pure returns (uint8) -``` - - - -*Returns the version of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint8 | undefined - -### createListing - -```solidity -function createListing(IMarketplace.ListingParameters _params) external nonpayable -``` - -Lets a token owner list tokens (ERC 721 or ERC 1155) for sale in a direct listing, or an auction. - -*NFTs to list for sale in an auction are escrowed in Marketplace. For direct listings, the contract only checks whether the listing's creator owns and has approved Marketplace to transfer the NFTs to list.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _params | IMarketplace.ListingParameters | The parameters that govern the listing to be created. - -### getPlatformFeeInfo - -```solidity -function getPlatformFeeInfo() external view returns (address, uint16) -``` - - - -*Returns the platform fee bps and recipient.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### offer - -```solidity -function offer(uint256 _listingId, uint256 _quantityWanted, address _currency, uint256 _pricePerToken, uint256 _expirationTimestamp) external payable -``` - -Lets someone make an offer to a direct listing, or bid in an auction. - -*Each (address, listing ID) pair maps to a single unique offer. So e.g. if a buyer makes makes two offers to the same direct listing, the last offer is counted as the buyer's offer to that listing.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _listingId | uint256 | The unique ID of the lisitng to make an offer/bid to. -| _quantityWanted | uint256 | For auction listings: the 'quantity wanted' is the total amount of NFTs being auctioned, regardless of the value of `_quantityWanted` passed. For direct listings: `_quantityWanted` is the quantity of NFTs from the listing, for which the offer is being made. -| _currency | address | For auction listings: the 'currency of the bid' is the currency accepted by the auction, regardless of the value of `_currency` passed. For direct listings: this is the currency in which the offer is made. -| _pricePerToken | uint256 | For direct listings: offered price per token. For auction listings: the bid amount per token. The total offer/bid amount is `_quantityWanted * _pricePerToken`. -| _expirationTimestamp | uint256 | For aution listings: inapplicable. For direct listings: The timestamp after which the seller can no longer accept the offer. - -### setContractURI - -```solidity -function setContractURI(string _uri) external nonpayable -``` - - - -*Sets contract URI for the storefront-level metadata of the contract. Only module admin can call this function.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _uri | string | undefined - -### setPlatformFeeInfo - -```solidity -function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) external nonpayable -``` - - - -*Lets a module admin update the fees on primary sales.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _platformFeeRecipient | address | undefined -| _platformFeeBps | uint256 | undefined - -### updateListing - -```solidity -function updateListing(uint256 _listingId, uint256 _quantityToList, uint256 _reservePricePerToken, uint256 _buyoutPricePerToken, address _currencyToAccept, uint256 _startTime, uint256 _secondsUntilEndTime) external nonpayable -``` - -Lets a listing's creator edit the listing's parameters. A direct listing can be edited whenever. An auction listing cannot be edited after the auction has started. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _listingId | uint256 | The uid of the lisitng to edit. -| _quantityToList | uint256 | The amount of NFTs to list for sale in the listing. For direct lisitngs, the contract only checks whether the listing creator owns and has approved Marketplace to transfer `_quantityToList` amount of NFTs to list for sale. For auction listings, the contract ensures that exactly `_quantityToList` amount of NFTs to list are escrowed. -| _reservePricePerToken | uint256 | For direct listings: this value is ignored. For auctions: the minimum bid amount of the auction is `reservePricePerToken * quantityToList` -| _buyoutPricePerToken | uint256 | For direct listings: interpreted as 'price per token' listed. For auctions: if `buyoutPricePerToken` is greater than 0, and a bidder's bid is at least as great as `buyoutPricePerToken * quantityToList`, the bidder wins the auction, and the auction is closed. -| _currencyToAccept | address | For direct listings: the currency in which a buyer must pay the listing's fixed price to buy the NFT(s). For auctions: the currency in which the bidders must make bids. -| _startTime | uint256 | The unix timestamp after which listing is active. For direct listings: 'active' means NFTs can be bought from the listing. For auctions, 'active' means bids can be made in the auction. -| _secondsUntilEndTime | uint256 | No. of seconds after the provided `_startTime`, after which the listing is inactive. For direct listings: 'inactive' means NFTs cannot be bought from the listing. For auctions: 'inactive' means bids can no longer be made in the auction. - - - -## Events - -### AuctionBuffersUpdated - -```solidity -event AuctionBuffersUpdated(uint256 timeBuffer, uint256 bidBufferBps) -``` - - - -*Emitted when auction buffers are updated.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| timeBuffer | uint256 | undefined | -| bidBufferBps | uint256 | undefined | - -### AuctionClosed - -```solidity -event AuctionClosed(uint256 indexed listingId, address indexed closer, bool indexed cancelled, address auctionCreator, address winningBidder) -``` - - - -*Emitted when an auction is closed.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| listingId `indexed` | uint256 | undefined | -| closer `indexed` | address | undefined | -| cancelled `indexed` | bool | undefined | -| auctionCreator | address | undefined | -| winningBidder | address | undefined | - -### ListingAdded - -```solidity -event ListingAdded(uint256 indexed listingId, address indexed assetContract, address indexed lister, IMarketplace.Listing listing) -``` - - - -*Emitted when a new listing is created.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| listingId `indexed` | uint256 | undefined | -| assetContract `indexed` | address | undefined | -| lister `indexed` | address | undefined | -| listing | IMarketplace.Listing | undefined | - -### ListingRemoved - -```solidity -event ListingRemoved(uint256 indexed listingId, address indexed listingCreator) -``` - - - -*Emitted when a listing is cancelled.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| listingId `indexed` | uint256 | undefined | -| listingCreator `indexed` | address | undefined | - -### ListingUpdated - -```solidity -event ListingUpdated(uint256 indexed listingId, address indexed listingCreator) -``` - - - -*Emitted when the parameters of a listing are updated.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| listingId `indexed` | uint256 | undefined | -| listingCreator `indexed` | address | undefined | - -### NewOffer - -```solidity -event NewOffer(uint256 indexed listingId, address indexed offeror, enum IMarketplace.ListingType indexed listingType, uint256 quantityWanted, uint256 totalOfferAmount, address currency) -``` - - - -*Emitted when (1) a new offer is made to a direct listing, or (2) when a new bid is made in an auction.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| listingId `indexed` | uint256 | undefined | -| offeror `indexed` | address | undefined | -| listingType `indexed` | enum IMarketplace.ListingType | undefined | -| quantityWanted | uint256 | undefined | -| totalOfferAmount | uint256 | undefined | -| currency | address | undefined | - -### NewSale - -```solidity -event NewSale(uint256 indexed listingId, address indexed assetContract, address indexed lister, address buyer, uint256 quantityBought, uint256 totalPricePaid) -``` - - - -*Emitted when a buyer buys from a direct listing, or a lister accepts some buyer's offer to their direct listing.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| listingId `indexed` | uint256 | undefined | -| assetContract `indexed` | address | undefined | -| lister `indexed` | address | undefined | -| buyer | address | undefined | -| quantityBought | uint256 | undefined | -| totalPricePaid | uint256 | undefined | - -### PlatformFeeInfoUpdated - -```solidity -event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| platformFeeRecipient `indexed` | address | undefined | -| platformFeeBps | uint256 | undefined | - - - diff --git a/docs/IMintableERC1155.md b/docs/IMintableERC1155.md deleted file mode 100644 index 8a48d04ac..000000000 --- a/docs/IMintableERC1155.md +++ /dev/null @@ -1,56 +0,0 @@ -# IMintableERC1155 - - - - - -`SignatureMint1155` is an ERC 1155 contract. It lets anyone mint NFTs by producing a mint request and a signature (produced by an account with MINTER_ROLE, signing the mint request). - - - -## Methods - -### mintTo - -```solidity -function mintTo(address to, uint256 tokenId, string uri, uint256 amount) external nonpayable -``` - -Lets an account with MINTER_ROLE mint an NFT. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | The address to mint the NFT to. -| tokenId | uint256 | The tokenId of the NFTs to mint -| uri | string | The URI to assign to the NFT. -| amount | uint256 | The number of copies of the NFT to mint. - - - -## Events - -### TokensMinted - -```solidity -event TokensMinted(address indexed mintedTo, uint256 indexed tokenIdMinted, string uri, uint256 quantityMinted) -``` - - - -*Emitted when an account with MINTER_ROLE mints an NFT.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| mintedTo `indexed` | address | undefined | -| tokenIdMinted `indexed` | uint256 | undefined | -| uri | string | undefined | -| quantityMinted | uint256 | undefined | - - - diff --git a/docs/IMintableERC20.md b/docs/IMintableERC20.md deleted file mode 100644 index 67d30a1be..000000000 --- a/docs/IMintableERC20.md +++ /dev/null @@ -1,52 +0,0 @@ -# IMintableERC20 - - - - - - - - - -## Methods - -### mintTo - -```solidity -function mintTo(address to, uint256 amount) external nonpayable -``` - - - -*Creates `amount` new tokens for `to`. See {ERC20-_mint}. Requirements: - the caller must have the `MINTER_ROLE`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| amount | uint256 | undefined - - - -## Events - -### TokensMinted - -```solidity -event TokensMinted(address indexed mintedTo, uint256 quantityMinted) -``` - - - -*Emitted when tokens are minted with `mintTo`* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| mintedTo `indexed` | address | undefined | -| quantityMinted | uint256 | undefined | - - - diff --git a/docs/IMintableERC721.md b/docs/IMintableERC721.md deleted file mode 100644 index 98918afe2..000000000 --- a/docs/IMintableERC721.md +++ /dev/null @@ -1,59 +0,0 @@ -# IMintableERC721 - - - - - - - - - -## Methods - -### mintTo - -```solidity -function mintTo(address to, string uri) external nonpayable returns (uint256) -``` - -Lets an account mint an NFT. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | The address to mint the NFT to. -| uri | string | The URI to assign to the NFT. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | tokenId of the NFT minted. - - - -## Events - -### TokensMinted - -```solidity -event TokensMinted(address indexed mintedTo, uint256 indexed tokenIdMinted, string uri) -``` - - - -*Emitted when tokens are minted via `mintTo`* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| mintedTo `indexed` | address | undefined | -| tokenIdMinted `indexed` | uint256 | undefined | -| uri | string | undefined | - - - diff --git a/docs/IMulticall.md b/docs/IMulticall.md deleted file mode 100644 index 2c04fca1d..000000000 --- a/docs/IMulticall.md +++ /dev/null @@ -1,37 +0,0 @@ -# IMulticall - - - - - - - -*Provides a function to batch together multiple calls in a single external call. _Available since v4.1._* - -## Methods - -### multicall - -```solidity -function multicall(bytes[] data) external nonpayable returns (bytes[] results) -``` - - - -*Receives and executes a batch of function calls on this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| results | bytes[] | undefined - - - - diff --git a/docs/IMultiwrap.md b/docs/IMultiwrap.md deleted file mode 100644 index e640e4d16..000000000 --- a/docs/IMultiwrap.md +++ /dev/null @@ -1,96 +0,0 @@ -# IMultiwrap - - - - - -Thirdweb's Multiwrap contract lets you wrap arbitrary ERC20, ERC721 and ERC1155 tokens you own into a single wrapped token / NFT. A wrapped NFT can be unwrapped i.e. burned in exchange for its underlying contents. - - - -## Methods - -### unwrap - -```solidity -function unwrap(uint256 tokenId, address recipient) external nonpayable -``` - -Unwrap a wrapped NFT to retrieve underlying ERC1155, ERC721, ERC20 tokens. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | The token Id of the wrapped NFT to unwrap. -| recipient | address | The recipient of the underlying ERC1155, ERC721, ERC20 tokens of the wrapped NFT. - -### wrap - -```solidity -function wrap(ITokenBundle.Token[] wrappedContents, string uriForWrappedToken, address recipient) external payable returns (uint256 tokenId) -``` - -Wrap multiple ERC1155, ERC721, ERC20 tokens into a single wrapped NFT. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| wrappedContents | ITokenBundle.Token[] | The tokens to wrap. -| uriForWrappedToken | string | The metadata URI for the wrapped NFT. -| recipient | address | The recipient of the wrapped NFT. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - - - -## Events - -### TokensUnwrapped - -```solidity -event TokensUnwrapped(address indexed unwrapper, address indexed recipientOfWrappedContents, uint256 indexed tokenIdOfWrappedToken) -``` - - - -*Emitted when tokens are unwrapped.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| unwrapper `indexed` | address | undefined | -| recipientOfWrappedContents `indexed` | address | undefined | -| tokenIdOfWrappedToken `indexed` | uint256 | undefined | - -### TokensWrapped - -```solidity -event TokensWrapped(address indexed wrapper, address indexed recipientOfWrappedToken, uint256 indexed tokenIdOfWrappedToken, ITokenBundle.Token[] wrappedContents) -``` - - - -*Emitted when tokens are wrapped.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| wrapper `indexed` | address | undefined | -| recipientOfWrappedToken `indexed` | address | undefined | -| tokenIdOfWrappedToken `indexed` | uint256 | undefined | -| wrappedContents | ITokenBundle.Token[] | undefined | - - - diff --git a/docs/IOwnable.md b/docs/IOwnable.md deleted file mode 100644 index aa21d97c2..000000000 --- a/docs/IOwnable.md +++ /dev/null @@ -1,68 +0,0 @@ -# IOwnable - - - - - -Thirdweb's `Ownable` is a contract extension to be used with any base contract. It exposes functions for setting and reading who the 'owner' of the inheriting smart contract is, and lets the inheriting contract perform conditional logic that uses information about who the contract's owner is. - - - -## Methods - -### owner - -```solidity -function owner() external view returns (address) -``` - - - -*Returns the owner of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### setOwner - -```solidity -function setOwner(address _newOwner) external nonpayable -``` - - - -*Lets a module admin set a new owner for the contract. The new owner must be a module admin.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _newOwner | address | undefined - - - -## Events - -### OwnerUpdated - -```solidity -event OwnerUpdated(address indexed prevOwner, address indexed newOwner) -``` - - - -*Emitted when a new Owner is set.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| prevOwner `indexed` | address | undefined | -| newOwner `indexed` | address | undefined | - - - diff --git a/docs/IPack.md b/docs/IPack.md deleted file mode 100644 index 32df02d77..000000000 --- a/docs/IPack.md +++ /dev/null @@ -1,107 +0,0 @@ -# IPack - - - - - -The thirdweb `Pack` contract is a lootbox mechanism. An account can bundle up arbitrary ERC20, ERC721 and ERC1155 tokens into a set of packs. A pack can then be opened in return for a selection of the tokens in the pack. The selection of tokens distributed on opening a pack depends on the relative supply of all tokens in the packs. - - - -## Methods - -### createPack - -```solidity -function createPack(ITokenBundle.Token[] contents, uint256[] numOfRewardUnits, string packUri, uint128 openStartTimestamp, uint128 amountDistributedPerOpen, address recipient) external payable returns (uint256 packId, uint256 packTotalSupply) -``` - -Creates a pack with the stated contents. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| contents | ITokenBundle.Token[] | The reward units to pack in the packs. -| numOfRewardUnits | uint256[] | The number of reward units to create, for each asset specified in `contents`. -| packUri | string | The (metadata) URI assigned to the packs created. -| openStartTimestamp | uint128 | The timestamp after which packs can be opened. -| amountDistributedPerOpen | uint128 | The number of reward units distributed per open. -| recipient | address | The recipient of the packs created. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| packId | uint256 | The unique identifer of the created set of packs. -| packTotalSupply | uint256 | The total number of packs created. - -### openPack - -```solidity -function openPack(uint256 packId, uint256 amountToOpen) external nonpayable returns (struct ITokenBundle.Token[]) -``` - -Lets a pack owner open a pack and receive the pack's reward unit. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| packId | uint256 | The identifier of the pack to open. -| amountToOpen | uint256 | The number of packs to open at once. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | ITokenBundle.Token[] | undefined - - - -## Events - -### PackCreated - -```solidity -event PackCreated(uint256 indexed packId, address indexed packCreator, address recipient, uint256 totalPacksCreated) -``` - -Emitted when a set of packs is created. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| packId `indexed` | uint256 | undefined | -| packCreator `indexed` | address | undefined | -| recipient | address | undefined | -| totalPacksCreated | uint256 | undefined | - -### PackOpened - -```solidity -event PackOpened(uint256 indexed packId, address indexed opener, uint256 numOfPacksOpened, ITokenBundle.Token[] rewardUnitsDistributed) -``` - -Emitted when a pack is opened. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| packId `indexed` | uint256 | undefined | -| opener `indexed` | address | undefined | -| numOfPacksOpened | uint256 | undefined | -| rewardUnitsDistributed | ITokenBundle.Token[] | undefined | - - - diff --git a/docs/IPermissions.md b/docs/IPermissions.md deleted file mode 100644 index 3156847c8..000000000 --- a/docs/IPermissions.md +++ /dev/null @@ -1,168 +0,0 @@ -# IPermissions - - - - - - - -*External interface of AccessControl declared to support ERC165 detection.* - -## Methods - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {AccessControl-_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - - - -## Events - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - -*Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole` `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite {RoleAdminChanged} not being emitted signaling this. _Available since v3.1._* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - -*Emitted when `account` is granted `role`. `sender` is the account that originated the contract call, an admin role bearer except when using {AccessControl-_setupRole}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - -*Emitted when `account` is revoked `role`. `sender` is the account that originated the contract call: - if using `revokeRole`, it is the admin role bearer - if using `renounceRole`, it is the role bearer (i.e. `account`)* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - - - diff --git a/docs/IPermissionsEnumerable.md b/docs/IPermissionsEnumerable.md deleted file mode 100644 index aba66bbfb..000000000 --- a/docs/IPermissionsEnumerable.md +++ /dev/null @@ -1,213 +0,0 @@ -# IPermissionsEnumerable - - - - - - - -*External interface of AccessControlEnumerable declared to support ERC165 detection.* - -## Methods - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {AccessControl-_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleMember - -```solidity -function getRoleMember(bytes32 role, uint256 index) external view returns (address) -``` - - - -*Returns one of the accounts that have `role`. `index` must be a value between 0 and {getRoleMemberCount}, non-inclusive. Role bearers are not sorted in any particular way, and their ordering may change at any point. WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure you perform all queries on the same block. See the following https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] for more information.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getRoleMemberCount - -```solidity -function getRoleMemberCount(bytes32 role) external view returns (uint256) -``` - - - -*Returns the number of accounts that have `role`. Can be used together with {getRoleMember} to enumerate all bearers of a role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - - - -## Events - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - - - diff --git a/docs/IPlatformFee.md b/docs/IPlatformFee.md deleted file mode 100644 index 2ccaa430f..000000000 --- a/docs/IPlatformFee.md +++ /dev/null @@ -1,70 +0,0 @@ -# IPlatformFee - - - - - -Thirdweb's `PlatformFee` is a contract extension to be used with any base contract. It exposes functions for setting and reading the recipient of platform fee and the platform fee basis points, and lets the inheriting contract perform conditional logic that uses information about platform fees, if desired. - - - -## Methods - -### getPlatformFeeInfo - -```solidity -function getPlatformFeeInfo() external view returns (address, uint16) -``` - - - -*Returns the platform fee bps and recipient.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### setPlatformFeeInfo - -```solidity -function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) external nonpayable -``` - - - -*Lets a module admin update the fees on primary sales.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _platformFeeRecipient | address | undefined -| _platformFeeBps | uint256 | undefined - - - -## Events - -### PlatformFeeInfoUpdated - -```solidity -event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps) -``` - - - -*Emitted when fee on primary sales is updated.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| platformFeeRecipient `indexed` | address | undefined | -| platformFeeBps | uint256 | undefined | - - - diff --git a/docs/IPrimarySale.md b/docs/IPrimarySale.md deleted file mode 100644 index e0ecea162..000000000 --- a/docs/IPrimarySale.md +++ /dev/null @@ -1,67 +0,0 @@ -# IPrimarySale - - - - - -Thirdweb's `Primary` is a contract extension to be used with any base contract. It exposes functions for setting and reading the recipient of primary sales, and lets the inheriting contract perform conditional logic that uses information about primary sales, if desired. - - - -## Methods - -### primarySaleRecipient - -```solidity -function primarySaleRecipient() external view returns (address) -``` - - - -*The adress that receives all primary sales value.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### setPrimarySaleRecipient - -```solidity -function setPrimarySaleRecipient(address _saleRecipient) external nonpayable -``` - - - -*Lets a module admin set the default recipient of all primary sales.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _saleRecipient | address | undefined - - - -## Events - -### PrimarySaleRecipientUpdated - -```solidity -event PrimarySaleRecipientUpdated(address indexed recipient) -``` - - - -*Emitted when a new sale recipient is set.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| recipient `indexed` | address | undefined | - - - diff --git a/docs/IRoyalty.md b/docs/IRoyalty.md deleted file mode 100644 index 41ef88651..000000000 --- a/docs/IRoyalty.md +++ /dev/null @@ -1,175 +0,0 @@ -# IRoyalty - - - - - -Thirdweb's `Royalty` is a contract extension to be used with any base contract. It exposes functions for setting and reading the recipient of royalty fee and the royalty fee basis points, and lets the inheriting contract perform conditional logic that uses information about royalty fees, if desired. The `Royalty` contract is ERC2981 compliant. - - - -## Methods - -### getDefaultRoyaltyInfo - -```solidity -function getDefaultRoyaltyInfo() external view returns (address, uint16) -``` - - - -*Returns the royalty recipient and fee bps.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### getRoyaltyInfoForToken - -```solidity -function getRoyaltyInfoForToken(uint256 tokenId) external view returns (address, uint16) -``` - - - -*Returns the royalty recipient for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### royaltyInfo - -```solidity -function royaltyInfo(uint256 tokenId, uint256 salePrice) external view returns (address receiver, uint256 royaltyAmount) -``` - - - -*Returns how much royalty is owed and to whom, based on a sale price that may be denominated in any unit of exchange. The royalty amount is denominated and should be payed in that same unit of exchange.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined -| salePrice | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| receiver | address | undefined -| royaltyAmount | uint256 | undefined - -### setDefaultRoyaltyInfo - -```solidity -function setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) external nonpayable -``` - - - -*Lets a module admin update the royalty bps and recipient.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _royaltyRecipient | address | undefined -| _royaltyBps | uint256 | undefined - -### setRoyaltyInfoForToken - -```solidity -function setRoyaltyInfoForToken(uint256 tokenId, address recipient, uint256 bps) external nonpayable -``` - - - -*Lets a module admin set the royalty recipient for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined -| recipient | address | undefined -| bps | uint256 | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - -## Events - -### DefaultRoyalty - -```solidity -event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps) -``` - - - -*Emitted when royalty info is updated.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| newRoyaltyRecipient `indexed` | address | undefined | -| newRoyaltyBps | uint256 | undefined | - -### RoyaltyForToken - -```solidity -event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, uint256 royaltyBps) -``` - - - -*Emitted when royalty recipient for tokenId is set* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId `indexed` | uint256 | undefined | -| royaltyRecipient `indexed` | address | undefined | -| royaltyBps | uint256 | undefined | - - - diff --git a/docs/ISignatureMintERC1155.md b/docs/ISignatureMintERC1155.md deleted file mode 100644 index c97ddd801..000000000 --- a/docs/ISignatureMintERC1155.md +++ /dev/null @@ -1,84 +0,0 @@ -# ISignatureMintERC1155 - - - - - -The 'signature minting' mechanism used in thirdweb Token smart contracts is a way for a contract admin to authorize an external party's request to mint tokens on the admin's contract. At a high level, this means you can authorize some external party to mint tokens on your contract, and specify what exactly will be minted by that external party. - - - -## Methods - -### mintWithSignature - -```solidity -function mintWithSignature(ISignatureMintERC1155.MintRequest req, bytes signature) external payable returns (address signer) -``` - -Mints tokens according to the provided mint request. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| req | ISignatureMintERC1155.MintRequest | The payload / mint request. -| signature | bytes | The signature produced by an account signing the mint request. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| signer | address | undefined - -### verify - -```solidity -function verify(ISignatureMintERC1155.MintRequest req, bytes signature) external view returns (bool success, address signer) -``` - -Verifies that a mint request is signed by an account holding MINTER_ROLE (at the time of the function call). - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| req | ISignatureMintERC1155.MintRequest | The payload / mint request. -| signature | bytes | The signature produced by an account signing the mint request. returns (success, signer) Result of verification and the recovered address. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| success | bool | undefined -| signer | address | undefined - - - -## Events - -### TokensMintedWithSignature - -```solidity -event TokensMintedWithSignature(address indexed signer, address indexed mintedTo, uint256 indexed tokenIdMinted, ISignatureMintERC1155.MintRequest mintRequest) -``` - - - -*Emitted when tokens are minted.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| signer `indexed` | address | undefined | -| mintedTo `indexed` | address | undefined | -| tokenIdMinted `indexed` | uint256 | undefined | -| mintRequest | ISignatureMintERC1155.MintRequest | undefined | - - - diff --git a/docs/ISignatureMintERC20.md b/docs/ISignatureMintERC20.md deleted file mode 100644 index 8e3e73b7f..000000000 --- a/docs/ISignatureMintERC20.md +++ /dev/null @@ -1,83 +0,0 @@ -# ISignatureMintERC20 - - - - - -The 'signature minting' mechanism used in thirdweb Token smart contracts is a way for a contract admin to authorize an external party's request to mint tokens on the admin's contract. At a high level, this means you can authorize some external party to mint tokens on your contract, and specify what exactly will be minted by that external party. - - - -## Methods - -### mintWithSignature - -```solidity -function mintWithSignature(ISignatureMintERC20.MintRequest req, bytes signature) external payable returns (address signer) -``` - -Mints tokens according to the provided mint request. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| req | ISignatureMintERC20.MintRequest | The payload / mint request. -| signature | bytes | The signature produced by an account signing the mint request. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| signer | address | undefined - -### verify - -```solidity -function verify(ISignatureMintERC20.MintRequest req, bytes signature) external view returns (bool success, address signer) -``` - -Verifies that a mint request is signed by an account holding MINTER_ROLE (at the time of the function call). - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| req | ISignatureMintERC20.MintRequest | The payload / mint request. -| signature | bytes | The signature produced by an account signing the mint request. returns (success, signer) Result of verification and the recovered address. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| success | bool | undefined -| signer | address | undefined - - - -## Events - -### TokensMintedWithSignature - -```solidity -event TokensMintedWithSignature(address indexed signer, address indexed mintedTo, ISignatureMintERC20.MintRequest mintRequest) -``` - - - -*Emitted when tokens are minted.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| signer `indexed` | address | undefined | -| mintedTo `indexed` | address | undefined | -| mintRequest | ISignatureMintERC20.MintRequest | undefined | - - - diff --git a/docs/ISignatureMintERC721.md b/docs/ISignatureMintERC721.md deleted file mode 100644 index 74e612f7f..000000000 --- a/docs/ISignatureMintERC721.md +++ /dev/null @@ -1,84 +0,0 @@ -# ISignatureMintERC721 - - - - - -The 'signature minting' mechanism used in thirdweb Token smart contracts is a way for a contract admin to authorize an external party's request to mint tokens on the admin's contract. At a high level, this means you can authorize some external party to mint tokens on your contract, and specify what exactly will be minted by that external party. - - - -## Methods - -### mintWithSignature - -```solidity -function mintWithSignature(ISignatureMintERC721.MintRequest req, bytes signature) external payable returns (address signer) -``` - -Mints tokens according to the provided mint request. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| req | ISignatureMintERC721.MintRequest | The payload / mint request. -| signature | bytes | The signature produced by an account signing the mint request. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| signer | address | undefined - -### verify - -```solidity -function verify(ISignatureMintERC721.MintRequest req, bytes signature) external view returns (bool success, address signer) -``` - -Verifies that a mint request is signed by an account holding MINTER_ROLE (at the time of the function call). - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| req | ISignatureMintERC721.MintRequest | The payload / mint request. -| signature | bytes | The signature produced by an account signing the mint request. returns (success, signer) Result of verification and the recovered address. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| success | bool | undefined -| signer | address | undefined - - - -## Events - -### TokensMintedWithSignature - -```solidity -event TokensMintedWithSignature(address indexed signer, address indexed mintedTo, uint256 indexed tokenIdMinted, ISignatureMintERC721.MintRequest mintRequest) -``` - - - -*Emitted when tokens are minted.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| signer `indexed` | address | undefined | -| mintedTo `indexed` | address | undefined | -| tokenIdMinted `indexed` | uint256 | undefined | -| mintRequest | ISignatureMintERC721.MintRequest | undefined | - - - diff --git a/docs/ITWFee.md b/docs/ITWFee.md deleted file mode 100644 index 02fbdd273..000000000 --- a/docs/ITWFee.md +++ /dev/null @@ -1,39 +0,0 @@ -# ITWFee - - - - - - - - - -## Methods - -### getFeeInfo - -```solidity -function getFeeInfo(address _proxy, uint256 _type) external view returns (address recipient, uint256 bps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _proxy | address | undefined -| _type | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| recipient | address | undefined -| bps | uint256 | undefined - - - - diff --git a/docs/IThirdwebContract.md b/docs/IThirdwebContract.md deleted file mode 100644 index 4142404d3..000000000 --- a/docs/IThirdwebContract.md +++ /dev/null @@ -1,82 +0,0 @@ -# IThirdwebContract - - - - - - - - - -## Methods - -### contractType - -```solidity -function contractType() external pure returns (bytes32) -``` - - - -*Returns the module type of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### contractURI - -```solidity -function contractURI() external view returns (string) -``` - - - -*Returns the metadata URI of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### contractVersion - -```solidity -function contractVersion() external pure returns (uint8) -``` - - - -*Returns the version of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint8 | undefined - -### setContractURI - -```solidity -function setContractURI(string _uri) external nonpayable -``` - - - -*Sets contract URI for the storefront-level metadata of the contract. Only module admin can call this function.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _uri | string | undefined - - - - diff --git a/docs/ITokenBundle.md b/docs/ITokenBundle.md deleted file mode 100644 index 9d6329b42..000000000 --- a/docs/ITokenBundle.md +++ /dev/null @@ -1,12 +0,0 @@ -# ITokenBundle - - - - - -Group together arbitrary ERC20, ERC721 and ERC1155 tokens into a single bundle. The `Token` struct is a generic type that can describe any ERC20, ERC721 or ERC1155 token. The `Bundle` struct is a data structure to track a group/bundle of multiple assets i.e. ERC20, ERC721 and ERC1155 tokens, each described as a `Token`. Expressing tokens as the `Token` type, and grouping them as a `Bundle` allows for writing generic logic to handle any ERC20, ERC721 or ERC1155 tokens. - - - - - diff --git a/docs/ITokenERC1155.md b/docs/ITokenERC1155.md deleted file mode 100644 index 5db23fadd..000000000 --- a/docs/ITokenERC1155.md +++ /dev/null @@ -1,339 +0,0 @@ -# ITokenERC1155 - - - - - -`SignatureMint1155` is an ERC 1155 contract. It lets anyone mint NFTs by producing a mint request and a signature (produced by an account with MINTER_ROLE, signing the mint request). - - - -## Methods - -### balanceOf - -```solidity -function balanceOf(address account, uint256 id) external view returns (uint256) -``` - - - -*Returns the amount of tokens of token type `id` owned by `account`. Requirements: - `account` cannot be the zero address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| id | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### balanceOfBatch - -```solidity -function balanceOfBatch(address[] accounts, uint256[] ids) external view returns (uint256[]) -``` - - - -*xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {balanceOf}. Requirements: - `accounts` and `ids` must have the same length.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| accounts | address[] | undefined -| ids | uint256[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256[] | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address account, address operator) external view returns (bool) -``` - - - -*Returns true if `operator` is approved to transfer ``account``'s tokens. See {setApprovalForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### mintTo - -```solidity -function mintTo(address to, uint256 tokenId, string uri, uint256 amount) external nonpayable -``` - -Lets an account with MINTER_ROLE mint an NFT. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | The address to mint the NFT to. -| tokenId | uint256 | The tokenId of the NFTs to mint -| uri | string | The URI to assign to the NFT. -| amount | uint256 | The number of copies of the NFT to mint. - -### mintWithSignature - -```solidity -function mintWithSignature(ITokenERC1155.MintRequest req, bytes signature) external payable -``` - -Mints an NFT according to the provided mint request. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| req | ITokenERC1155.MintRequest | The mint request. -| signature | bytes | he signature produced by an account signing the mint request. - -### safeBatchTransferFrom - -```solidity -function safeBatchTransferFrom(address from, address to, uint256[] ids, uint256[] amounts, bytes data) external nonpayable -``` - - - -*xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {safeTransferFrom}. Emits a {TransferBatch} event. Requirements: - `ids` and `amounts` must have the same length. - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the acceptance magic value.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| ids | uint256[] | undefined -| amounts | uint256[] | undefined -| data | bytes | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data) external nonpayable -``` - - - -*Transfers `amount` tokens of token type `id` from `from` to `to`. Emits a {TransferSingle} event. Requirements: - `to` cannot be the zero address. - If the caller is not `from`, it must be have been approved to spend ``from``'s tokens via {setApprovalForAll}. - `from` must have a balance of tokens of type `id` of at least `amount`. - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the acceptance magic value.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| id | uint256 | undefined -| amount | uint256 | undefined -| data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool approved) external nonpayable -``` - - - -*Grants or revokes permission to `operator` to transfer the caller's tokens, according to `approved`, Emits an {ApprovalForAll} event. Requirements: - `operator` cannot be the caller.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| approved | bool | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### verify - -```solidity -function verify(ITokenERC1155.MintRequest req, bytes signature) external view returns (bool success, address signer) -``` - -Verifies that a mint request is signed by an account holding MINTER_ROLE (at the time of the function call). - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| req | ITokenERC1155.MintRequest | The mint request. -| signature | bytes | The signature produced by an account signing the mint request. returns (success, signer) Result of verification and the recovered address. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| success | bool | undefined -| signer | address | undefined - - - -## Events - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed account, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### TokensMinted - -```solidity -event TokensMinted(address indexed mintedTo, uint256 indexed tokenIdMinted, string uri, uint256 quantityMinted) -``` - - - -*Emitted when an account with MINTER_ROLE mints an NFT.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| mintedTo `indexed` | address | undefined | -| tokenIdMinted `indexed` | uint256 | undefined | -| uri | string | undefined | -| quantityMinted | uint256 | undefined | - -### TokensMintedWithSignature - -```solidity -event TokensMintedWithSignature(address indexed signer, address indexed mintedTo, uint256 indexed tokenIdMinted, ITokenERC1155.MintRequest mintRequest) -``` - - - -*Emitted when tokens are minted.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| signer `indexed` | address | undefined | -| mintedTo `indexed` | address | undefined | -| tokenIdMinted `indexed` | uint256 | undefined | -| mintRequest | ITokenERC1155.MintRequest | undefined | - -### TransferBatch - -```solidity -event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| ids | uint256[] | undefined | -| values | uint256[] | undefined | - -### TransferSingle - -```solidity -event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| id | uint256 | undefined | -| value | uint256 | undefined | - -### URI - -```solidity -event URI(string value, uint256 indexed id) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| value | string | undefined | -| id `indexed` | uint256 | undefined | - - - diff --git a/docs/ITokenERC20.md b/docs/ITokenERC20.md deleted file mode 100644 index ba0e1cc6a..000000000 --- a/docs/ITokenERC20.md +++ /dev/null @@ -1,330 +0,0 @@ -# ITokenERC20 - - - - - - - - - -## Methods - -### allowance - -```solidity -function allowance(address owner, address spender) external view returns (uint256) -``` - - - -*Returns the remaining number of tokens that `spender` will be allowed to spend on behalf of `owner` through {transferFrom}. This is zero by default. This value changes when {approve} or {transferFrom} are called.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| spender | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### approve - -```solidity -function approve(address spender, uint256 amount) external nonpayable returns (bool) -``` - - - -*Sets `amount` as the allowance of `spender` over the caller's tokens. Returns a boolean value indicating whether the operation succeeded. IMPORTANT: Beware that changing an allowance with this method brings the risk that someone may use both the old and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 Emits an {Approval} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### balanceOf - -```solidity -function balanceOf(address account) external view returns (uint256) -``` - - - -*Returns the amount of tokens owned by `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### decimals - -```solidity -function decimals() external view returns (uint8) -``` - - - -*Returns the decimals places of the token.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint8 | undefined - -### mintTo - -```solidity -function mintTo(address to, uint256 amount) external nonpayable -``` - - - -*Creates `amount` new tokens for `to`. See {ERC20-_mint}. Requirements: - the caller must have the `MINTER_ROLE`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| amount | uint256 | undefined - -### mintWithSignature - -```solidity -function mintWithSignature(ITokenERC20.MintRequest req, bytes signature) external payable -``` - -Mints an NFT according to the provided mint request. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| req | ITokenERC20.MintRequest | The mint request. -| signature | bytes | he signature produced by an account signing the mint request. - -### name - -```solidity -function name() external view returns (string) -``` - - - -*Returns the name of the token.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - -*Returns the symbol of the token.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - -*Returns the amount of tokens in existence.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transfer - -```solidity -function transfer(address to, uint256 amount) external nonpayable returns (bool) -``` - - - -*Moves `amount` tokens from the caller's account to `to`. Returns a boolean value indicating whether the operation succeeded. Emits a {Transfer} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 amount) external nonpayable returns (bool) -``` - - - -*Moves `amount` tokens from `from` to `to` using the allowance mechanism. `amount` is then deducted from the caller's allowance. Returns a boolean value indicating whether the operation succeeded. Emits a {Transfer} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### verify - -```solidity -function verify(ITokenERC20.MintRequest req, bytes signature) external view returns (bool success, address signer) -``` - -Verifies that a mint request is signed by an account holding MINTER_ROLE (at the time of the function call). - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| req | ITokenERC20.MintRequest | The mint request. -| signature | bytes | The signature produced by an account signing the mint request. returns (success, signer) Result of verification and the recovered address. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| success | bool | undefined -| signer | address | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed spender, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| spender `indexed` | address | undefined | -| value | uint256 | undefined | - -### TokensMinted - -```solidity -event TokensMinted(address indexed mintedTo, uint256 quantityMinted) -``` - - - -*Emitted when an account with MINTER_ROLE mints an NFT.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| mintedTo `indexed` | address | undefined | -| quantityMinted | uint256 | undefined | - -### TokensMintedWithSignature - -```solidity -event TokensMintedWithSignature(address indexed signer, address indexed mintedTo, ITokenERC20.MintRequest mintRequest) -``` - - - -*Emitted when tokens are minted.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| signer `indexed` | address | undefined | -| mintedTo `indexed` | address | undefined | -| mintRequest | ITokenERC20.MintRequest | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| value | uint256 | undefined | - - - diff --git a/docs/ITokenERC721.md b/docs/ITokenERC721.md deleted file mode 100644 index 9d0a72c9a..000000000 --- a/docs/ITokenERC721.md +++ /dev/null @@ -1,361 +0,0 @@ -# ITokenERC721 - - - - - -`SignatureMint` is an ERC 721 contract. It lets anyone mint NFTs by producing a mint request and a signature (produced by an account with MINTER_ROLE, signing the mint request). - - - -## Methods - -### approve - -```solidity -function approve(address to, uint256 tokenId) external nonpayable -``` - - - -*Gives permission to `to` to transfer `tokenId` token to another account. The approval is cleared when the token is transferred. Only a single account can be approved at a time, so approving the zero address clears previous approvals. Requirements: - The caller must own the token or be an approved operator. - `tokenId` must exist. Emits an {Approval} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| tokenId | uint256 | undefined - -### balanceOf - -```solidity -function balanceOf(address owner) external view returns (uint256 balance) -``` - - - -*Returns the number of tokens in ``owner``'s account.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| balance | uint256 | undefined - -### getApproved - -```solidity -function getApproved(uint256 tokenId) external view returns (address operator) -``` - - - -*Returns the account approved for `tokenId` token. Requirements: - `tokenId` must exist.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address owner, address operator) external view returns (bool) -``` - - - -*Returns if the `operator` is allowed to manage all of the assets of `owner`. See {setApprovalForAll}* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### mintTo - -```solidity -function mintTo(address to, string uri) external nonpayable returns (uint256) -``` - -Lets an account with MINTER_ROLE mint an NFT. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | The address to mint the NFT to. -| uri | string | The URI to assign to the NFT. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | tokenId of the NFT minted. - -### mintWithSignature - -```solidity -function mintWithSignature(ITokenERC721.MintRequest req, bytes signature) external payable returns (uint256) -``` - -Mints an NFT according to the provided mint request. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| req | ITokenERC721.MintRequest | The mint request. -| signature | bytes | he signature produced by an account signing the mint request. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### ownerOf - -```solidity -function ownerOf(uint256 tokenId) external view returns (address owner) -``` - - - -*Returns the owner of the `tokenId` token. Requirements: - `tokenId` must exist.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 tokenId, bytes data) external nonpayable -``` - - - -*Safely transfers `tokenId` token from `from` to `to`. Requirements: - `from` cannot be the zero address. - `to` cannot be the zero address. - `tokenId` token must exist and be owned by `from`. - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. Emits a {Transfer} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined -| data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool _approved) external nonpayable -``` - - - -*Approve or remove `operator` as an operator for the caller. Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller. Requirements: - The `operator` cannot be the caller. Emits an {ApprovalForAll} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| _approved | bool | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 tokenId) external nonpayable -``` - - - -*Transfers `tokenId` token from `from` to `to`. WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible. Requirements: - `from` cannot be the zero address. - `to` cannot be the zero address. - `tokenId` token must be owned by `from`. - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. Emits a {Transfer} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined - -### verify - -```solidity -function verify(ITokenERC721.MintRequest req, bytes signature) external view returns (bool success, address signer) -``` - -Verifies that a mint request is signed by an account holding MINTER_ROLE (at the time of the function call). - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| req | ITokenERC721.MintRequest | The mint request. -| signature | bytes | The signature produced by an account signing the mint request. returns (success, signer) Result of verification and the recovered address. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| success | bool | undefined -| signer | address | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| approved `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed owner, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### TokensMinted - -```solidity -event TokensMinted(address indexed mintedTo, uint256 indexed tokenIdMinted, string uri) -``` - - - -*Emitted when an account with MINTER_ROLE mints an NFT.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| mintedTo `indexed` | address | undefined | -| tokenIdMinted `indexed` | uint256 | undefined | -| uri | string | undefined | - -### TokensMintedWithSignature - -```solidity -event TokensMintedWithSignature(address indexed signer, address indexed mintedTo, uint256 indexed tokenIdMinted, ITokenERC721.MintRequest mintRequest) -``` - - - -*Emitted when tokens are minted.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| signer `indexed` | address | undefined | -| mintedTo `indexed` | address | undefined | -| tokenIdMinted `indexed` | uint256 | undefined | -| mintRequest | ITokenERC721.MintRequest | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - - - diff --git a/docs/IVotesUpgradeable.md b/docs/IVotesUpgradeable.md deleted file mode 100644 index 7e696f1b2..000000000 --- a/docs/IVotesUpgradeable.md +++ /dev/null @@ -1,180 +0,0 @@ -# IVotesUpgradeable - - - - - - - -*Common interface for {ERC20Votes}, {ERC721Votes}, and other {Votes}-enabled contracts. _Available since v4.5._* - -## Methods - -### delegate - -```solidity -function delegate(address delegatee) external nonpayable -``` - - - -*Delegates votes from the sender to `delegatee`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| delegatee | address | undefined - -### delegateBySig - -```solidity -function delegateBySig(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) external nonpayable -``` - - - -*Delegates votes from signer to `delegatee`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| delegatee | address | undefined -| nonce | uint256 | undefined -| expiry | uint256 | undefined -| v | uint8 | undefined -| r | bytes32 | undefined -| s | bytes32 | undefined - -### delegates - -```solidity -function delegates(address account) external view returns (address) -``` - - - -*Returns the delegate that `account` has chosen.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getPastTotalSupply - -```solidity -function getPastTotalSupply(uint256 blockNumber) external view returns (uint256) -``` - - - -*Returns the total supply of votes available at the end of a past block (`blockNumber`). NOTE: This value is the sum of all available votes, which is not necessarily the sum of all delegated votes. Votes that have not been delegated are still part of total supply, even though they would not participate in a vote.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| blockNumber | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getPastVotes - -```solidity -function getPastVotes(address account, uint256 blockNumber) external view returns (uint256) -``` - - - -*Returns the amount of votes that `account` had at the end of a past block (`blockNumber`).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| blockNumber | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getVotes - -```solidity -function getVotes(address account) external view returns (uint256) -``` - - - -*Returns the current amount of votes that `account` has.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - - - -## Events - -### DelegateChanged - -```solidity -event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate) -``` - - - -*Emitted when an account changes their delegate.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| delegator `indexed` | address | undefined | -| fromDelegate `indexed` | address | undefined | -| toDelegate `indexed` | address | undefined | - -### DelegateVotesChanged - -```solidity -event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance) -``` - - - -*Emitted when a token transfer or delegate change results in changes to a delegate's number of votes.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| delegate `indexed` | address | undefined | -| previousBalance | uint256 | undefined | -| newBalance | uint256 | undefined | - - - diff --git a/docs/IWETH.md b/docs/IWETH.md deleted file mode 100644 index 5a49f6d68..000000000 --- a/docs/IWETH.md +++ /dev/null @@ -1,65 +0,0 @@ -# IWETH - - - - - - - - - -## Methods - -### deposit - -```solidity -function deposit() external payable -``` - - - - - - -### transfer - -```solidity -function transfer(address to, uint256 value) external nonpayable returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| value | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### withdraw - -```solidity -function withdraw(uint256 amount) external nonpayable -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| amount | uint256 | undefined - - - - diff --git a/docs/Initializable.md b/docs/Initializable.md deleted file mode 100644 index 647d95b62..000000000 --- a/docs/Initializable.md +++ /dev/null @@ -1,12 +0,0 @@ -# Initializable - - - - - - - -*This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer function so it can only be called once. The {initializer} modifier provided by this contract will have this effect. TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}. CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure that all initializers are idempotent. This is not verified automatically as constructors are by Solidity. [CAUTION] ==== Avoid leaving a contract uninitialized. An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation contract, which may impact the proxy. To initialize the implementation contract, you can either invoke the initializer manually, or you can include a constructor to automatically mark it as initialized when it is deployed: [.hljs-theme-light.nopadding] ```* - - - diff --git a/docs/LazyMint.md b/docs/LazyMint.md deleted file mode 100644 index cdd33f542..000000000 --- a/docs/LazyMint.md +++ /dev/null @@ -1,100 +0,0 @@ -# LazyMint - - - - - -The `LazyMint` is a contract extension for any base NFT contract. It lets you 'lazy mint' any number of NFTs at once. Here, 'lazy mint' means defining the metadata for particular tokenIds of your NFT contract, without actually minting a non-zero balance of NFTs of those tokenIds. - - - -## Methods - -### getBaseURICount - -```solidity -function getBaseURICount() external view returns (uint256) -``` - - - -*Returns the number of batches of tokens having the same baseURI.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getBatchIdAtIndex - -```solidity -function getBatchIdAtIndex(uint256 _index) external view returns (uint256) -``` - - - -*Returns the id for the batch of tokens the given tokenId belongs to.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### lazyMint - -```solidity -function lazyMint(uint256 _amount, string _baseURIForTokens, bytes _data) external nonpayable returns (uint256 batchId) -``` - -Lets an authorized address lazy mint a given amount of NFTs. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _amount | uint256 | The number of NFTs to lazy mint. -| _baseURIForTokens | string | The base URI for the 'n' number of NFTs being lazy minted, where the metadata for each of those NFTs is `${baseURIForTokens}/${tokenId}`. -| _data | bytes | Additional bytes data to be used at the discretion of the consumer of the contract. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| batchId | uint256 | A unique integer identifier for the batch of NFTs lazy minted together. - - - -## Events - -### TokensLazyMinted - -```solidity -event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| startTokenId `indexed` | uint256 | undefined | -| endTokenId | uint256 | undefined | -| baseURI | string | undefined | -| encryptedBaseURI | bytes | undefined | - - - diff --git a/docs/Marketplace.md b/docs/Marketplace.md deleted file mode 100644 index 841b875e6..000000000 --- a/docs/Marketplace.md +++ /dev/null @@ -1,964 +0,0 @@ -# Marketplace - - - - - - - - - -## Methods - -### DEFAULT_ADMIN_ROLE - -```solidity -function DEFAULT_ADMIN_ROLE() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### MAX_BPS - -```solidity -function MAX_BPS() external view returns (uint64) -``` - - - -*The max bps of the contract. So, 10_000 == 100 %* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint64 | undefined - -### acceptOffer - -```solidity -function acceptOffer(uint256 _listingId, address _offeror, address _currency, uint256 _pricePerToken) external nonpayable -``` - - - -*Lets a listing's creator accept an offer for their direct listing.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _listingId | uint256 | undefined -| _offeror | address | undefined -| _currency | address | undefined -| _pricePerToken | uint256 | undefined - -### bidBufferBps - -```solidity -function bidBufferBps() external view returns (uint64) -``` - - - -*The minimum % increase required from the previous winning bid. Default: 5%.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint64 | undefined - -### buy - -```solidity -function buy(uint256 _listingId, address _buyFor, uint256 _quantityToBuy, address _currency, uint256 _totalPrice) external payable -``` - - - -*Lets an account buy a given quantity of tokens from a listing.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _listingId | uint256 | undefined -| _buyFor | address | undefined -| _quantityToBuy | uint256 | undefined -| _currency | address | undefined -| _totalPrice | uint256 | undefined - -### cancelDirectListing - -```solidity -function cancelDirectListing(uint256 _listingId) external nonpayable -``` - - - -*Lets a direct listing creator cancel their listing.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _listingId | uint256 | undefined - -### closeAuction - -```solidity -function closeAuction(uint256 _listingId, address _closeFor) external nonpayable -``` - - - -*Lets an account close an auction for either the (1) winning bidder, or (2) auction creator.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _listingId | uint256 | undefined -| _closeFor | address | undefined - -### contractType - -```solidity -function contractType() external pure returns (bytes32) -``` - - - -*Returns the type of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### contractURI - -```solidity -function contractURI() external view returns (string) -``` - - - -*Contract level metadata.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### contractVersion - -```solidity -function contractVersion() external pure returns (uint8) -``` - - - -*Returns the version of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint8 | undefined - -### createListing - -```solidity -function createListing(IMarketplace.ListingParameters _params) external nonpayable -``` - - - -*Lets a token owner list tokens for sale: Direct Listing or Auction.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _params | IMarketplace.ListingParameters | undefined - -### getPlatformFeeInfo - -```solidity -function getPlatformFeeInfo() external view returns (address, uint16) -``` - - - -*Returns the platform fee recipient and bps.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleMember - -```solidity -function getRoleMember(bytes32 role, uint256 index) external view returns (address) -``` - - - -*Returns one of the accounts that have `role`. `index` must be a value between 0 and {getRoleMemberCount}, non-inclusive. Role bearers are not sorted in any particular way, and their ordering may change at any point. WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure you perform all queries on the same block. See the following https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] for more information.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getRoleMemberCount - -```solidity -function getRoleMemberCount(bytes32 role) external view returns (uint256) -``` - - - -*Returns the number of accounts that have `role`. Can be used together with {getRoleMember} to enumerate all bearers of a role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### initialize - -```solidity -function initialize(address _defaultAdmin, string _contractURI, address[] _trustedForwarders, address _platformFeeRecipient, uint256 _platformFeeBps) external nonpayable -``` - - - -*Initiliazes the contract, like a constructor.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _defaultAdmin | address | undefined -| _contractURI | string | undefined -| _trustedForwarders | address[] | undefined -| _platformFeeRecipient | address | undefined -| _platformFeeBps | uint256 | undefined - -### isTrustedForwarder - -```solidity -function isTrustedForwarder(address forwarder) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| forwarder | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### listings - -```solidity -function listings(uint256) external view returns (uint256 listingId, address tokenOwner, address assetContract, uint256 tokenId, uint256 startTime, uint256 endTime, uint256 quantity, address currency, uint256 reservePricePerToken, uint256 buyoutPricePerToken, enum IMarketplace.TokenType tokenType, enum IMarketplace.ListingType listingType) -``` - - - -*Mapping from uid of listing => listing info.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| listingId | uint256 | undefined -| tokenOwner | address | undefined -| assetContract | address | undefined -| tokenId | uint256 | undefined -| startTime | uint256 | undefined -| endTime | uint256 | undefined -| quantity | uint256 | undefined -| currency | address | undefined -| reservePricePerToken | uint256 | undefined -| buyoutPricePerToken | uint256 | undefined -| tokenType | enum IMarketplace.TokenType | undefined -| listingType | enum IMarketplace.ListingType | undefined - -### multicall - -```solidity -function multicall(bytes[] data) external nonpayable returns (bytes[] results) -``` - - - -*Receives and executes a batch of function calls on this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| results | bytes[] | undefined - -### offer - -```solidity -function offer(uint256 _listingId, uint256 _quantityWanted, address _currency, uint256 _pricePerToken, uint256 _expirationTimestamp) external payable -``` - - - -*Lets an account (1) make an offer to a direct listing, or (2) make a bid in an auction.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _listingId | uint256 | undefined -| _quantityWanted | uint256 | undefined -| _currency | address | undefined -| _pricePerToken | uint256 | undefined -| _expirationTimestamp | uint256 | undefined - -### offers - -```solidity -function offers(uint256, address) external view returns (uint256 listingId, address offeror, uint256 quantityWanted, address currency, uint256 pricePerToken, uint256 expirationTimestamp) -``` - - - -*Mapping from uid of a direct listing => offeror address => offer made to the direct listing by the respective offeror.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined -| _1 | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| listingId | uint256 | undefined -| offeror | address | undefined -| quantityWanted | uint256 | undefined -| currency | address | undefined -| pricePerToken | uint256 | undefined -| expirationTimestamp | uint256 | undefined - -### onERC1155BatchReceived - -```solidity -function onERC1155BatchReceived(address, address, uint256[], uint256[], bytes) external nonpayable returns (bytes4) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | address | undefined -| _2 | uint256[] | undefined -| _3 | uint256[] | undefined -| _4 | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - -### onERC1155Received - -```solidity -function onERC1155Received(address, address, uint256, uint256, bytes) external nonpayable returns (bytes4) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | address | undefined -| _2 | uint256 | undefined -| _3 | uint256 | undefined -| _4 | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - -### onERC721Received - -```solidity -function onERC721Received(address, address, uint256, bytes) external pure returns (bytes4) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | address | undefined -| _2 | uint256 | undefined -| _3 | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been revoked `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### setAuctionBuffers - -```solidity -function setAuctionBuffers(uint256 _timeBuffer, uint256 _bidBufferBps) external nonpayable -``` - - - -*Lets a contract admin set auction buffers.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _timeBuffer | uint256 | undefined -| _bidBufferBps | uint256 | undefined - -### setContractURI - -```solidity -function setContractURI(string _uri) external nonpayable -``` - - - -*Lets a contract admin set the URI for the contract-level metadata.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _uri | string | undefined - -### setPlatformFeeInfo - -```solidity -function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) external nonpayable -``` - - - -*Lets a contract admin update platform fee recipient and bps.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _platformFeeRecipient | address | undefined -| _platformFeeBps | uint256 | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### thirdwebFee - -```solidity -function thirdwebFee() external view returns (contract ITWFee) -``` - - - -*The thirdweb contract with fee related information.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | contract ITWFee | undefined - -### timeBuffer - -```solidity -function timeBuffer() external view returns (uint64) -``` - - - -*The amount of time added to an auction's 'endTime', if a bid is made within `timeBuffer` seconds of the existing `endTime`. Default: 15 minutes.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint64 | undefined - -### totalListings - -```solidity -function totalListings() external view returns (uint256) -``` - - - -*Total number of listings ever created in the marketplace.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### updateListing - -```solidity -function updateListing(uint256 _listingId, uint256 _quantityToList, uint256 _reservePricePerToken, uint256 _buyoutPricePerToken, address _currencyToAccept, uint256 _startTime, uint256 _secondsUntilEndTime) external nonpayable -``` - - - -*Lets a listing's creator edit the listing's parameters.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _listingId | uint256 | undefined -| _quantityToList | uint256 | undefined -| _reservePricePerToken | uint256 | undefined -| _buyoutPricePerToken | uint256 | undefined -| _currencyToAccept | address | undefined -| _startTime | uint256 | undefined -| _secondsUntilEndTime | uint256 | undefined - -### winningBid - -```solidity -function winningBid(uint256) external view returns (uint256 listingId, address offeror, uint256 quantityWanted, address currency, uint256 pricePerToken, uint256 expirationTimestamp) -``` - - - -*Mapping from uid of an auction listing => current winning bid in an auction.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| listingId | uint256 | undefined -| offeror | address | undefined -| quantityWanted | uint256 | undefined -| currency | address | undefined -| pricePerToken | uint256 | undefined -| expirationTimestamp | uint256 | undefined - - - -## Events - -### AuctionBuffersUpdated - -```solidity -event AuctionBuffersUpdated(uint256 timeBuffer, uint256 bidBufferBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| timeBuffer | uint256 | undefined | -| bidBufferBps | uint256 | undefined | - -### AuctionClosed - -```solidity -event AuctionClosed(uint256 indexed listingId, address indexed closer, bool indexed cancelled, address auctionCreator, address winningBidder) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| listingId `indexed` | uint256 | undefined | -| closer `indexed` | address | undefined | -| cancelled `indexed` | bool | undefined | -| auctionCreator | address | undefined | -| winningBidder | address | undefined | - -### ListingAdded - -```solidity -event ListingAdded(uint256 indexed listingId, address indexed assetContract, address indexed lister, IMarketplace.Listing listing) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| listingId `indexed` | uint256 | undefined | -| assetContract `indexed` | address | undefined | -| lister `indexed` | address | undefined | -| listing | IMarketplace.Listing | undefined | - -### ListingRemoved - -```solidity -event ListingRemoved(uint256 indexed listingId, address indexed listingCreator) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| listingId `indexed` | uint256 | undefined | -| listingCreator `indexed` | address | undefined | - -### ListingUpdated - -```solidity -event ListingUpdated(uint256 indexed listingId, address indexed listingCreator) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| listingId `indexed` | uint256 | undefined | -| listingCreator `indexed` | address | undefined | - -### NewOffer - -```solidity -event NewOffer(uint256 indexed listingId, address indexed offeror, enum IMarketplace.ListingType indexed listingType, uint256 quantityWanted, uint256 totalOfferAmount, address currency) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| listingId `indexed` | uint256 | undefined | -| offeror `indexed` | address | undefined | -| listingType `indexed` | enum IMarketplace.ListingType | undefined | -| quantityWanted | uint256 | undefined | -| totalOfferAmount | uint256 | undefined | -| currency | address | undefined | - -### NewSale - -```solidity -event NewSale(uint256 indexed listingId, address indexed assetContract, address indexed lister, address buyer, uint256 quantityBought, uint256 totalPricePaid) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| listingId `indexed` | uint256 | undefined | -| assetContract `indexed` | address | undefined | -| lister `indexed` | address | undefined | -| buyer | address | undefined | -| quantityBought | uint256 | undefined | -| totalPricePaid | uint256 | undefined | - -### PlatformFeeInfoUpdated - -```solidity -event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| platformFeeRecipient `indexed` | address | undefined | -| platformFeeBps | uint256 | undefined | - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - - - diff --git a/docs/MathUpgradeable.md b/docs/MathUpgradeable.md deleted file mode 100644 index 9c18c68bc..000000000 --- a/docs/MathUpgradeable.md +++ /dev/null @@ -1,12 +0,0 @@ -# MathUpgradeable - - - - - - - -*Standard math utilities missing in the Solidity language.* - - - diff --git a/docs/MerkleProof.md b/docs/MerkleProof.md deleted file mode 100644 index 1a364dc9a..000000000 --- a/docs/MerkleProof.md +++ /dev/null @@ -1,12 +0,0 @@ -# MerkleProof - - - - - - - -*These functions deal with verification of Merkle Trees proofs. The proofs can be generated using the JavaScript library https://github.com/miguelmota/merkletreejs[merkletreejs]. Note: the hashing algorithm should be keccak256 and pair sorting should be enabled. See `test/utils/cryptography/MerkleProof.test.js` for some examples. Source: https://github.com/ensdomains/governance/blob/master/contracts/MerkleProof.sol* - - - diff --git a/docs/MinimalForwarder.md b/docs/MinimalForwarder.md deleted file mode 100644 index 0844ef9e1..000000000 --- a/docs/MinimalForwarder.md +++ /dev/null @@ -1,84 +0,0 @@ -# MinimalForwarder - - - - - - - -*Simple minimal forwarder to be used together with an ERC2771 compatible contract. See {ERC2771Context}.* - -## Methods - -### execute - -```solidity -function execute(MinimalForwarder.ForwardRequest req, bytes signature) external payable returns (bool, bytes) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| req | MinimalForwarder.ForwardRequest | undefined -| signature | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined -| _1 | bytes | undefined - -### getNonce - -```solidity -function getNonce(address from) external view returns (uint256) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### verify - -```solidity -function verify(MinimalForwarder.ForwardRequest req, bytes signature) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| req | MinimalForwarder.ForwardRequest | undefined -| signature | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - - diff --git a/docs/Mock.md b/docs/Mock.md deleted file mode 100644 index 192c3b6e1..000000000 --- a/docs/Mock.md +++ /dev/null @@ -1,66 +0,0 @@ -# Mock - - - - - - - - - -## Methods - -### erc1155 - -```solidity -function erc1155() external view returns (contract IERC1155) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | contract IERC1155 | undefined - -### erc20 - -```solidity -function erc20() external view returns (contract IERC20) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | contract IERC20 | undefined - -### erc721 - -```solidity -function erc721() external view returns (contract IERC721) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | contract IERC721 | undefined - - - - diff --git a/docs/MockContract.md b/docs/MockContract.md deleted file mode 100644 index 2a6385859..000000000 --- a/docs/MockContract.md +++ /dev/null @@ -1,49 +0,0 @@ -# MockContract - - - - - - - - - -## Methods - -### contractType - -```solidity -function contractType() external view returns (bytes32) -``` - - - -*Returns the module type of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### contractVersion - -```solidity -function contractVersion() external view returns (uint8) -``` - - - -*Returns the version of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint8 | undefined - - - - diff --git a/docs/Multicall.md b/docs/Multicall.md deleted file mode 100644 index 5fd54846f..000000000 --- a/docs/Multicall.md +++ /dev/null @@ -1,37 +0,0 @@ -# Multicall - - - - - - - -*Provides a function to batch together multiple calls in a single external call. _Available since v4.1._* - -## Methods - -### multicall - -```solidity -function multicall(bytes[] data) external nonpayable returns (bytes[] results) -``` - - - -*Receives and executes a batch of function calls on this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| results | bytes[] | undefined - - - - diff --git a/docs/MulticallUpgradeable.md b/docs/MulticallUpgradeable.md deleted file mode 100644 index 7cfabcaec..000000000 --- a/docs/MulticallUpgradeable.md +++ /dev/null @@ -1,37 +0,0 @@ -# MulticallUpgradeable - - - - - - - -*Provides a function to batch together multiple calls in a single external call. _Available since v4.1._* - -## Methods - -### multicall - -```solidity -function multicall(bytes[] data) external nonpayable returns (bytes[] results) -``` - - - -*Receives and executes a batch of function calls on this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| results | bytes[] | undefined - - - - diff --git a/docs/Multiwrap.md b/docs/Multiwrap.md deleted file mode 100644 index 9b1d06396..000000000 --- a/docs/Multiwrap.md +++ /dev/null @@ -1,1158 +0,0 @@ -# Multiwrap - - - - - - - - - -## Methods - -### DEFAULT_ADMIN_ROLE - -```solidity -function DEFAULT_ADMIN_ROLE() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### NATIVE_TOKEN - -```solidity -function NATIVE_TOKEN() external view returns (address) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### approve - -```solidity -function approve(address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-approve}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| tokenId | uint256 | undefined - -### balanceOf - -```solidity -function balanceOf(address owner) external view returns (uint256) -``` - - - -*See {IERC721-balanceOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### contractType - -```solidity -function contractType() external pure returns (bytes32) -``` - - - -*Returns the type of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### contractURI - -```solidity -function contractURI() external view returns (string) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### contractVersion - -```solidity -function contractVersion() external pure returns (uint8) -``` - - - -*Returns the version of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint8 | undefined - -### getApproved - -```solidity -function getApproved(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-getApproved}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getDefaultRoyaltyInfo - -```solidity -function getDefaultRoyaltyInfo() external view returns (address, uint16) -``` - - - -*Returns the default royalty recipient and bps.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {AccessControl-_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleMember - -```solidity -function getRoleMember(bytes32 role, uint256 index) external view returns (address member) -``` - - - -*Returns one of the accounts that have `role`. `index` must be a value between 0 and {getRoleMemberCount}, non-inclusive. Role bearers are not sorted in any particular way, and their ordering may change at any point. WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure you perform all queries on the same block. See the following https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] for more information.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| member | address | undefined - -### getRoleMemberCount - -```solidity -function getRoleMemberCount(bytes32 role) external view returns (uint256 count) -``` - - - -*Returns the number of accounts that have `role`. Can be used together with {getRoleMember} to enumerate all bearers of a role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| count | uint256 | undefined - -### getRoyaltyInfoForToken - -```solidity -function getRoyaltyInfoForToken(uint256 _tokenId) external view returns (address, uint16) -``` - - - -*Returns the royalty recipient and bps for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### getTokenCountOfBundle - -```solidity -function getTokenCountOfBundle(uint256 _bundleId) external view returns (uint256) -``` - - - -*Returns the total number of assets in a particular bundle.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _bundleId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getTokenOfBundle - -```solidity -function getTokenOfBundle(uint256 _bundleId, uint256 index) external view returns (struct ITokenBundle.Token) -``` - - - -*Returns an asset contained in a particular bundle, at a particular index.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _bundleId | uint256 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | ITokenBundle.Token | undefined - -### getUriOfBundle - -```solidity -function getUriOfBundle(uint256 _bundleId) external view returns (string) -``` - - - -*Returns the uri of a particular bundle.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _bundleId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### getWrappedContents - -```solidity -function getWrappedContents(uint256 _tokenId) external view returns (struct ITokenBundle.Token[] contents) -``` - - - -*Returns the underlying contents of a wrapped NFT.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| contents | ITokenBundle.Token[] | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### hasRoleWithSwitch - -```solidity -function hasRoleWithSwitch(bytes32 role, address account) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### initialize - -```solidity -function initialize(address _defaultAdmin, string _name, string _symbol, string _contractURI, address[] _trustedForwarders, address _royaltyRecipient, uint256 _royaltyBps) external nonpayable -``` - - - -*Initiliazes the contract, like a constructor.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _defaultAdmin | address | undefined -| _name | string | undefined -| _symbol | string | undefined -| _contractURI | string | undefined -| _trustedForwarders | address[] | undefined -| _royaltyRecipient | address | undefined -| _royaltyBps | uint256 | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address owner, address operator) external view returns (bool) -``` - - - -*See {IERC721-isApprovedForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### isTrustedForwarder - -```solidity -function isTrustedForwarder(address forwarder) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| forwarder | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### multicall - -```solidity -function multicall(bytes[] data) external nonpayable returns (bytes[] results) -``` - - - -*Receives and executes a batch of function calls on this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| results | bytes[] | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*See {IERC721Metadata-name}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### nextTokenIdToMint - -```solidity -function nextTokenIdToMint() external view returns (uint256) -``` - - - -*The next token ID of the NFT to mint.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### onERC1155BatchReceived - -```solidity -function onERC1155BatchReceived(address, address, uint256[], uint256[], bytes) external nonpayable returns (bytes4) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | address | undefined -| _2 | uint256[] | undefined -| _3 | uint256[] | undefined -| _4 | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - -### onERC1155Received - -```solidity -function onERC1155Received(address, address, uint256, uint256, bytes) external nonpayable returns (bytes4) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | address | undefined -| _2 | uint256 | undefined -| _3 | uint256 | undefined -| _4 | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - -### onERC721Received - -```solidity -function onERC721Received(address, address, uint256, bytes) external nonpayable returns (bytes4) -``` - - - -*See {IERC721Receiver-onERC721Received}. Always returns `IERC721Receiver.onERC721Received.selector`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | address | undefined -| _2 | uint256 | undefined -| _3 | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - -### owner - -```solidity -function owner() external view returns (address) -``` - - - -*Returns the owner of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### ownerOf - -```solidity -function ownerOf(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-ownerOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### royaltyInfo - -```solidity -function royaltyInfo(uint256 tokenId, uint256 salePrice) external view returns (address receiver, uint256 royaltyAmount) -``` - - - -*Returns the royalty recipient and amount, given a tokenId and sale price.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined -| salePrice | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| receiver | address | undefined -| royaltyAmount | uint256 | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 tokenId, bytes _data) external nonpayable -``` - - - -*See {IERC721-safeTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined -| _data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool approved) external nonpayable -``` - - - -*See {IERC721-setApprovalForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| approved | bool | undefined - -### setContractURI - -```solidity -function setContractURI(string _uri) external nonpayable -``` - - - -*Lets a contract admin set the URI for contract-level metadata.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _uri | string | undefined - -### setDefaultRoyaltyInfo - -```solidity -function setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) external nonpayable -``` - - - -*Lets a contract admin update the default royalty recipient and bps.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _royaltyRecipient | address | undefined -| _royaltyBps | uint256 | undefined - -### setOwner - -```solidity -function setOwner(address _newOwner) external nonpayable -``` - - - -*Lets a contract admin set a new owner for the contract. The new owner must be a contract admin.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _newOwner | address | undefined - -### setRoyaltyInfoForToken - -```solidity -function setRoyaltyInfoForToken(uint256 _tokenId, address _recipient, uint256 _bps) external nonpayable -``` - - - -*Lets a contract admin set the royalty recipient and bps for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined -| _recipient | address | undefined -| _bps | uint256 | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See ERC 165* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - -*See {IERC721Metadata-symbol}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### tokenURI - -```solidity -function tokenURI(uint256 _tokenId) external view returns (string) -``` - - - -*Returns the URI for a given tokenId.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-transferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined - -### unwrap - -```solidity -function unwrap(uint256 _tokenId, address _recipient) external nonpayable -``` - - - -*Unwrap a wrapped NFT to retrieve underlying ERC1155, ERC721, ERC20 tokens.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined -| _recipient | address | undefined - -### wrap - -```solidity -function wrap(ITokenBundle.Token[] _tokensToWrap, string _uriForWrappedToken, address _recipient) external payable returns (uint256 tokenId) -``` - - - -*Wrap multiple ERC1155, ERC721, ERC20 tokens into a single wrapped NFT.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokensToWrap | ITokenBundle.Token[] | undefined -| _uriForWrappedToken | string | undefined -| _recipient | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| approved `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed owner, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### ContractURIUpdated - -```solidity -event ContractURIUpdated(string prevURI, string newURI) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| prevURI | string | undefined | -| newURI | string | undefined | - -### DefaultRoyalty - -```solidity -event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| newRoyaltyRecipient `indexed` | address | undefined | -| newRoyaltyBps | uint256 | undefined | - -### OwnerUpdated - -```solidity -event OwnerUpdated(address indexed prevOwner, address indexed newOwner) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| prevOwner `indexed` | address | undefined | -| newOwner `indexed` | address | undefined | - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoyaltyForToken - -```solidity -event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, uint256 royaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId `indexed` | uint256 | undefined | -| royaltyRecipient `indexed` | address | undefined | -| royaltyBps | uint256 | undefined | - -### TokensUnwrapped - -```solidity -event TokensUnwrapped(address indexed unwrapper, address indexed recipientOfWrappedContents, uint256 indexed tokenIdOfWrappedToken) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| unwrapper `indexed` | address | undefined | -| recipientOfWrappedContents `indexed` | address | undefined | -| tokenIdOfWrappedToken `indexed` | uint256 | undefined | - -### TokensWrapped - -```solidity -event TokensWrapped(address indexed wrapper, address indexed recipientOfWrappedToken, uint256 indexed tokenIdOfWrappedToken, ITokenBundle.Token[] wrappedContents) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| wrapper `indexed` | address | undefined | -| recipientOfWrappedToken `indexed` | address | undefined | -| tokenIdOfWrappedToken `indexed` | uint256 | undefined | -| wrappedContents | ITokenBundle.Token[] | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - - - diff --git a/docs/Ownable.md b/docs/Ownable.md deleted file mode 100644 index b6165007a..000000000 --- a/docs/Ownable.md +++ /dev/null @@ -1,68 +0,0 @@ -# Ownable - - - - - -Thirdweb's `Ownable` is a contract extension to be used with any base contract. It exposes functions for setting and reading who the 'owner' of the inheriting smart contract is, and lets the inheriting contract perform conditional logic that uses information about who the contract's owner is. - - - -## Methods - -### owner - -```solidity -function owner() external view returns (address) -``` - - - -*Returns the owner of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### setOwner - -```solidity -function setOwner(address _newOwner) external nonpayable -``` - - - -*Lets a contract admin set a new owner for the contract. The new owner must be a contract admin.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _newOwner | address | undefined - - - -## Events - -### OwnerUpdated - -```solidity -event OwnerUpdated(address indexed prevOwner, address indexed newOwner) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| prevOwner `indexed` | address | undefined | -| newOwner `indexed` | address | undefined | - - - diff --git a/docs/Pack.md b/docs/Pack.md deleted file mode 100644 index 2c623cc62..000000000 --- a/docs/Pack.md +++ /dev/null @@ -1,1228 +0,0 @@ -# Pack - - - - - - - - - -## Methods - -### DEFAULT_ADMIN_ROLE - -```solidity -function DEFAULT_ADMIN_ROLE() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### NATIVE_TOKEN - -```solidity -function NATIVE_TOKEN() external view returns (address) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### balanceOf - -```solidity -function balanceOf(address account, uint256 id) external view returns (uint256) -``` - - - -*See {IERC1155-balanceOf}. Requirements: - `account` cannot be the zero address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| id | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### balanceOfBatch - -```solidity -function balanceOfBatch(address[] accounts, uint256[] ids) external view returns (uint256[]) -``` - - - -*See {IERC1155-balanceOfBatch}. Requirements: - `accounts` and `ids` must have the same length.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| accounts | address[] | undefined -| ids | uint256[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256[] | undefined - -### contractType - -```solidity -function contractType() external pure returns (bytes32) -``` - - - -*Returns the type of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### contractURI - -```solidity -function contractURI() external view returns (string) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### contractVersion - -```solidity -function contractVersion() external pure returns (uint8) -``` - - - -*Returns the version of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint8 | undefined - -### createPack - -```solidity -function createPack(ITokenBundle.Token[] _contents, uint256[] _numOfRewardUnits, string _packUri, uint128 _openStartTimestamp, uint128 _amountDistributedPerOpen, address _recipient) external payable returns (uint256 packId, uint256 packTotalSupply) -``` - - - -*Creates a pack with the stated contents.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _contents | ITokenBundle.Token[] | undefined -| _numOfRewardUnits | uint256[] | undefined -| _packUri | string | undefined -| _openStartTimestamp | uint128 | undefined -| _amountDistributedPerOpen | uint128 | undefined -| _recipient | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| packId | uint256 | undefined -| packTotalSupply | uint256 | undefined - -### getDefaultRoyaltyInfo - -```solidity -function getDefaultRoyaltyInfo() external view returns (address, uint16) -``` - - - -*Returns the default royalty recipient and bps.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### getPackContents - -```solidity -function getPackContents(uint256 _packId) external view returns (struct ITokenBundle.Token[] contents, uint256[] perUnitAmounts) -``` - - - -*Returns the underlying contents of a pack.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _packId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| contents | ITokenBundle.Token[] | undefined -| perUnitAmounts | uint256[] | undefined - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {AccessControl-_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleMember - -```solidity -function getRoleMember(bytes32 role, uint256 index) external view returns (address member) -``` - - - -*Returns one of the accounts that have `role`. `index` must be a value between 0 and {getRoleMemberCount}, non-inclusive. Role bearers are not sorted in any particular way, and their ordering may change at any point. WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure you perform all queries on the same block. See the following https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] for more information.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| member | address | undefined - -### getRoleMemberCount - -```solidity -function getRoleMemberCount(bytes32 role) external view returns (uint256 count) -``` - - - -*Returns the number of accounts that have `role`. Can be used together with {getRoleMember} to enumerate all bearers of a role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| count | uint256 | undefined - -### getRoyaltyInfoForToken - -```solidity -function getRoyaltyInfoForToken(uint256 _tokenId) external view returns (address, uint16) -``` - - - -*Returns the royalty recipient and bps for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### getTokenCountOfBundle - -```solidity -function getTokenCountOfBundle(uint256 _bundleId) external view returns (uint256) -``` - - - -*Returns the total number of assets in a particular bundle.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _bundleId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getTokenOfBundle - -```solidity -function getTokenOfBundle(uint256 _bundleId, uint256 index) external view returns (struct ITokenBundle.Token) -``` - - - -*Returns an asset contained in a particular bundle, at a particular index.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _bundleId | uint256 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | ITokenBundle.Token | undefined - -### getUriOfBundle - -```solidity -function getUriOfBundle(uint256 _bundleId) external view returns (string) -``` - - - -*Returns the uri of a particular bundle.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _bundleId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### hasRoleWithSwitch - -```solidity -function hasRoleWithSwitch(bytes32 role, address account) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### initialize - -```solidity -function initialize(address _defaultAdmin, string _name, string _symbol, string _contractURI, address[] _trustedForwarders, address _royaltyRecipient, uint256 _royaltyBps) external nonpayable -``` - - - -*Initiliazes the contract, like a constructor.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _defaultAdmin | address | undefined -| _name | string | undefined -| _symbol | string | undefined -| _contractURI | string | undefined -| _trustedForwarders | address[] | undefined -| _royaltyRecipient | address | undefined -| _royaltyBps | uint256 | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address account, address operator) external view returns (bool) -``` - - - -*See {IERC1155-isApprovedForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### isTrustedForwarder - -```solidity -function isTrustedForwarder(address forwarder) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| forwarder | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### multicall - -```solidity -function multicall(bytes[] data) external nonpayable returns (bytes[] results) -``` - - - -*Receives and executes a batch of function calls on this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| results | bytes[] | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### nextTokenIdToMint - -```solidity -function nextTokenIdToMint() external view returns (uint256) -``` - - - -*The token Id of the next set of packs to be minted.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### onERC1155BatchReceived - -```solidity -function onERC1155BatchReceived(address, address, uint256[], uint256[], bytes) external nonpayable returns (bytes4) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | address | undefined -| _2 | uint256[] | undefined -| _3 | uint256[] | undefined -| _4 | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - -### onERC1155Received - -```solidity -function onERC1155Received(address, address, uint256, uint256, bytes) external nonpayable returns (bytes4) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | address | undefined -| _2 | uint256 | undefined -| _3 | uint256 | undefined -| _4 | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - -### onERC721Received - -```solidity -function onERC721Received(address, address, uint256, bytes) external nonpayable returns (bytes4) -``` - - - -*See {IERC721Receiver-onERC721Received}. Always returns `IERC721Receiver.onERC721Received.selector`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | address | undefined -| _2 | uint256 | undefined -| _3 | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - -### openPack - -```solidity -function openPack(uint256 _packId, uint256 _amountToOpen) external nonpayable returns (struct ITokenBundle.Token[]) -``` - -Lets a pack owner open packs and receive the packs' reward units. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _packId | uint256 | undefined -| _amountToOpen | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | ITokenBundle.Token[] | undefined - -### owner - -```solidity -function owner() external view returns (address) -``` - - - -*Returns the owner of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### paused - -```solidity -function paused() external view returns (bool) -``` - - - -*Returns true if the contract is paused, and false otherwise.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### royaltyInfo - -```solidity -function royaltyInfo(uint256 tokenId, uint256 salePrice) external view returns (address receiver, uint256 royaltyAmount) -``` - - - -*Returns the royalty recipient and amount, given a tokenId and sale price.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined -| salePrice | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| receiver | address | undefined -| royaltyAmount | uint256 | undefined - -### safeBatchTransferFrom - -```solidity -function safeBatchTransferFrom(address from, address to, uint256[] ids, uint256[] amounts, bytes data) external nonpayable -``` - - - -*See {IERC1155-safeBatchTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| ids | uint256[] | undefined -| amounts | uint256[] | undefined -| data | bytes | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data) external nonpayable -``` - - - -*See {IERC1155-safeTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| id | uint256 | undefined -| amount | uint256 | undefined -| data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool approved) external nonpayable -``` - - - -*See {IERC1155-setApprovalForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| approved | bool | undefined - -### setContractURI - -```solidity -function setContractURI(string _uri) external nonpayable -``` - - - -*Lets a contract admin set the URI for contract-level metadata.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _uri | string | undefined - -### setDefaultRoyaltyInfo - -```solidity -function setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) external nonpayable -``` - - - -*Lets a contract admin update the default royalty recipient and bps.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _royaltyRecipient | address | undefined -| _royaltyBps | uint256 | undefined - -### setOwner - -```solidity -function setOwner(address _newOwner) external nonpayable -``` - - - -*Lets a contract admin set a new owner for the contract. The new owner must be a contract admin.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _newOwner | address | undefined - -### setRoyaltyInfoForToken - -```solidity -function setRoyaltyInfoForToken(uint256 _tokenId, address _recipient, uint256 _bps) external nonpayable -``` - - - -*Lets a contract admin set the royalty recipient and bps for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined -| _recipient | address | undefined -| _bps | uint256 | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See ERC 165* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### totalSupply - -```solidity -function totalSupply(uint256) external view returns (uint256) -``` - - - -*Mapping from token ID => total circulating supply of token with that ID.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### uri - -```solidity -function uri(uint256 _tokenId) external view returns (string) -``` - - - -*Returns the URI for a given tokenId.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - - - -## Events - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed account, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### ContractURIUpdated - -```solidity -event ContractURIUpdated(string prevURI, string newURI) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| prevURI | string | undefined | -| newURI | string | undefined | - -### DefaultRoyalty - -```solidity -event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| newRoyaltyRecipient `indexed` | address | undefined | -| newRoyaltyBps | uint256 | undefined | - -### OwnerUpdated - -```solidity -event OwnerUpdated(address indexed prevOwner, address indexed newOwner) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| prevOwner `indexed` | address | undefined | -| newOwner `indexed` | address | undefined | - -### PackCreated - -```solidity -event PackCreated(uint256 indexed packId, address indexed packCreator, address recipient, uint256 totalPacksCreated) -``` - -Emitted when a set of packs is created. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| packId `indexed` | uint256 | undefined | -| packCreator `indexed` | address | undefined | -| recipient | address | undefined | -| totalPacksCreated | uint256 | undefined | - -### PackOpened - -```solidity -event PackOpened(uint256 indexed packId, address indexed opener, uint256 numOfPacksOpened, ITokenBundle.Token[] rewardUnitsDistributed) -``` - -Emitted when a pack is opened. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| packId `indexed` | uint256 | undefined | -| opener `indexed` | address | undefined | -| numOfPacksOpened | uint256 | undefined | -| rewardUnitsDistributed | ITokenBundle.Token[] | undefined | - -### Paused - -```solidity -event Paused(address account) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined | - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoyaltyForToken - -```solidity -event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, uint256 royaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId `indexed` | uint256 | undefined | -| royaltyRecipient `indexed` | address | undefined | -| royaltyBps | uint256 | undefined | - -### TransferBatch - -```solidity -event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| ids | uint256[] | undefined | -| values | uint256[] | undefined | - -### TransferSingle - -```solidity -event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| id | uint256 | undefined | -| value | uint256 | undefined | - -### URI - -```solidity -event URI(string value, uint256 indexed id) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| value | string | undefined | -| id `indexed` | uint256 | undefined | - -### Unpaused - -```solidity -event Unpaused(address account) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined | - - - diff --git a/docs/PausableUpgradeable.md b/docs/PausableUpgradeable.md deleted file mode 100644 index ed2f54150..000000000 --- a/docs/PausableUpgradeable.md +++ /dev/null @@ -1,67 +0,0 @@ -# PausableUpgradeable - - - - - - - -*Contract module which allows children to implement an emergency stop mechanism that can be triggered by an authorized account. This module is used through inheritance. It will make available the modifiers `whenNotPaused` and `whenPaused`, which can be applied to the functions of your contract. Note that they will not be pausable by simply including this module, only once the modifiers are put in place.* - -## Methods - -### paused - -```solidity -function paused() external view returns (bool) -``` - - - -*Returns true if the contract is paused, and false otherwise.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - -## Events - -### Paused - -```solidity -event Paused(address account) -``` - - - -*Emitted when the pause is triggered by `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined | - -### Unpaused - -```solidity -event Unpaused(address account) -``` - - - -*Emitted when the pause is lifted by `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined | - - - diff --git a/docs/PaymentSplitterUpgradeable.md b/docs/PaymentSplitterUpgradeable.md deleted file mode 100644 index 139cc7ffe..000000000 --- a/docs/PaymentSplitterUpgradeable.md +++ /dev/null @@ -1,221 +0,0 @@ -# PaymentSplitterUpgradeable - - - -> PaymentSplitter - - - -*This contract allows to split Ether payments among a group of accounts. The sender does not need to be aware that the Ether will be split in this way, since it is handled transparently by the contract. The split can be in equal parts or in any other arbitrary proportion. The way this is specified is by assigning each account to a number of shares. Of all the Ether that this contract receives, each account will then be able to claim an amount proportional to the percentage of total shares they were assigned. `PaymentSplitter` follows a _pull payment_ model. This means that payments are not automatically forwarded to the accounts but kept in this contract, and the actual transfer is triggered as a separate step by calling the {release} function. NOTE: This contract assumes that ERC20 tokens will behave similarly to native tokens (Ether). Rebasing tokens, and tokens that apply fees during transfers, are likely to not be supported as expected. If in doubt, we encourage you to run tests before sending real value to this contract.* - -## Methods - -### payee - -```solidity -function payee(uint256 index) external view returns (address) -``` - - - -*Getter for the address of the payee number `index`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### payeeCount - -```solidity -function payeeCount() external view returns (uint256) -``` - - - -*Get the number of payees* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### release - -```solidity -function release(contract IERC20Upgradeable token, address account) external nonpayable -``` - - - -*Triggers a transfer to `account` of the amount of `token` tokens they are owed, according to their percentage of the total shares and their previous withdrawals. `token` must be the address of an IERC20 contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| token | contract IERC20Upgradeable | undefined -| account | address | undefined - -### released - -```solidity -function released(address account) external view returns (uint256) -``` - - - -*Getter for the amount of `token` tokens already released to a payee. `token` should be the address of an IERC20 contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### shares - -```solidity -function shares(address account) external view returns (uint256) -``` - - - -*Getter for the amount of shares held by an account.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### totalReleased - -```solidity -function totalReleased() external view returns (uint256) -``` - - - -*Getter for the total amount of `token` already released. `token` should be the address of an IERC20 contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### totalShares - -```solidity -function totalShares() external view returns (uint256) -``` - - - -*Getter for the total shares held by payees.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - - - -## Events - -### ERC20PaymentReleased - -```solidity -event ERC20PaymentReleased(contract IERC20Upgradeable indexed token, address to, uint256 amount) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| token `indexed` | contract IERC20Upgradeable | undefined | -| to | address | undefined | -| amount | uint256 | undefined | - -### PayeeAdded - -```solidity -event PayeeAdded(address account, uint256 shares) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined | -| shares | uint256 | undefined | - -### PaymentReceived - -```solidity -event PaymentReceived(address from, uint256 amount) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined | -| amount | uint256 | undefined | - -### PaymentReleased - -```solidity -event PaymentReleased(address to, uint256 amount) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined | -| amount | uint256 | undefined | - - - diff --git a/docs/Permissions.md b/docs/Permissions.md deleted file mode 100644 index 56b39fc63..000000000 --- a/docs/Permissions.md +++ /dev/null @@ -1,208 +0,0 @@ -# Permissions - - - - - - - - - -## Methods - -### DEFAULT_ADMIN_ROLE - -```solidity -function DEFAULT_ADMIN_ROLE() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {AccessControl-_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### hasRoleWithSwitch - -```solidity -function hasRoleWithSwitch(bytes32 role, address account) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - - - -## Events - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - - - diff --git a/docs/PermissionsEnumerable.md b/docs/PermissionsEnumerable.md deleted file mode 100644 index e7c2ccd45..000000000 --- a/docs/PermissionsEnumerable.md +++ /dev/null @@ -1,253 +0,0 @@ -# PermissionsEnumerable - - - - - - - - - -## Methods - -### DEFAULT_ADMIN_ROLE - -```solidity -function DEFAULT_ADMIN_ROLE() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {AccessControl-_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleMember - -```solidity -function getRoleMember(bytes32 role, uint256 index) external view returns (address member) -``` - - - -*Returns one of the accounts that have `role`. `index` must be a value between 0 and {getRoleMemberCount}, non-inclusive. Role bearers are not sorted in any particular way, and their ordering may change at any point. WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure you perform all queries on the same block. See the following https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] for more information.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| member | address | undefined - -### getRoleMemberCount - -```solidity -function getRoleMemberCount(bytes32 role) external view returns (uint256 count) -``` - - - -*Returns the number of accounts that have `role`. Can be used together with {getRoleMember} to enumerate all bearers of a role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| count | uint256 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### hasRoleWithSwitch - -```solidity -function hasRoleWithSwitch(bytes32 role, address account) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - - - -## Events - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - - - diff --git a/docs/PlatformFee.md b/docs/PlatformFee.md deleted file mode 100644 index d37375d4b..000000000 --- a/docs/PlatformFee.md +++ /dev/null @@ -1,70 +0,0 @@ -# PlatformFee - - - - - -Thirdweb's `PlatformFee` is a contract extension to be used with any base contract. It exposes functions for setting and reading the recipient of platform fee and the platform fee basis points, and lets the inheriting contract perform conditional logic that uses information about platform fees, if desired. - - - -## Methods - -### getPlatformFeeInfo - -```solidity -function getPlatformFeeInfo() external view returns (address, uint16) -``` - - - -*Returns the platform fee recipient and bps.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### setPlatformFeeInfo - -```solidity -function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) external nonpayable -``` - - - -*Lets a contract admin update the platform fee recipient and bps* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _platformFeeRecipient | address | undefined -| _platformFeeBps | uint256 | undefined - - - -## Events - -### PlatformFeeInfoUpdated - -```solidity -event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| platformFeeRecipient `indexed` | address | undefined | -| platformFeeBps | uint256 | undefined | - - - diff --git a/docs/PrimarySale.md b/docs/PrimarySale.md deleted file mode 100644 index 81dce119a..000000000 --- a/docs/PrimarySale.md +++ /dev/null @@ -1,67 +0,0 @@ -# PrimarySale - - - - - -Thirdweb's `PrimarySale` is a contract extension to be used with any base contract. It exposes functions for setting and reading the recipient of primary sales, and lets the inheriting contract perform conditional logic that uses information about primary sales, if desired. - - - -## Methods - -### primarySaleRecipient - -```solidity -function primarySaleRecipient() external view returns (address) -``` - - - -*The adress that receives all primary sales value.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### setPrimarySaleRecipient - -```solidity -function setPrimarySaleRecipient(address _saleRecipient) external nonpayable -``` - - - -*Lets a contract admin set the recipient for all primary sales.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _saleRecipient | address | undefined - - - -## Events - -### PrimarySaleRecipientUpdated - -```solidity -event PrimarySaleRecipientUpdated(address indexed recipient) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| recipient `indexed` | address | undefined | - - - diff --git a/docs/Proxy.md b/docs/Proxy.md deleted file mode 100644 index 30095d23a..000000000 --- a/docs/Proxy.md +++ /dev/null @@ -1,12 +0,0 @@ -# Proxy - - - - - - - -*This abstract contract provides a fallback function that delegates all calls to another contract using the EVM instruction `delegatecall`. We refer to the second contract as the _implementation_ behind the proxy, and it has to be specified by overriding the virtual {_implementation} function. Additionally, delegation to the implementation can be triggered manually through the {_fallback} function, or to a different contract through the {_delegate} function. The success and return data of the delegated call will be returned back to the caller of the proxy.* - - - diff --git a/docs/ReentrancyGuardUpgradeable.md b/docs/ReentrancyGuardUpgradeable.md deleted file mode 100644 index 6955ca2de..000000000 --- a/docs/ReentrancyGuardUpgradeable.md +++ /dev/null @@ -1,12 +0,0 @@ -# ReentrancyGuardUpgradeable - - - - - - - -*Contract module that helps prevent reentrant calls to a function. Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier available, which can be applied to functions to make sure there are no nested (reentrant) calls to them. Note that because there is a single `nonReentrant` guard, functions marked as `nonReentrant` may not call one another. This can be worked around by making those functions `private`, and then adding `external` `nonReentrant` entry points to them. TIP: If you would like to learn more about reentrancy and alternative ways to protect against it, check out our blog post https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].* - - - diff --git a/docs/Royalty.md b/docs/Royalty.md deleted file mode 100644 index b81311b63..000000000 --- a/docs/Royalty.md +++ /dev/null @@ -1,175 +0,0 @@ -# Royalty - - - - - -Thirdweb's `Royalty` is a contract extension to be used with any base contract. It exposes functions for setting and reading the recipient of royalty fee and the royalty fee basis points, and lets the inheriting contract perform conditional logic that uses information about royalty fees, if desired. The `Royalty` contract is ERC2981 compliant. - - - -## Methods - -### getDefaultRoyaltyInfo - -```solidity -function getDefaultRoyaltyInfo() external view returns (address, uint16) -``` - - - -*Returns the default royalty recipient and bps.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### getRoyaltyInfoForToken - -```solidity -function getRoyaltyInfoForToken(uint256 _tokenId) external view returns (address, uint16) -``` - - - -*Returns the royalty recipient and bps for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### royaltyInfo - -```solidity -function royaltyInfo(uint256 tokenId, uint256 salePrice) external view returns (address receiver, uint256 royaltyAmount) -``` - - - -*Returns the royalty recipient and amount, given a tokenId and sale price.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined -| salePrice | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| receiver | address | undefined -| royaltyAmount | uint256 | undefined - -### setDefaultRoyaltyInfo - -```solidity -function setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) external nonpayable -``` - - - -*Lets a contract admin update the default royalty recipient and bps.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _royaltyRecipient | address | undefined -| _royaltyBps | uint256 | undefined - -### setRoyaltyInfoForToken - -```solidity -function setRoyaltyInfoForToken(uint256 _tokenId, address _recipient, uint256 _bps) external nonpayable -``` - - - -*Lets a contract admin set the royalty recipient and bps for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined -| _recipient | address | undefined -| _bps | uint256 | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - -## Events - -### DefaultRoyalty - -```solidity -event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| newRoyaltyRecipient `indexed` | address | undefined | -| newRoyaltyBps | uint256 | undefined | - -### RoyaltyForToken - -```solidity -event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, uint256 royaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId `indexed` | uint256 | undefined | -| royaltyRecipient `indexed` | address | undefined | -| royaltyBps | uint256 | undefined | - - - diff --git a/docs/SafeCastUpgradeable.md b/docs/SafeCastUpgradeable.md deleted file mode 100644 index c09bb4d09..000000000 --- a/docs/SafeCastUpgradeable.md +++ /dev/null @@ -1,12 +0,0 @@ -# SafeCastUpgradeable - - - - - - - -*Wrappers over Solidity's uintXX/intXX casting operators with added overflow checks. Downcasting from uint256/int256 in Solidity does not revert on overflow. This can easily result in undesired exploitation or bugs, since developers usually assume that overflows raise errors. `SafeCast` restores this intuition by reverting the transaction when such an operation overflows. Using this library instead of the unchecked operations eliminates an entire class of bugs, so it's recommended to use it always. Can be combined with {SafeMath} and {SignedSafeMath} to extend it to smaller types, by performing all math on `uint256` and `int256` and then downcasting.* - - - diff --git a/docs/SafeERC20.md b/docs/SafeERC20.md deleted file mode 100644 index 80367d00d..000000000 --- a/docs/SafeERC20.md +++ /dev/null @@ -1,12 +0,0 @@ -# SafeERC20 - - - -> SafeERC20 - - - -*Wrappers around ERC20 operations that throw on failure (when the token contract returns false). Tokens that return no value (and instead revert or throw on failure) are also supported, non-reverting calls are assumed to be successful. To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract, which allows you to call the safe operations as `token.safeTransfer(...)`, etc.* - - - diff --git a/docs/SafeERC20Upgradeable.md b/docs/SafeERC20Upgradeable.md deleted file mode 100644 index 77a1b1229..000000000 --- a/docs/SafeERC20Upgradeable.md +++ /dev/null @@ -1,12 +0,0 @@ -# SafeERC20Upgradeable - - - -> SafeERC20 - - - -*Wrappers around ERC20 operations that throw on failure (when the token contract returns false). Tokens that return no value (and instead revert or throw on failure) are also supported, non-reverting calls are assumed to be successful. To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract, which allows you to call the safe operations as `token.safeTransfer(...)`, etc.* - - - diff --git a/docs/SignatureDrop.md b/docs/SignatureDrop.md deleted file mode 100644 index 9cfe9c56f..000000000 --- a/docs/SignatureDrop.md +++ /dev/null @@ -1,1641 +0,0 @@ -# SignatureDrop - - - - - - - - - -## Methods - -### DEFAULT_ADMIN_ROLE - -```solidity -function DEFAULT_ADMIN_ROLE() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### approve - -```solidity -function approve(address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-approve}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| tokenId | uint256 | undefined - -### balanceOf - -```solidity -function balanceOf(address owner) external view returns (uint256) -``` - - - -*See {IERC721-balanceOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### burn - -```solidity -function burn(uint256 tokenId) external nonpayable -``` - - - -*Burns `tokenId`. See {ERC721-_burn}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -### claim - -```solidity -function claim(address _receiver, uint256 _quantity, address _currency, uint256 _pricePerToken, IDropSinglePhase.AllowlistProof _allowlistProof, bytes _data) external payable -``` - - - -*Lets an account claim tokens.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _receiver | address | undefined -| _quantity | uint256 | undefined -| _currency | address | undefined -| _pricePerToken | uint256 | undefined -| _allowlistProof | IDropSinglePhase.AllowlistProof | undefined -| _data | bytes | undefined - -### claimCondition - -```solidity -function claimCondition() external view returns (uint256 startTimestamp, uint256 maxClaimableSupply, uint256 supplyClaimed, uint256 quantityLimitPerTransaction, uint256 waitTimeInSecondsBetweenClaims, bytes32 merkleRoot, uint256 pricePerToken, address currency) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| startTimestamp | uint256 | undefined -| maxClaimableSupply | uint256 | undefined -| supplyClaimed | uint256 | undefined -| quantityLimitPerTransaction | uint256 | undefined -| waitTimeInSecondsBetweenClaims | uint256 | undefined -| merkleRoot | bytes32 | undefined -| pricePerToken | uint256 | undefined -| currency | address | undefined - -### contractType - -```solidity -function contractType() external pure returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### contractURI - -```solidity -function contractURI() external view returns (string) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### contractVersion - -```solidity -function contractVersion() external pure returns (uint256) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### encryptDecrypt - -```solidity -function encryptDecrypt(bytes data, bytes key) external pure returns (bytes result) -``` - - - -*See: https://ethereum.stackexchange.com/questions/69825/decrypt-message-on-chain* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes | undefined -| key | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| result | bytes | undefined - -### encryptedBaseURI - -```solidity -function encryptedBaseURI(uint256) external view returns (bytes) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes | undefined - -### getApproved - -```solidity -function getApproved(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-getApproved}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getBaseURICount - -```solidity -function getBaseURICount() external view returns (uint256) -``` - - - -*Returns the number of batches of tokens having the same baseURI.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getBatchIdAtIndex - -```solidity -function getBatchIdAtIndex(uint256 _index) external view returns (uint256) -``` - - - -*Returns the id for the batch of tokens the given tokenId belongs to.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getClaimTimestamp - -```solidity -function getClaimTimestamp(address _claimer) external view returns (uint256 lastClaimedAt, uint256 nextValidClaimTimestamp) -``` - - - -*Returns the timestamp for when a claimer is eligible for claiming NFTs again.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _claimer | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| lastClaimedAt | uint256 | undefined -| nextValidClaimTimestamp | uint256 | undefined - -### getDefaultRoyaltyInfo - -```solidity -function getDefaultRoyaltyInfo() external view returns (address, uint16) -``` - - - -*Returns the default royalty recipient and bps.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### getPlatformFeeInfo - -```solidity -function getPlatformFeeInfo() external view returns (address, uint16) -``` - - - -*Returns the platform fee recipient and bps.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### getRevealURI - -```solidity -function getRevealURI(uint256 _batchId, bytes _key) external view returns (string revealedURI) -``` - - - -*Returns the decrypted i.e. revealed URI for a batch of tokens.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _batchId | uint256 | undefined -| _key | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| revealedURI | string | undefined - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {AccessControl-_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleMember - -```solidity -function getRoleMember(bytes32 role, uint256 index) external view returns (address member) -``` - - - -*Returns one of the accounts that have `role`. `index` must be a value between 0 and {getRoleMemberCount}, non-inclusive. Role bearers are not sorted in any particular way, and their ordering may change at any point. WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure you perform all queries on the same block. See the following https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] for more information.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| member | address | undefined - -### getRoleMemberCount - -```solidity -function getRoleMemberCount(bytes32 role) external view returns (uint256 count) -``` - - - -*Returns the number of accounts that have `role`. Can be used together with {getRoleMember} to enumerate all bearers of a role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| count | uint256 | undefined - -### getRoyaltyInfoForToken - -```solidity -function getRoyaltyInfoForToken(uint256 _tokenId) external view returns (address, uint16) -``` - - - -*Returns the royalty recipient and bps for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### hasRoleWithSwitch - -```solidity -function hasRoleWithSwitch(bytes32 role, address account) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### initialize - -```solidity -function initialize(address _defaultAdmin, string _name, string _symbol, string _contractURI, address[] _trustedForwarders, address _saleRecipient, address _royaltyRecipient, uint128 _royaltyBps, uint128 _platformFeeBps, address _platformFeeRecipient) external nonpayable -``` - - - -*Initiliazes the contract, like a constructor.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _defaultAdmin | address | undefined -| _name | string | undefined -| _symbol | string | undefined -| _contractURI | string | undefined -| _trustedForwarders | address[] | undefined -| _saleRecipient | address | undefined -| _royaltyRecipient | address | undefined -| _royaltyBps | uint128 | undefined -| _platformFeeBps | uint128 | undefined -| _platformFeeRecipient | address | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address owner, address operator) external view returns (bool) -``` - - - -*See {IERC721-isApprovedForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### isEncryptedBatch - -```solidity -function isEncryptedBatch(uint256 _batchId) external view returns (bool) -``` - - - -*Returns whether the relvant batch of NFTs is subject to a delayed reveal.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _batchId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### isTrustedForwarder - -```solidity -function isTrustedForwarder(address forwarder) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| forwarder | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### lazyMint - -```solidity -function lazyMint(uint256 _amount, string _baseURIForTokens, bytes _encryptedBaseURI) external nonpayable returns (uint256 batchId) -``` - - - -*Lets an account with `MINTER_ROLE` lazy mint 'n' NFTs. The URIs for each token is the provided `_baseURIForTokens` + `{tokenId}`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _amount | uint256 | undefined -| _baseURIForTokens | string | undefined -| _encryptedBaseURI | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| batchId | uint256 | undefined - -### mintWithSignature - -```solidity -function mintWithSignature(ISignatureMintERC721.MintRequest _req, bytes _signature) external payable returns (address signer) -``` - - - -*Claim lazy minted tokens via signature.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _req | ISignatureMintERC721.MintRequest | undefined -| _signature | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| signer | address | undefined - -### multicall - -```solidity -function multicall(bytes[] data) external nonpayable returns (bytes[] results) -``` - - - -*Receives and executes a batch of function calls on this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| results | bytes[] | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*See {IERC721Metadata-name}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### nextTokenIdToMint - -```solidity -function nextTokenIdToMint() external view returns (uint256) -``` - - - -*The tokenId of the next NFT that will be minted / lazy minted.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### owner - -```solidity -function owner() external view returns (address) -``` - - - -*Returns the owner of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### ownerOf - -```solidity -function ownerOf(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-ownerOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### primarySaleRecipient - -```solidity -function primarySaleRecipient() external view returns (address) -``` - - - -*The adress that receives all primary sales value.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### reveal - -```solidity -function reveal(uint256 _index, bytes _key) external nonpayable returns (string revealedURI) -``` - - - -*Lets an account with `MINTER_ROLE` reveal the URI for a batch of 'delayed-reveal' NFTs.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _index | uint256 | undefined -| _key | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| revealedURI | string | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### royaltyInfo - -```solidity -function royaltyInfo(uint256 tokenId, uint256 salePrice) external view returns (address receiver, uint256 royaltyAmount) -``` - - - -*Returns the royalty recipient and amount, given a tokenId and sale price.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined -| salePrice | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| receiver | address | undefined -| royaltyAmount | uint256 | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 tokenId, bytes _data) external nonpayable -``` - - - -*See {IERC721-safeTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined -| _data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool approved) external nonpayable -``` - - - -*See {IERC721-setApprovalForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| approved | bool | undefined - -### setClaimConditions - -```solidity -function setClaimConditions(IClaimCondition.ClaimCondition _condition, bool _resetClaimEligibility) external nonpayable -``` - - - -*Lets a contract admin set claim conditions.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _condition | IClaimCondition.ClaimCondition | undefined -| _resetClaimEligibility | bool | undefined - -### setContractURI - -```solidity -function setContractURI(string _uri) external nonpayable -``` - - - -*Lets a contract admin set the URI for contract-level metadata.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _uri | string | undefined - -### setDefaultRoyaltyInfo - -```solidity -function setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) external nonpayable -``` - - - -*Lets a contract admin update the default royalty recipient and bps.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _royaltyRecipient | address | undefined -| _royaltyBps | uint256 | undefined - -### setOwner - -```solidity -function setOwner(address _newOwner) external nonpayable -``` - - - -*Lets a contract admin set a new owner for the contract. The new owner must be a contract admin.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _newOwner | address | undefined - -### setPlatformFeeInfo - -```solidity -function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) external nonpayable -``` - - - -*Lets a contract admin update the platform fee recipient and bps* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _platformFeeRecipient | address | undefined -| _platformFeeBps | uint256 | undefined - -### setPrimarySaleRecipient - -```solidity -function setPrimarySaleRecipient(address _saleRecipient) external nonpayable -``` - - - -*Lets a contract admin set the recipient for all primary sales.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _saleRecipient | address | undefined - -### setRoyaltyInfoForToken - -```solidity -function setRoyaltyInfoForToken(uint256 _tokenId, address _recipient, uint256 _bps) external nonpayable -``` - - - -*Lets a contract admin set the royalty recipient and bps for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined -| _recipient | address | undefined -| _bps | uint256 | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See ERC 165* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - -*See {IERC721Metadata-symbol}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### tokenURI - -```solidity -function tokenURI(uint256 _tokenId) external view returns (string) -``` - - - -*Returns the URI for a given tokenId.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### totalMinted - -```solidity -function totalMinted() external view returns (uint256) -``` - -Returns the total amount of tokens minted in the contract. - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - -*Burned tokens are calculated here, use _totalMinted() if you want to count just minted tokens.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-transferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined - -### verify - -```solidity -function verify(ISignatureMintERC721.MintRequest _req, bytes _signature) external view returns (bool success, address signer) -``` - - - -*Verifies that a mint request is signed by an account holding MINTER_ROLE (at the time of the function call).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _req | ISignatureMintERC721.MintRequest | undefined -| _signature | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| success | bool | undefined -| signer | address | undefined - -### verifyClaim - -```solidity -function verifyClaim(address _claimer, uint256 _quantity, address _currency, uint256 _pricePerToken, bool verifyMaxQuantityPerTransaction) external view -``` - - - -*Checks a request to claim NFTs against the active claim condition's criteria.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _claimer | address | undefined -| _quantity | uint256 | undefined -| _currency | address | undefined -| _pricePerToken | uint256 | undefined -| verifyMaxQuantityPerTransaction | bool | undefined - -### verifyClaimMerkleProof - -```solidity -function verifyClaimMerkleProof(address _claimer, uint256 _quantity, IDropSinglePhase.AllowlistProof _allowlistProof) external view returns (bool validMerkleProof, uint256 merkleProofIndex) -``` - - - -*Checks whether a claimer meets the claim condition's allowlist criteria.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _claimer | address | undefined -| _quantity | uint256 | undefined -| _allowlistProof | IDropSinglePhase.AllowlistProof | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| validMerkleProof | bool | undefined -| merkleProofIndex | uint256 | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| approved `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed owner, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### ClaimConditionUpdated - -```solidity -event ClaimConditionUpdated(IClaimCondition.ClaimCondition condition, bool resetEligibility) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| condition | IClaimCondition.ClaimCondition | undefined | -| resetEligibility | bool | undefined | - -### ContractURIUpdated - -```solidity -event ContractURIUpdated(string prevURI, string newURI) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| prevURI | string | undefined | -| newURI | string | undefined | - -### DefaultRoyalty - -```solidity -event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| newRoyaltyRecipient `indexed` | address | undefined | -| newRoyaltyBps | uint256 | undefined | - -### OwnerUpdated - -```solidity -event OwnerUpdated(address indexed prevOwner, address indexed newOwner) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| prevOwner `indexed` | address | undefined | -| newOwner `indexed` | address | undefined | - -### PlatformFeeInfoUpdated - -```solidity -event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| platformFeeRecipient `indexed` | address | undefined | -| platformFeeBps | uint256 | undefined | - -### PrimarySaleRecipientUpdated - -```solidity -event PrimarySaleRecipientUpdated(address indexed recipient) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| recipient `indexed` | address | undefined | - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoyaltyForToken - -```solidity -event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, uint256 royaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId `indexed` | uint256 | undefined | -| royaltyRecipient `indexed` | address | undefined | -| royaltyBps | uint256 | undefined | - -### TokenURIRevealed - -```solidity -event TokenURIRevealed(uint256 indexed index, string revealedURI) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| index `indexed` | uint256 | undefined | -| revealedURI | string | undefined | - -### TokensClaimed - -```solidity -event TokensClaimed(address indexed claimer, address indexed receiver, uint256 indexed startTokenId, uint256 quantityClaimed) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| claimer `indexed` | address | undefined | -| receiver `indexed` | address | undefined | -| startTokenId `indexed` | uint256 | undefined | -| quantityClaimed | uint256 | undefined | - -### TokensLazyMinted - -```solidity -event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| startTokenId `indexed` | uint256 | undefined | -| endTokenId | uint256 | undefined | -| baseURI | string | undefined | -| encryptedBaseURI | bytes | undefined | - -### TokensMintedWithSignature - -```solidity -event TokensMintedWithSignature(address indexed signer, address indexed mintedTo, uint256 indexed tokenIdMinted, ISignatureMintERC721.MintRequest mintRequest) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| signer `indexed` | address | undefined | -| mintedTo `indexed` | address | undefined | -| tokenIdMinted `indexed` | uint256 | undefined | -| mintRequest | ISignatureMintERC721.MintRequest | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - - - -## Errors - -### ApprovalCallerNotOwnerNorApproved - -```solidity -error ApprovalCallerNotOwnerNorApproved() -``` - -The caller must own the token or be an approved operator. - - - - -### ApprovalQueryForNonexistentToken - -```solidity -error ApprovalQueryForNonexistentToken() -``` - -The token does not exist. - - - - -### ApprovalToCurrentOwner - -```solidity -error ApprovalToCurrentOwner() -``` - -The caller cannot approve to the current owner. - - - - -### ApproveToCaller - -```solidity -error ApproveToCaller() -``` - -The caller cannot approve to their own address. - - - - -### BalanceQueryForZeroAddress - -```solidity -error BalanceQueryForZeroAddress() -``` - -Cannot query the balance for the zero address. - - - - -### MintToZeroAddress - -```solidity -error MintToZeroAddress() -``` - -Cannot mint to the zero address. - - - - -### MintZeroQuantity - -```solidity -error MintZeroQuantity() -``` - -The quantity of tokens minted must be more than zero. - - - - -### OwnerQueryForNonexistentToken - -```solidity -error OwnerQueryForNonexistentToken() -``` - -The token does not exist. - - - - -### TransferCallerNotOwnerNorApproved - -```solidity -error TransferCallerNotOwnerNorApproved() -``` - -The caller must own the token or be an approved operator. - - - - -### TransferFromIncorrectOwner - -```solidity -error TransferFromIncorrectOwner() -``` - -The token must be owned by `from`. - - - - -### TransferToNonERC721ReceiverImplementer - -```solidity -error TransferToNonERC721ReceiverImplementer() -``` - -Cannot safely transfer to a contract that does not implement the ERC721Receiver interface. - - - - -### TransferToZeroAddress - -```solidity -error TransferToZeroAddress() -``` - -Cannot transfer to the zero address. - - - - -### URIQueryForNonexistentToken - -```solidity -error URIQueryForNonexistentToken() -``` - -The token does not exist. - - - - - diff --git a/docs/SignatureMintERC1155.md b/docs/SignatureMintERC1155.md deleted file mode 100644 index 5ce84876c..000000000 --- a/docs/SignatureMintERC1155.md +++ /dev/null @@ -1,84 +0,0 @@ -# SignatureMintERC1155 - - - - - - - - - -## Methods - -### mintWithSignature - -```solidity -function mintWithSignature(ISignatureMintERC1155.MintRequest req, bytes signature) external payable returns (address signer) -``` - -Mints tokens according to the provided mint request. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| req | ISignatureMintERC1155.MintRequest | The payload / mint request. -| signature | bytes | The signature produced by an account signing the mint request. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| signer | address | undefined - -### verify - -```solidity -function verify(ISignatureMintERC1155.MintRequest _req, bytes _signature) external view returns (bool success, address signer) -``` - - - -*Verifies that a mint request is signed by an account holding MINTER_ROLE (at the time of the function call).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _req | ISignatureMintERC1155.MintRequest | undefined -| _signature | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| success | bool | undefined -| signer | address | undefined - - - -## Events - -### TokensMintedWithSignature - -```solidity -event TokensMintedWithSignature(address indexed signer, address indexed mintedTo, uint256 indexed tokenIdMinted, ISignatureMintERC1155.MintRequest mintRequest) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| signer `indexed` | address | undefined | -| mintedTo `indexed` | address | undefined | -| tokenIdMinted `indexed` | uint256 | undefined | -| mintRequest | ISignatureMintERC1155.MintRequest | undefined | - - - diff --git a/docs/SignatureMintERC1155Upgradeable.md b/docs/SignatureMintERC1155Upgradeable.md deleted file mode 100644 index af95852c0..000000000 --- a/docs/SignatureMintERC1155Upgradeable.md +++ /dev/null @@ -1,84 +0,0 @@ -# SignatureMintERC1155Upgradeable - - - - - - - - - -## Methods - -### mintWithSignature - -```solidity -function mintWithSignature(ISignatureMintERC1155.MintRequest req, bytes signature) external payable returns (address signer) -``` - -Mints tokens according to the provided mint request. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| req | ISignatureMintERC1155.MintRequest | The payload / mint request. -| signature | bytes | The signature produced by an account signing the mint request. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| signer | address | undefined - -### verify - -```solidity -function verify(ISignatureMintERC1155.MintRequest _req, bytes _signature) external view returns (bool success, address signer) -``` - - - -*Verifies that a mint request is signed by an account holding MINTER_ROLE (at the time of the function call).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _req | ISignatureMintERC1155.MintRequest | undefined -| _signature | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| success | bool | undefined -| signer | address | undefined - - - -## Events - -### TokensMintedWithSignature - -```solidity -event TokensMintedWithSignature(address indexed signer, address indexed mintedTo, uint256 indexed tokenIdMinted, ISignatureMintERC1155.MintRequest mintRequest) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| signer `indexed` | address | undefined | -| mintedTo `indexed` | address | undefined | -| tokenIdMinted `indexed` | uint256 | undefined | -| mintRequest | ISignatureMintERC1155.MintRequest | undefined | - - - diff --git a/docs/SignatureMintERC20.md b/docs/SignatureMintERC20.md deleted file mode 100644 index 305495db8..000000000 --- a/docs/SignatureMintERC20.md +++ /dev/null @@ -1,83 +0,0 @@ -# SignatureMintERC20 - - - - - - - - - -## Methods - -### mintWithSignature - -```solidity -function mintWithSignature(ISignatureMintERC20.MintRequest req, bytes signature) external payable returns (address signer) -``` - -Mints tokens according to the provided mint request. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| req | ISignatureMintERC20.MintRequest | The payload / mint request. -| signature | bytes | The signature produced by an account signing the mint request. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| signer | address | undefined - -### verify - -```solidity -function verify(ISignatureMintERC20.MintRequest _req, bytes _signature) external view returns (bool success, address signer) -``` - - - -*Verifies that a mint request is signed by an account holding MINTER_ROLE (at the time of the function call).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _req | ISignatureMintERC20.MintRequest | undefined -| _signature | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| success | bool | undefined -| signer | address | undefined - - - -## Events - -### TokensMintedWithSignature - -```solidity -event TokensMintedWithSignature(address indexed signer, address indexed mintedTo, ISignatureMintERC20.MintRequest mintRequest) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| signer `indexed` | address | undefined | -| mintedTo `indexed` | address | undefined | -| mintRequest | ISignatureMintERC20.MintRequest | undefined | - - - diff --git a/docs/SignatureMintERC20Upgradeable.md b/docs/SignatureMintERC20Upgradeable.md deleted file mode 100644 index f95d84e83..000000000 --- a/docs/SignatureMintERC20Upgradeable.md +++ /dev/null @@ -1,83 +0,0 @@ -# SignatureMintERC20Upgradeable - - - - - - - - - -## Methods - -### mintWithSignature - -```solidity -function mintWithSignature(ISignatureMintERC20.MintRequest req, bytes signature) external payable returns (address signer) -``` - -Mints tokens according to the provided mint request. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| req | ISignatureMintERC20.MintRequest | The payload / mint request. -| signature | bytes | The signature produced by an account signing the mint request. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| signer | address | undefined - -### verify - -```solidity -function verify(ISignatureMintERC20.MintRequest _req, bytes _signature) external view returns (bool success, address signer) -``` - - - -*Verifies that a mint request is signed by an account holding MINTER_ROLE (at the time of the function call).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _req | ISignatureMintERC20.MintRequest | undefined -| _signature | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| success | bool | undefined -| signer | address | undefined - - - -## Events - -### TokensMintedWithSignature - -```solidity -event TokensMintedWithSignature(address indexed signer, address indexed mintedTo, ISignatureMintERC20.MintRequest mintRequest) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| signer `indexed` | address | undefined | -| mintedTo `indexed` | address | undefined | -| mintRequest | ISignatureMintERC20.MintRequest | undefined | - - - diff --git a/docs/SignatureMintERC721.md b/docs/SignatureMintERC721.md deleted file mode 100644 index cdaf51432..000000000 --- a/docs/SignatureMintERC721.md +++ /dev/null @@ -1,84 +0,0 @@ -# SignatureMintERC721 - - - - - - - - - -## Methods - -### mintWithSignature - -```solidity -function mintWithSignature(ISignatureMintERC721.MintRequest req, bytes signature) external payable returns (address signer) -``` - -Mints tokens according to the provided mint request. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| req | ISignatureMintERC721.MintRequest | The payload / mint request. -| signature | bytes | The signature produced by an account signing the mint request. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| signer | address | undefined - -### verify - -```solidity -function verify(ISignatureMintERC721.MintRequest _req, bytes _signature) external view returns (bool success, address signer) -``` - - - -*Verifies that a mint request is signed by an authorized account.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _req | ISignatureMintERC721.MintRequest | undefined -| _signature | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| success | bool | undefined -| signer | address | undefined - - - -## Events - -### TokensMintedWithSignature - -```solidity -event TokensMintedWithSignature(address indexed signer, address indexed mintedTo, uint256 indexed tokenIdMinted, ISignatureMintERC721.MintRequest mintRequest) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| signer `indexed` | address | undefined | -| mintedTo `indexed` | address | undefined | -| tokenIdMinted `indexed` | uint256 | undefined | -| mintRequest | ISignatureMintERC721.MintRequest | undefined | - - - diff --git a/docs/SignatureMintERC721Upgradeable.md b/docs/SignatureMintERC721Upgradeable.md deleted file mode 100644 index 8c7dc170d..000000000 --- a/docs/SignatureMintERC721Upgradeable.md +++ /dev/null @@ -1,84 +0,0 @@ -# SignatureMintERC721Upgradeable - - - - - - - - - -## Methods - -### mintWithSignature - -```solidity -function mintWithSignature(ISignatureMintERC721.MintRequest req, bytes signature) external payable returns (address signer) -``` - -Mints tokens according to the provided mint request. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| req | ISignatureMintERC721.MintRequest | The payload / mint request. -| signature | bytes | The signature produced by an account signing the mint request. - -#### Returns - -| Name | Type | Description | -|---|---|---| -| signer | address | undefined - -### verify - -```solidity -function verify(ISignatureMintERC721.MintRequest _req, bytes _signature) external view returns (bool success, address signer) -``` - - - -*Verifies that a mint request is signed by an account holding MINTER_ROLE (at the time of the function call).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _req | ISignatureMintERC721.MintRequest | undefined -| _signature | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| success | bool | undefined -| signer | address | undefined - - - -## Events - -### TokensMintedWithSignature - -```solidity -event TokensMintedWithSignature(address indexed signer, address indexed mintedTo, uint256 indexed tokenIdMinted, ISignatureMintERC721.MintRequest mintRequest) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| signer `indexed` | address | undefined | -| mintedTo `indexed` | address | undefined | -| tokenIdMinted `indexed` | uint256 | undefined | -| mintRequest | ISignatureMintERC721.MintRequest | undefined | - - - diff --git a/docs/Split.md b/docs/Split.md deleted file mode 100644 index 7560d78ad..000000000 --- a/docs/Split.md +++ /dev/null @@ -1,614 +0,0 @@ -# Split - - - - - - - - - -## Methods - -### DEFAULT_ADMIN_ROLE - -```solidity -function DEFAULT_ADMIN_ROLE() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### contractType - -```solidity -function contractType() external pure returns (bytes32) -``` - - - -*Returns the module type of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### contractURI - -```solidity -function contractURI() external view returns (string) -``` - - - -*Contract level metadata.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### contractVersion - -```solidity -function contractVersion() external pure returns (uint8) -``` - - - -*Returns the version of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint8 | undefined - -### distribute - -```solidity -function distribute() external nonpayable -``` - - - -*Release owed amount of the `token` to all of the payees.* - - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleMember - -```solidity -function getRoleMember(bytes32 role, uint256 index) external view returns (address) -``` - - - -*Returns one of the accounts that have `role`. `index` must be a value between 0 and {getRoleMemberCount}, non-inclusive. Role bearers are not sorted in any particular way, and their ordering may change at any point. WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure you perform all queries on the same block. See the following https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] for more information.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getRoleMemberCount - -```solidity -function getRoleMemberCount(bytes32 role) external view returns (uint256) -``` - - - -*Returns the number of accounts that have `role`. Can be used together with {getRoleMember} to enumerate all bearers of a role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### initialize - -```solidity -function initialize(address _defaultAdmin, string _contractURI, address[] _trustedForwarders, address[] _payees, uint256[] _shares) external nonpayable -``` - - - -*Performs the job of the constructor.shares_ are scaled by 10,000 to prevent precision loss when including fees* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _defaultAdmin | address | undefined -| _contractURI | string | undefined -| _trustedForwarders | address[] | undefined -| _payees | address[] | undefined -| _shares | uint256[] | undefined - -### isTrustedForwarder - -```solidity -function isTrustedForwarder(address forwarder) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| forwarder | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### multicall - -```solidity -function multicall(bytes[] data) external nonpayable returns (bytes[] results) -``` - - - -*Receives and executes a batch of function calls on this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| results | bytes[] | undefined - -### payee - -```solidity -function payee(uint256 index) external view returns (address) -``` - - - -*Getter for the address of the payee number `index`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### payeeCount - -```solidity -function payeeCount() external view returns (uint256) -``` - - - -*Get the number of payees* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### release - -```solidity -function release(contract IERC20Upgradeable token, address account) external nonpayable -``` - - - -*Triggers a transfer to `account` of the amount of `token` tokens they are owed, according to their percentage of the total shares and their previous withdrawals. `token` must be the address of an IERC20 contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| token | contract IERC20Upgradeable | undefined -| account | address | undefined - -### released - -```solidity -function released(address account) external view returns (uint256) -``` - - - -*Getter for the amount of `token` tokens already released to a payee. `token` should be the address of an IERC20 contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been revoked `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### setContractURI - -```solidity -function setContractURI(string _uri) external nonpayable -``` - - - -*Sets contract URI for the contract-level metadata of the contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _uri | string | undefined - -### shares - -```solidity -function shares(address account) external view returns (uint256) -``` - - - -*Getter for the amount of shares held by an account.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### thirdwebFee - -```solidity -function thirdwebFee() external view returns (contract ITWFee) -``` - - - -*The thirdweb contract with fee related information.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | contract ITWFee | undefined - -### totalReleased - -```solidity -function totalReleased() external view returns (uint256) -``` - - - -*Getter for the total amount of `token` already released. `token` should be the address of an IERC20 contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### totalShares - -```solidity -function totalShares() external view returns (uint256) -``` - - - -*Getter for the total shares held by payees.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - - - -## Events - -### ERC20PaymentReleased - -```solidity -event ERC20PaymentReleased(contract IERC20Upgradeable indexed token, address to, uint256 amount) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| token `indexed` | contract IERC20Upgradeable | undefined | -| to | address | undefined | -| amount | uint256 | undefined | - -### PayeeAdded - -```solidity -event PayeeAdded(address account, uint256 shares) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined | -| shares | uint256 | undefined | - -### PaymentReceived - -```solidity -event PaymentReceived(address from, uint256 amount) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined | -| amount | uint256 | undefined | - -### PaymentReleased - -```solidity -event PaymentReleased(address to, uint256 amount) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined | -| amount | uint256 | undefined | - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - - - diff --git a/docs/StorageSlot.md b/docs/StorageSlot.md deleted file mode 100644 index ebc1f786c..000000000 --- a/docs/StorageSlot.md +++ /dev/null @@ -1,12 +0,0 @@ -# StorageSlot - - - - - - - -*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. Example usage to set ERC1967 implementation slot: ``` contract ERC1967 { bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; function _getImplementation() internal view returns (address) { return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; } function _setImplementation(address newImplementation) internal { require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract"); StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; } } ``` _Available since v4.1 for `address`, `bool`, `bytes32`, and `uint256`._* - - - diff --git a/docs/Strings.md b/docs/Strings.md deleted file mode 100644 index f85055d64..000000000 --- a/docs/Strings.md +++ /dev/null @@ -1,12 +0,0 @@ -# Strings - - - - - - - -*String operations.* - - - diff --git a/docs/StringsUpgradeable.md b/docs/StringsUpgradeable.md deleted file mode 100644 index 84464ba83..000000000 --- a/docs/StringsUpgradeable.md +++ /dev/null @@ -1,12 +0,0 @@ -# StringsUpgradeable - - - - - - - -*String operations.* - - - diff --git a/docs/TWAddress.md b/docs/TWAddress.md deleted file mode 100644 index b10c561c5..000000000 --- a/docs/TWAddress.md +++ /dev/null @@ -1,12 +0,0 @@ -# TWAddress - - - - - - - -*Collection of functions related to the address type* - - - diff --git a/docs/TWBitMaps.md b/docs/TWBitMaps.md deleted file mode 100644 index 0ac4af7c4..000000000 --- a/docs/TWBitMaps.md +++ /dev/null @@ -1,12 +0,0 @@ -# TWBitMaps - - - - - - - -*Library for managing uint256 to bool mapping in a compact and efficient way, providing the keys are sequential. Largelly inspired by Uniswap's https://github.com/Uniswap/merkle-distributor/blob/master/contracts/MerkleDistributor.sol[merkle-distributor].* - - - diff --git a/docs/TWFactory.md b/docs/TWFactory.md deleted file mode 100644 index 54e56c384..000000000 --- a/docs/TWFactory.md +++ /dev/null @@ -1,621 +0,0 @@ -# TWFactory - - - - - - - - - -## Methods - -### DEFAULT_ADMIN_ROLE - -```solidity -function DEFAULT_ADMIN_ROLE() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### FACTORY_ROLE - -```solidity -function FACTORY_ROLE() external view returns (bytes32) -``` - - - -*Only FACTORY_ROLE holders can approve/unapprove implementations for proxies to point to.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### addImplementation - -```solidity -function addImplementation(address _implementation) external nonpayable -``` - - - -*Lets a contract admin set the address of a contract type x version.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _implementation | address | undefined - -### approval - -```solidity -function approval(address) external view returns (bool) -``` - - - -*mapping of implementation address to deployment approval* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### approveImplementation - -```solidity -function approveImplementation(address _implementation, bool _toApprove) external nonpayable -``` - - - -*Lets a contract admin approve a specific contract for deployment.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _implementation | address | undefined -| _toApprove | bool | undefined - -### currentVersion - -```solidity -function currentVersion(bytes32) external view returns (uint256) -``` - - - -*mapping of implementation address to implementation added version* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### deployProxy - -```solidity -function deployProxy(bytes32 _type, bytes _data) external nonpayable returns (address) -``` - - - -*Deploys a proxy that points to the latest version of the given contract type.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _type | bytes32 | undefined -| _data | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### deployProxyByImplementation - -```solidity -function deployProxyByImplementation(address _implementation, bytes _data, bytes32 _salt) external nonpayable returns (address deployedProxy) -``` - - - -*Deploys a proxy that points to the given implementation.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _implementation | address | undefined -| _data | bytes | undefined -| _salt | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| deployedProxy | address | undefined - -### deployProxyDeterministic - -```solidity -function deployProxyDeterministic(bytes32 _type, bytes _data, bytes32 _salt) external nonpayable returns (address) -``` - - - -*Deploys a proxy at a deterministic address by taking in `salt` as a parameter. Proxy points to the latest version of the given contract type.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _type | bytes32 | undefined -| _data | bytes | undefined -| _salt | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### deployer - -```solidity -function deployer(address) external view returns (address) -``` - - - -*mapping of proxy address to deployer address* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getImplementation - -```solidity -function getImplementation(bytes32 _type, uint256 _version) external view returns (address) -``` - - - -*Returns the implementation given a contract type and version.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _type | bytes32 | undefined -| _version | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getLatestImplementation - -```solidity -function getLatestImplementation(bytes32 _type) external view returns (address) -``` - - - -*Returns the latest implementation given a contract type.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _type | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleMember - -```solidity -function getRoleMember(bytes32 role, uint256 index) external view returns (address) -``` - - - -*Returns one of the accounts that have `role`. `index` must be a value between 0 and {getRoleMemberCount}, non-inclusive. Role bearers are not sorted in any particular way, and their ordering may change at any point. WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure you perform all queries on the same block. See the following https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] for more information.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getRoleMemberCount - -```solidity -function getRoleMemberCount(bytes32 role) external view returns (uint256) -``` - - - -*Returns the number of accounts that have `role`. Can be used together with {getRoleMember} to enumerate all bearers of a role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### implementation - -```solidity -function implementation(bytes32, uint256) external view returns (address) -``` - - - -*mapping of contract type to module version to implementation address* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined -| _1 | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### isTrustedForwarder - -```solidity -function isTrustedForwarder(address forwarder) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| forwarder | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### multicall - -```solidity -function multicall(bytes[] data) external nonpayable returns (bytes[] results) -``` - - - -*Receives and executes a batch of function calls on this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| results | bytes[] | undefined - -### registry - -```solidity -function registry() external view returns (contract TWRegistry) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | contract TWRegistry | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been revoked `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - -## Events - -### ImplementationAdded - -```solidity -event ImplementationAdded(address implementation, bytes32 indexed contractType, uint256 version) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| implementation | address | undefined | -| contractType `indexed` | bytes32 | undefined | -| version | uint256 | undefined | - -### ImplementationApproved - -```solidity -event ImplementationApproved(address implementation, bool isApproved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| implementation | address | undefined | -| isApproved | bool | undefined | - -### ProxyDeployed - -```solidity -event ProxyDeployed(address indexed implementation, address proxy, address indexed deployer) -``` - - - -*Emitted when a proxy is deployed.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| implementation `indexed` | address | undefined | -| proxy | address | undefined | -| deployer `indexed` | address | undefined | - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - - - diff --git a/docs/TWFee.md b/docs/TWFee.md deleted file mode 100644 index 7300897a2..000000000 --- a/docs/TWFee.md +++ /dev/null @@ -1,509 +0,0 @@ -# TWFee - - - - - - - - - -## Methods - -### DEFAULT_ADMIN_ROLE - -```solidity -function DEFAULT_ADMIN_ROLE() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### MAX_FEE_BPS - -```solidity -function MAX_FEE_BPS() external view returns (uint256) -``` - - - -*The maximum threshold for fees. 1%* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### factory - -```solidity -function factory() external view returns (contract TWFactory) -``` - - - -*The factory for deploying contracts.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | contract TWFactory | undefined - -### feeInfo - -```solidity -function feeInfo(uint256, uint256) external view returns (uint256 bps, address recipient) -``` - - - -*Mapping from pricing tier id => Fee Type (lib/FeeType.sol) => FeeInfo* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined -| _1 | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| bps | uint256 | undefined -| recipient | address | undefined - -### getFeeInfo - -```solidity -function getFeeInfo(address _proxy, uint256 _feeType) external view returns (address recipient, uint256 bps) -``` - - - -*Returns the fee info for a given module and fee type.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _proxy | address | undefined -| _feeType | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| recipient | address | undefined -| bps | uint256 | undefined - -### getFeeTier - -```solidity -function getFeeTier(address _deployer, address _proxy) external view returns (uint128 tierId, uint128 validUntilTimestamp) -``` - - - -*Returns the fee tier for a proxy deployer wallet or contract address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _deployer | address | undefined -| _proxy | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| tierId | uint128 | undefined -| validUntilTimestamp | uint128 | undefined - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleMember - -```solidity -function getRoleMember(bytes32 role, uint256 index) external view returns (address) -``` - - - -*Returns one of the accounts that have `role`. `index` must be a value between 0 and {getRoleMemberCount}, non-inclusive. Role bearers are not sorted in any particular way, and their ordering may change at any point. WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure you perform all queries on the same block. See the following https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] for more information.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getRoleMemberCount - -```solidity -function getRoleMemberCount(bytes32 role) external view returns (uint256) -``` - - - -*Returns the number of accounts that have `role`. Can be used together with {getRoleMember} to enumerate all bearers of a role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### isTrustedForwarder - -```solidity -function isTrustedForwarder(address forwarder) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| forwarder | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### multicall - -```solidity -function multicall(bytes[] data) external nonpayable returns (bytes[] results) -``` - - - -*Receives and executes a batch of function calls on this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| results | bytes[] | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been revoked `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### setFeeInfoForTier - -```solidity -function setFeeInfoForTier(uint256 _tierId, uint256 _feeBps, address _feeRecipient, uint256 _feeType) external nonpayable -``` - - - -*Lets the admin set fee bps and recipient for the given pricing tier and fee type.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tierId | uint256 | undefined -| _feeBps | uint256 | undefined -| _feeRecipient | address | undefined -| _feeType | uint256 | undefined - -### setFeeTierPlacementExtension - -```solidity -function setFeeTierPlacementExtension(address _extension) external nonpayable -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _extension | address | undefined - -### setTier - -```solidity -function setTier(address _proxyOrDeployer, uint128 _tierId, uint128 _validUntilTimestamp) external nonpayable -``` - - - -*Lets a TIER_CONTROLLER_ROLE holder assign a tier to a proxy deployer.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _proxyOrDeployer | address | undefined -| _tierId | uint128 | undefined -| _validUntilTimestamp | uint128 | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### tierPlacementExtension - -```solidity -function tierPlacementExtension() external view returns (contract IFeeTierPlacementExtension) -``` - - - -*If we want to extend the logic for fee tier placement, we could easily points it to a different extension implementation.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | contract IFeeTierPlacementExtension | undefined - - - -## Events - -### FeeTierUpdated - -```solidity -event FeeTierUpdated(uint256 indexed tierId, uint256 indexed feeType, address recipient, uint256 bps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tierId `indexed` | uint256 | undefined | -| feeType `indexed` | uint256 | undefined | -| recipient | address | undefined | -| bps | uint256 | undefined | - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### TierUpdated - -```solidity -event TierUpdated(address indexed proxyOrDeployer, uint256 tierId, uint256 validUntilTimestamp) -``` - - - -*Events* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proxyOrDeployer `indexed` | address | undefined | -| tierId | uint256 | undefined | -| validUntilTimestamp | uint256 | undefined | - - - diff --git a/docs/TWProxy.md b/docs/TWProxy.md deleted file mode 100644 index 6a9bec3e6..000000000 --- a/docs/TWProxy.md +++ /dev/null @@ -1,12 +0,0 @@ -# TWProxy - - - - - - - - - - - diff --git a/docs/TWRegistry.md b/docs/TWRegistry.md deleted file mode 100644 index 6e56f1b9a..000000000 --- a/docs/TWRegistry.md +++ /dev/null @@ -1,425 +0,0 @@ -# TWRegistry - - - - - - - - - -## Methods - -### DEFAULT_ADMIN_ROLE - -```solidity -function DEFAULT_ADMIN_ROLE() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### OPERATOR_ROLE - -```solidity -function OPERATOR_ROLE() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### add - -```solidity -function add(address _deployer, address _deployment) external nonpayable -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _deployer | address | undefined -| _deployment | address | undefined - -### count - -```solidity -function count(address _deployer) external view returns (uint256) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _deployer | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getAll - -```solidity -function getAll(address _deployer) external view returns (address[]) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _deployer | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address[] | undefined - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleMember - -```solidity -function getRoleMember(bytes32 role, uint256 index) external view returns (address) -``` - - - -*Returns one of the accounts that have `role`. `index` must be a value between 0 and {getRoleMemberCount}, non-inclusive. Role bearers are not sorted in any particular way, and their ordering may change at any point. WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure you perform all queries on the same block. See the following https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] for more information.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getRoleMemberCount - -```solidity -function getRoleMemberCount(bytes32 role) external view returns (uint256) -``` - - - -*Returns the number of accounts that have `role`. Can be used together with {getRoleMember} to enumerate all bearers of a role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### isTrustedForwarder - -```solidity -function isTrustedForwarder(address forwarder) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| forwarder | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### multicall - -```solidity -function multicall(bytes[] data) external nonpayable returns (bytes[] results) -``` - - - -*Receives and executes a batch of function calls on this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| results | bytes[] | undefined - -### remove - -```solidity -function remove(address _deployer, address _deployment) external nonpayable -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _deployer | address | undefined -| _deployment | address | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been revoked `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - -## Events - -### Added - -```solidity -event Added(address indexed deployer, address indexed deployment) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| deployer `indexed` | address | undefined | -| deployment `indexed` | address | undefined | - -### Deleted - -```solidity -event Deleted(address indexed deployer, address indexed deployment) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| deployer `indexed` | address | undefined | -| deployment `indexed` | address | undefined | - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - - - diff --git a/docs/TWStrings.md b/docs/TWStrings.md deleted file mode 100644 index 463ff475b..000000000 --- a/docs/TWStrings.md +++ /dev/null @@ -1,12 +0,0 @@ -# TWStrings - - - - - - - -*String operations.* - - - diff --git a/docs/TimersUpgradeable.md b/docs/TimersUpgradeable.md deleted file mode 100644 index 58aef0003..000000000 --- a/docs/TimersUpgradeable.md +++ /dev/null @@ -1,12 +0,0 @@ -# TimersUpgradeable - - - - - - - -*Tooling for timepoints, timers and delays* - - - diff --git a/docs/TokenBundle.md b/docs/TokenBundle.md deleted file mode 100644 index 6b545d82f..000000000 --- a/docs/TokenBundle.md +++ /dev/null @@ -1,82 +0,0 @@ -# TokenBundle - - - - - - - - - -## Methods - -### getTokenCountOfBundle - -```solidity -function getTokenCountOfBundle(uint256 _bundleId) external view returns (uint256) -``` - - - -*Returns the total number of assets in a particular bundle.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _bundleId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getTokenOfBundle - -```solidity -function getTokenOfBundle(uint256 _bundleId, uint256 index) external view returns (struct ITokenBundle.Token) -``` - - - -*Returns an asset contained in a particular bundle, at a particular index.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _bundleId | uint256 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | ITokenBundle.Token | undefined - -### getUriOfBundle - -```solidity -function getUriOfBundle(uint256 _bundleId) external view returns (string) -``` - - - -*Returns the uri of a particular bundle.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _bundleId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - - - - diff --git a/docs/TokenERC1155.md b/docs/TokenERC1155.md deleted file mode 100644 index 483edbd5f..000000000 --- a/docs/TokenERC1155.md +++ /dev/null @@ -1,1177 +0,0 @@ -# TokenERC1155 - - - - - - - - - -## Methods - -### DEFAULT_ADMIN_ROLE - -```solidity -function DEFAULT_ADMIN_ROLE() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### balanceOf - -```solidity -function balanceOf(address account, uint256 id) external view returns (uint256) -``` - - - -*See {IERC1155-balanceOf}. Requirements: - `account` cannot be the zero address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| id | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### balanceOfBatch - -```solidity -function balanceOfBatch(address[] accounts, uint256[] ids) external view returns (uint256[]) -``` - - - -*See {IERC1155-balanceOfBatch}. Requirements: - `accounts` and `ids` must have the same length.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| accounts | address[] | undefined -| ids | uint256[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256[] | undefined - -### burn - -```solidity -function burn(address account, uint256 id, uint256 value) external nonpayable -``` - - - -*Lets a token owner burn the tokens they own (i.e. destroy for good)* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| id | uint256 | undefined -| value | uint256 | undefined - -### burnBatch - -```solidity -function burnBatch(address account, uint256[] ids, uint256[] values) external nonpayable -``` - - - -*Lets a token owner burn multiple tokens they own at once (i.e. destroy for good)* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| ids | uint256[] | undefined -| values | uint256[] | undefined - -### contractType - -```solidity -function contractType() external pure returns (bytes32) -``` - - - -*Returns the module type of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### contractURI - -```solidity -function contractURI() external view returns (string) -``` - - - -*Contract level metadata.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### contractVersion - -```solidity -function contractVersion() external pure returns (uint8) -``` - - - -*Returns the version of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint8 | undefined - -### getDefaultRoyaltyInfo - -```solidity -function getDefaultRoyaltyInfo() external view returns (address, uint16) -``` - - - -*Returns the platform fee bps and recipient.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### getPlatformFeeInfo - -```solidity -function getPlatformFeeInfo() external view returns (address, uint16) -``` - - - -*Returns the platform fee bps and recipient.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleMember - -```solidity -function getRoleMember(bytes32 role, uint256 index) external view returns (address) -``` - - - -*Returns one of the accounts that have `role`. `index` must be a value between 0 and {getRoleMemberCount}, non-inclusive. Role bearers are not sorted in any particular way, and their ordering may change at any point. WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure you perform all queries on the same block. See the following https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] for more information.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getRoleMemberCount - -```solidity -function getRoleMemberCount(bytes32 role) external view returns (uint256) -``` - - - -*Returns the number of accounts that have `role`. Can be used together with {getRoleMember} to enumerate all bearers of a role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getRoyaltyInfoForToken - -```solidity -function getRoyaltyInfoForToken(uint256 _tokenId) external view returns (address, uint16) -``` - - - -*Returns the royalty recipient for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### initialize - -```solidity -function initialize(address _defaultAdmin, string _name, string _symbol, string _contractURI, address[] _trustedForwarders, address _primarySaleRecipient, address _royaltyRecipient, uint128 _royaltyBps, uint128 _platformFeeBps, address _platformFeeRecipient) external nonpayable -``` - - - -*Initiliazes the contract, like a constructor.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _defaultAdmin | address | undefined -| _name | string | undefined -| _symbol | string | undefined -| _contractURI | string | undefined -| _trustedForwarders | address[] | undefined -| _primarySaleRecipient | address | undefined -| _royaltyRecipient | address | undefined -| _royaltyBps | uint128 | undefined -| _platformFeeBps | uint128 | undefined -| _platformFeeRecipient | address | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address account, address operator) external view returns (bool) -``` - - - -*See {IERC1155-isApprovedForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### isTrustedForwarder - -```solidity -function isTrustedForwarder(address forwarder) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| forwarder | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### mintTo - -```solidity -function mintTo(address _to, uint256 _tokenId, string _uri, uint256 _amount) external nonpayable -``` - - - -*Lets an account with MINTER_ROLE mint an NFT.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _to | address | undefined -| _tokenId | uint256 | undefined -| _uri | string | undefined -| _amount | uint256 | undefined - -### mintWithSignature - -```solidity -function mintWithSignature(ITokenERC1155.MintRequest _req, bytes _signature) external payable -``` - - - -*Mints an NFT according to the provided mint request.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _req | ITokenERC1155.MintRequest | undefined -| _signature | bytes | undefined - -### multicall - -```solidity -function multicall(bytes[] data) external nonpayable returns (bytes[] results) -``` - - - -*Receives and executes a batch of function calls on this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| results | bytes[] | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### nextTokenIdToMint - -```solidity -function nextTokenIdToMint() external view returns (uint256) -``` - - - -*The next token ID of the NFT to mint.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### owner - -```solidity -function owner() external view returns (address) -``` - - - -*Returns the address of the current owner.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### platformFeeBps - -```solidity -function platformFeeBps() external view returns (uint128) -``` - - - -*The % of primary sales collected by the contract as fees.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint128 | undefined - -### platformFeeRecipient - -```solidity -function platformFeeRecipient() external view returns (address) -``` - - - -*The adress that receives all primary sales value.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### primarySaleRecipient - -```solidity -function primarySaleRecipient() external view returns (address) -``` - - - -*The adress that receives all primary sales value.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been revoked `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### royaltyInfo - -```solidity -function royaltyInfo(uint256 tokenId, uint256 salePrice) external view returns (address receiver, uint256 royaltyAmount) -``` - - - -*See EIP-2981* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined -| salePrice | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| receiver | address | undefined -| royaltyAmount | uint256 | undefined - -### safeBatchTransferFrom - -```solidity -function safeBatchTransferFrom(address from, address to, uint256[] ids, uint256[] amounts, bytes data) external nonpayable -``` - - - -*See {IERC1155-safeBatchTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| ids | uint256[] | undefined -| amounts | uint256[] | undefined -| data | bytes | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data) external nonpayable -``` - - - -*See {IERC1155-safeTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| id | uint256 | undefined -| amount | uint256 | undefined -| data | bytes | undefined - -### saleRecipientForToken - -```solidity -function saleRecipientForToken(uint256) external view returns (address) -``` - - - -*Token ID => the address of the recipient of primary sales.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool approved) external nonpayable -``` - - - -*See {IERC1155-setApprovalForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| approved | bool | undefined - -### setContractURI - -```solidity -function setContractURI(string _uri) external nonpayable -``` - - - -*Lets a module admin set the URI for contract-level metadata.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _uri | string | undefined - -### setDefaultRoyaltyInfo - -```solidity -function setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) external nonpayable -``` - - - -*Lets a module admin update the royalty bps and recipient.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _royaltyRecipient | address | undefined -| _royaltyBps | uint256 | undefined - -### setOwner - -```solidity -function setOwner(address _newOwner) external nonpayable -``` - - - -*Lets a module admin set a new owner for the contract. The new owner must be a module admin.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _newOwner | address | undefined - -### setPlatformFeeInfo - -```solidity -function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) external nonpayable -``` - - - -*Lets a module admin update the fees on primary sales.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _platformFeeRecipient | address | undefined -| _platformFeeBps | uint256 | undefined - -### setPrimarySaleRecipient - -```solidity -function setPrimarySaleRecipient(address _saleRecipient) external nonpayable -``` - - - -*Lets a module admin set the default recipient of all primary sales.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _saleRecipient | address | undefined - -### setRoyaltyInfoForToken - -```solidity -function setRoyaltyInfoForToken(uint256 _tokenId, address _recipient, uint256 _bps) external nonpayable -``` - - - -*Lets a module admin set the royalty recipient for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined -| _recipient | address | undefined -| _bps | uint256 | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### thirdwebFee - -```solidity -function thirdwebFee() external view returns (contract ITWFee) -``` - - - -*The thirdweb contract with fee related information.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | contract ITWFee | undefined - -### totalSupply - -```solidity -function totalSupply(uint256) external view returns (uint256) -``` - - - -*Token ID => total circulating supply of tokens with that ID.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### uri - -```solidity -function uri(uint256 _tokenId) external view returns (string) -``` - - - -*Returns the URI for a tokenId* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### verify - -```solidity -function verify(ITokenERC1155.MintRequest _req, bytes _signature) external view returns (bool, address) -``` - - - -*Verifies that a mint request is signed by an account holding MINTER_ROLE (at the time of the function call).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _req | ITokenERC1155.MintRequest | undefined -| _signature | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined -| _1 | address | undefined - - - -## Events - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed account, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### DefaultRoyalty - -```solidity -event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| newRoyaltyRecipient `indexed` | address | undefined | -| newRoyaltyBps | uint256 | undefined | - -### OwnerUpdated - -```solidity -event OwnerUpdated(address indexed prevOwner, address indexed newOwner) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| prevOwner `indexed` | address | undefined | -| newOwner `indexed` | address | undefined | - -### PlatformFeeInfoUpdated - -```solidity -event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| platformFeeRecipient `indexed` | address | undefined | -| platformFeeBps | uint256 | undefined | - -### PrimarySaleRecipientUpdated - -```solidity -event PrimarySaleRecipientUpdated(address indexed recipient) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| recipient `indexed` | address | undefined | - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoyaltyForToken - -```solidity -event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, uint256 royaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId `indexed` | uint256 | undefined | -| royaltyRecipient `indexed` | address | undefined | -| royaltyBps | uint256 | undefined | - -### TokensMinted - -```solidity -event TokensMinted(address indexed mintedTo, uint256 indexed tokenIdMinted, string uri, uint256 quantityMinted) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| mintedTo `indexed` | address | undefined | -| tokenIdMinted `indexed` | uint256 | undefined | -| uri | string | undefined | -| quantityMinted | uint256 | undefined | - -### TokensMintedWithSignature - -```solidity -event TokensMintedWithSignature(address indexed signer, address indexed mintedTo, uint256 indexed tokenIdMinted, ITokenERC1155.MintRequest mintRequest) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| signer `indexed` | address | undefined | -| mintedTo `indexed` | address | undefined | -| tokenIdMinted `indexed` | uint256 | undefined | -| mintRequest | ITokenERC1155.MintRequest | undefined | - -### TransferBatch - -```solidity -event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| ids | uint256[] | undefined | -| values | uint256[] | undefined | - -### TransferSingle - -```solidity -event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| id | uint256 | undefined | -| value | uint256 | undefined | - -### URI - -```solidity -event URI(string value, uint256 indexed id) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| value | string | undefined | -| id `indexed` | uint256 | undefined | - - - diff --git a/docs/TokenERC20.md b/docs/TokenERC20.md deleted file mode 100644 index 7de176d09..000000000 --- a/docs/TokenERC20.md +++ /dev/null @@ -1,1217 +0,0 @@ -# TokenERC20 - - - - - - - - - -## Methods - -### DEFAULT_ADMIN_ROLE - -```solidity -function DEFAULT_ADMIN_ROLE() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### DOMAIN_SEPARATOR - -```solidity -function DOMAIN_SEPARATOR() external view returns (bytes32) -``` - - - -*See {IERC20Permit-DOMAIN_SEPARATOR}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### allowance - -```solidity -function allowance(address owner, address spender) external view returns (uint256) -``` - - - -*See {IERC20-allowance}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| spender | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### approve - -```solidity -function approve(address spender, uint256 amount) external nonpayable returns (bool) -``` - - - -*See {IERC20-approve}. NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on `transferFrom`. This is semantically equivalent to an infinite approval. Requirements: - `spender` cannot be the zero address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### balanceOf - -```solidity -function balanceOf(address account) external view returns (uint256) -``` - - - -*See {IERC20-balanceOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### burn - -```solidity -function burn(uint256 amount) external nonpayable -``` - - - -*Destroys `amount` tokens from the caller. See {ERC20-_burn}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| amount | uint256 | undefined - -### burnFrom - -```solidity -function burnFrom(address account, uint256 amount) external nonpayable -``` - - - -*Destroys `amount` tokens from `account`, deducting from the caller's allowance. See {ERC20-_burn} and {ERC20-allowance}. Requirements: - the caller must have allowance for ``accounts``'s tokens of at least `amount`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| amount | uint256 | undefined - -### checkpoints - -```solidity -function checkpoints(address account, uint32 pos) external view returns (struct ERC20VotesUpgradeable.Checkpoint) -``` - - - -*Get the `pos`-th checkpoint for `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| pos | uint32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | ERC20VotesUpgradeable.Checkpoint | undefined - -### contractType - -```solidity -function contractType() external pure returns (bytes32) -``` - - - -*Returns the module type of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### contractURI - -```solidity -function contractURI() external view returns (string) -``` - - - -*Returns the URI for the storefront-level metadata of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### contractVersion - -```solidity -function contractVersion() external pure returns (uint8) -``` - - - -*Returns the version of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint8 | undefined - -### decimals - -```solidity -function decimals() external view returns (uint8) -``` - - - -*Returns the number of decimals used to get its user representation. For example, if `decimals` equals `2`, a balance of `505` tokens should be displayed to a user as `5.05` (`505 / 10 ** 2`). Tokens usually opt for a value of 18, imitating the relationship between Ether and Wei. This is the value {ERC20} uses, unless this function is overridden; NOTE: This information is only used for _display_ purposes: it in no way affects any of the arithmetic of the contract, including {IERC20-balanceOf} and {IERC20-transfer}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint8 | undefined - -### decreaseAllowance - -```solidity -function decreaseAllowance(address spender, uint256 subtractedValue) external nonpayable returns (bool) -``` - - - -*Atomically decreases the allowance granted to `spender` by the caller. This is an alternative to {approve} that can be used as a mitigation for problems described in {IERC20-approve}. Emits an {Approval} event indicating the updated allowance. Requirements: - `spender` cannot be the zero address. - `spender` must have allowance for the caller of at least `subtractedValue`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| subtractedValue | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### delegate - -```solidity -function delegate(address delegatee) external nonpayable -``` - - - -*Delegate votes from the sender to `delegatee`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| delegatee | address | undefined - -### delegateBySig - -```solidity -function delegateBySig(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) external nonpayable -``` - - - -*Delegates votes from signer to `delegatee`* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| delegatee | address | undefined -| nonce | uint256 | undefined -| expiry | uint256 | undefined -| v | uint8 | undefined -| r | bytes32 | undefined -| s | bytes32 | undefined - -### delegates - -```solidity -function delegates(address account) external view returns (address) -``` - - - -*Get the address `account` is currently delegating to.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getPastTotalSupply - -```solidity -function getPastTotalSupply(uint256 blockNumber) external view returns (uint256) -``` - - - -*Retrieve the `totalSupply` at the end of `blockNumber`. Note, this value is the sum of all balances. It is but NOT the sum of all the delegated votes! Requirements: - `blockNumber` must have been already mined* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| blockNumber | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getPastVotes - -```solidity -function getPastVotes(address account, uint256 blockNumber) external view returns (uint256) -``` - - - -*Retrieve the number of votes for `account` at the end of `blockNumber`. Requirements: - `blockNumber` must have been already mined* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| blockNumber | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getPlatformFeeInfo - -```solidity -function getPlatformFeeInfo() external view returns (address, uint16) -``` - - - -*Returns the platform fee bps and recipient.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleMember - -```solidity -function getRoleMember(bytes32 role, uint256 index) external view returns (address) -``` - - - -*Returns one of the accounts that have `role`. `index` must be a value between 0 and {getRoleMemberCount}, non-inclusive. Role bearers are not sorted in any particular way, and their ordering may change at any point. WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure you perform all queries on the same block. See the following https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] for more information.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getRoleMemberCount - -```solidity -function getRoleMemberCount(bytes32 role) external view returns (uint256) -``` - - - -*Returns the number of accounts that have `role`. Can be used together with {getRoleMember} to enumerate all bearers of a role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getVotes - -```solidity -function getVotes(address account) external view returns (uint256) -``` - - - -*Gets the current votes balance for `account`* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### increaseAllowance - -```solidity -function increaseAllowance(address spender, uint256 addedValue) external nonpayable returns (bool) -``` - - - -*Atomically increases the allowance granted to `spender` by the caller. This is an alternative to {approve} that can be used as a mitigation for problems described in {IERC20-approve}. Emits an {Approval} event indicating the updated allowance. Requirements: - `spender` cannot be the zero address.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| addedValue | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### initialize - -```solidity -function initialize(address _defaultAdmin, string _name, string _symbol, string _contractURI, address[] _trustedForwarders, address _primarySaleRecipient, address _platformFeeRecipient, uint256 _platformFeeBps) external nonpayable -``` - - - -*Initiliazes the contract, like a constructor.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _defaultAdmin | address | undefined -| _name | string | undefined -| _symbol | string | undefined -| _contractURI | string | undefined -| _trustedForwarders | address[] | undefined -| _primarySaleRecipient | address | undefined -| _platformFeeRecipient | address | undefined -| _platformFeeBps | uint256 | undefined - -### isTrustedForwarder - -```solidity -function isTrustedForwarder(address forwarder) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| forwarder | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### mintTo - -```solidity -function mintTo(address to, uint256 amount) external nonpayable -``` - - - -*Creates `amount` new tokens for `to`. See {ERC20-_mint}. Requirements: - the caller must have the `MINTER_ROLE`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| amount | uint256 | undefined - -### mintWithSignature - -```solidity -function mintWithSignature(ITokenERC20.MintRequest _req, bytes _signature) external payable -``` - - - -*Mints tokens according to the provided mint request.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _req | ITokenERC20.MintRequest | undefined -| _signature | bytes | undefined - -### multicall - -```solidity -function multicall(bytes[] data) external nonpayable returns (bytes[] results) -``` - - - -*Receives and executes a batch of function calls on this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| results | bytes[] | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*Returns the name of the token.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### nonces - -```solidity -function nonces(address owner) external view returns (uint256) -``` - - - -*See {IERC20Permit-nonces}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### numCheckpoints - -```solidity -function numCheckpoints(address account) external view returns (uint32) -``` - - - -*Get number of checkpoints for `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint32 | undefined - -### pause - -```solidity -function pause() external nonpayable -``` - - - -*Pauses all token transfers. See {ERC20Pausable} and {Pausable-_pause}. Requirements: - the caller must have the `PAUSER_ROLE`.* - - -### paused - -```solidity -function paused() external view returns (bool) -``` - - - -*Returns true if the contract is paused, and false otherwise.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### permit - -```solidity -function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external nonpayable -``` - - - -*See {IERC20Permit-permit}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| spender | address | undefined -| value | uint256 | undefined -| deadline | uint256 | undefined -| v | uint8 | undefined -| r | bytes32 | undefined -| s | bytes32 | undefined - -### primarySaleRecipient - -```solidity -function primarySaleRecipient() external view returns (address) -``` - - - -*The adress that receives all primary sales value.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been revoked `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### setContractURI - -```solidity -function setContractURI(string _uri) external nonpayable -``` - - - -*Sets contract URI for the storefront-level metadata of the contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _uri | string | undefined - -### setPlatformFeeInfo - -```solidity -function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) external nonpayable -``` - - - -*Lets a module admin update the fees on primary sales.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _platformFeeRecipient | address | undefined -| _platformFeeBps | uint256 | undefined - -### setPrimarySaleRecipient - -```solidity -function setPrimarySaleRecipient(address _saleRecipient) external nonpayable -``` - - - -*Lets a module admin set the default recipient of all primary sales.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _saleRecipient | address | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - -*Returns the symbol of the token, usually a shorter version of the name.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - -*See {IERC20-totalSupply}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transfer - -```solidity -function transfer(address to, uint256 amount) external nonpayable returns (bool) -``` - - - -*See {IERC20-transfer}. Requirements: - `to` cannot be the zero address. - the caller must have a balance of at least `amount`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 amount) external nonpayable returns (bool) -``` - - - -*See {IERC20-transferFrom}. Emits an {Approval} event indicating the updated allowance. This is not required by the EIP. See the note at the beginning of {ERC20}. NOTE: Does not update the allowance if the current allowance is the maximum `uint256`. Requirements: - `from` and `to` cannot be the zero address. - `from` must have a balance of at least `amount`. - the caller must have allowance for ``from``'s tokens of at least `amount`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| amount | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### unpause - -```solidity -function unpause() external nonpayable -``` - - - -*Unpauses all token transfers. See {ERC20Pausable} and {Pausable-_unpause}. Requirements: - the caller must have the `PAUSER_ROLE`.* - - -### verify - -```solidity -function verify(ITokenERC20.MintRequest _req, bytes _signature) external view returns (bool, address) -``` - - - -*Verifies that a mint request is signed by an account holding MINTER_ROLE (at the time of the function call).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _req | ITokenERC20.MintRequest | undefined -| _signature | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined -| _1 | address | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed spender, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| spender `indexed` | address | undefined | -| value | uint256 | undefined | - -### DelegateChanged - -```solidity -event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| delegator `indexed` | address | undefined | -| fromDelegate `indexed` | address | undefined | -| toDelegate `indexed` | address | undefined | - -### DelegateVotesChanged - -```solidity -event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| delegate `indexed` | address | undefined | -| previousBalance | uint256 | undefined | -| newBalance | uint256 | undefined | - -### Paused - -```solidity -event Paused(address account) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined | - -### PlatformFeeInfoUpdated - -```solidity -event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| platformFeeRecipient `indexed` | address | undefined | -| platformFeeBps | uint256 | undefined | - -### PrimarySaleRecipientUpdated - -```solidity -event PrimarySaleRecipientUpdated(address indexed recipient) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| recipient `indexed` | address | undefined | - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### TokensMinted - -```solidity -event TokensMinted(address indexed mintedTo, uint256 quantityMinted) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| mintedTo `indexed` | address | undefined | -| quantityMinted | uint256 | undefined | - -### TokensMintedWithSignature - -```solidity -event TokensMintedWithSignature(address indexed signer, address indexed mintedTo, ITokenERC20.MintRequest mintRequest) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| signer `indexed` | address | undefined | -| mintedTo `indexed` | address | undefined | -| mintRequest | ITokenERC20.MintRequest | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 value) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| value | uint256 | undefined | - -### Unpaused - -```solidity -event Unpaused(address account) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined | - - - diff --git a/docs/TokenERC721.md b/docs/TokenERC721.md deleted file mode 100644 index 87ff0f65a..000000000 --- a/docs/TokenERC721.md +++ /dev/null @@ -1,1197 +0,0 @@ -# TokenERC721 - - - - - - - - - -## Methods - -### DEFAULT_ADMIN_ROLE - -```solidity -function DEFAULT_ADMIN_ROLE() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### approve - -```solidity -function approve(address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-approve}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| tokenId | uint256 | undefined - -### balanceOf - -```solidity -function balanceOf(address owner) external view returns (uint256) -``` - - - -*See {IERC721-balanceOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### burn - -```solidity -function burn(uint256 tokenId) external nonpayable -``` - - - -*Burns `tokenId`. See {ERC721-_burn}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -### contractType - -```solidity -function contractType() external pure returns (bytes32) -``` - - - -*Returns the module type of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### contractURI - -```solidity -function contractURI() external view returns (string) -``` - - - -*Contract level metadata.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### contractVersion - -```solidity -function contractVersion() external pure returns (uint8) -``` - - - -*Returns the version of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint8 | undefined - -### getApproved - -```solidity -function getApproved(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-getApproved}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getDefaultRoyaltyInfo - -```solidity -function getDefaultRoyaltyInfo() external view returns (address, uint16) -``` - - - -*Returns the platform fee bps and recipient.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### getPlatformFeeInfo - -```solidity -function getPlatformFeeInfo() external view returns (address, uint16) -``` - - - -*Returns the platform fee bps and recipient.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleMember - -```solidity -function getRoleMember(bytes32 role, uint256 index) external view returns (address) -``` - - - -*Returns one of the accounts that have `role`. `index` must be a value between 0 and {getRoleMemberCount}, non-inclusive. Role bearers are not sorted in any particular way, and their ordering may change at any point. WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure you perform all queries on the same block. See the following https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] for more information.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getRoleMemberCount - -```solidity -function getRoleMemberCount(bytes32 role) external view returns (uint256) -``` - - - -*Returns the number of accounts that have `role`. Can be used together with {getRoleMember} to enumerate all bearers of a role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getRoyaltyInfoForToken - -```solidity -function getRoyaltyInfoForToken(uint256 _tokenId) external view returns (address, uint16) -``` - - - -*Returns the royalty recipient for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### initialize - -```solidity -function initialize(address _defaultAdmin, string _name, string _symbol, string _contractURI, address[] _trustedForwarders, address _saleRecipient, address _royaltyRecipient, uint128 _royaltyBps, uint128 _platformFeeBps, address _platformFeeRecipient) external nonpayable -``` - - - -*Initiliazes the contract, like a constructor.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _defaultAdmin | address | undefined -| _name | string | undefined -| _symbol | string | undefined -| _contractURI | string | undefined -| _trustedForwarders | address[] | undefined -| _saleRecipient | address | undefined -| _royaltyRecipient | address | undefined -| _royaltyBps | uint128 | undefined -| _platformFeeBps | uint128 | undefined -| _platformFeeRecipient | address | undefined - -### isApprovedForAll - -```solidity -function isApprovedForAll(address owner, address operator) external view returns (bool) -``` - - - -*See {IERC721-isApprovedForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| operator | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### isTrustedForwarder - -```solidity -function isTrustedForwarder(address forwarder) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| forwarder | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### mintTo - -```solidity -function mintTo(address _to, string _uri) external nonpayable returns (uint256) -``` - - - -*Lets an account with MINTER_ROLE mint an NFT.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _to | address | undefined -| _uri | string | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### mintWithSignature - -```solidity -function mintWithSignature(ITokenERC721.MintRequest _req, bytes _signature) external payable returns (uint256 tokenIdMinted) -``` - - - -*Mints an NFT according to the provided mint request.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _req | ITokenERC721.MintRequest | undefined -| _signature | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| tokenIdMinted | uint256 | undefined - -### multicall - -```solidity -function multicall(bytes[] data) external nonpayable returns (bytes[] results) -``` - - - -*Receives and executes a batch of function calls on this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| results | bytes[] | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*See {IERC721Metadata-name}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### nextTokenIdToMint - -```solidity -function nextTokenIdToMint() external view returns (uint256) -``` - - - -*The token ID of the next token to mint.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### owner - -```solidity -function owner() external view returns (address) -``` - - - -*Returns the address of the current owner.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### ownerOf - -```solidity -function ownerOf(uint256 tokenId) external view returns (address) -``` - - - -*See {IERC721-ownerOf}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### platformFeeBps - -```solidity -function platformFeeBps() external view returns (uint128) -``` - - - -*The % of primary sales collected by the contract as fees.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint128 | undefined - -### platformFeeRecipient - -```solidity -function platformFeeRecipient() external view returns (address) -``` - - - -*The adress that receives all primary sales value.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### primarySaleRecipient - -```solidity -function primarySaleRecipient() external view returns (address) -``` - - - -*The adress that receives all primary sales value.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been revoked `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### royaltyInfo - -```solidity -function royaltyInfo(uint256 tokenId, uint256 salePrice) external view returns (address receiver, uint256 royaltyAmount) -``` - - - -*See EIP-2981* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined -| salePrice | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| receiver | address | undefined -| royaltyAmount | uint256 | undefined - -### safeTransferFrom - -```solidity -function safeTransferFrom(address from, address to, uint256 tokenId, bytes _data) external nonpayable -``` - - - -*See {IERC721-safeTransferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined -| _data | bytes | undefined - -### setApprovalForAll - -```solidity -function setApprovalForAll(address operator, bool approved) external nonpayable -``` - - - -*See {IERC721-setApprovalForAll}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator | address | undefined -| approved | bool | undefined - -### setContractURI - -```solidity -function setContractURI(string _uri) external nonpayable -``` - - - -*Lets a module admin set the URI for contract-level metadata.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _uri | string | undefined - -### setDefaultRoyaltyInfo - -```solidity -function setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) external nonpayable -``` - - - -*Lets a module admin update the royalty bps and recipient.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _royaltyRecipient | address | undefined -| _royaltyBps | uint256 | undefined - -### setOwner - -```solidity -function setOwner(address _newOwner) external nonpayable -``` - - - -*Lets a module admin set a new owner for the contract. The new owner must be a module admin.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _newOwner | address | undefined - -### setPlatformFeeInfo - -```solidity -function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) external nonpayable -``` - - - -*Lets a module admin update the fees on primary sales.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _platformFeeRecipient | address | undefined -| _platformFeeBps | uint256 | undefined - -### setPrimarySaleRecipient - -```solidity -function setPrimarySaleRecipient(address _saleRecipient) external nonpayable -``` - - - -*Lets a module admin set the default recipient of all primary sales.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _saleRecipient | address | undefined - -### setRoyaltyInfoForToken - -```solidity -function setRoyaltyInfoForToken(uint256 _tokenId, address _recipient, uint256 _bps) external nonpayable -``` - - - -*Lets a module admin set the royalty recipient for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined -| _recipient | address | undefined -| _bps | uint256 | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### symbol - -```solidity -function symbol() external view returns (string) -``` - - - -*See {IERC721Metadata-symbol}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### thirdwebFee - -```solidity -function thirdwebFee() external view returns (contract ITWFee) -``` - - - -*The thirdweb contract with fee related information.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | contract ITWFee | undefined - -### tokenByIndex - -```solidity -function tokenByIndex(uint256 index) external view returns (uint256) -``` - - - -*See {IERC721Enumerable-tokenByIndex}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### tokenOfOwnerByIndex - -```solidity -function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256) -``` - - - -*See {IERC721Enumerable-tokenOfOwnerByIndex}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### tokenURI - -```solidity -function tokenURI(uint256 _tokenId) external view returns (string) -``` - - - -*Returns the URI for a tokenId* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _tokenId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - - - -*See {IERC721Enumerable-totalSupply}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 tokenId) external nonpayable -``` - - - -*See {IERC721-transferFrom}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| tokenId | uint256 | undefined - -### verify - -```solidity -function verify(ITokenERC721.MintRequest _req, bytes _signature) external view returns (bool, address) -``` - - - -*Verifies that a mint request is signed by an account holding MINTER_ROLE (at the time of the function call).* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _req | ITokenERC721.MintRequest | undefined -| _signature | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined -| _1 | address | undefined - - - -## Events - -### Approval - -```solidity -event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| approved `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - -### ApprovalForAll - -```solidity -event ApprovalForAll(address indexed owner, address indexed operator, bool approved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| approved | bool | undefined | - -### DefaultRoyalty - -```solidity -event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| newRoyaltyRecipient `indexed` | address | undefined | -| newRoyaltyBps | uint256 | undefined | - -### OwnerUpdated - -```solidity -event OwnerUpdated(address indexed prevOwner, address indexed newOwner) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| prevOwner `indexed` | address | undefined | -| newOwner `indexed` | address | undefined | - -### PlatformFeeInfoUpdated - -```solidity -event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| platformFeeRecipient `indexed` | address | undefined | -| platformFeeBps | uint256 | undefined | - -### PrimarySaleRecipientUpdated - -```solidity -event PrimarySaleRecipientUpdated(address indexed recipient) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| recipient `indexed` | address | undefined | - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoyaltyForToken - -```solidity -event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, uint256 royaltyBps) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId `indexed` | uint256 | undefined | -| royaltyRecipient `indexed` | address | undefined | -| royaltyBps | uint256 | undefined | - -### TokensMinted - -```solidity -event TokensMinted(address indexed mintedTo, uint256 indexed tokenIdMinted, string uri) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| mintedTo `indexed` | address | undefined | -| tokenIdMinted `indexed` | uint256 | undefined | -| uri | string | undefined | - -### TokensMintedWithSignature - -```solidity -event TokensMintedWithSignature(address indexed signer, address indexed mintedTo, uint256 indexed tokenIdMinted, ITokenERC721.MintRequest mintRequest) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| signer `indexed` | address | undefined | -| mintedTo `indexed` | address | undefined | -| tokenIdMinted `indexed` | uint256 | undefined | -| mintRequest | ITokenERC721.MintRequest | undefined | - -### Transfer - -```solidity -event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from `indexed` | address | undefined | -| to `indexed` | address | undefined | -| tokenId `indexed` | uint256 | undefined | - - - diff --git a/docs/TokenStore.md b/docs/TokenStore.md deleted file mode 100644 index f2b69f79d..000000000 --- a/docs/TokenStore.md +++ /dev/null @@ -1,198 +0,0 @@ -# TokenStore - - - - - - - - - -## Methods - -### NATIVE_TOKEN - -```solidity -function NATIVE_TOKEN() external view returns (address) -``` - - - -*The address interpreted as native token of the chain.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getTokenCountOfBundle - -```solidity -function getTokenCountOfBundle(uint256 _bundleId) external view returns (uint256) -``` - - - -*Returns the total number of assets in a particular bundle.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _bundleId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getTokenOfBundle - -```solidity -function getTokenOfBundle(uint256 _bundleId, uint256 index) external view returns (struct ITokenBundle.Token) -``` - - - -*Returns an asset contained in a particular bundle, at a particular index.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _bundleId | uint256 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | ITokenBundle.Token | undefined - -### getUriOfBundle - -```solidity -function getUriOfBundle(uint256 _bundleId) external view returns (string) -``` - - - -*Returns the uri of a particular bundle.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _bundleId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### onERC1155BatchReceived - -```solidity -function onERC1155BatchReceived(address, address, uint256[], uint256[], bytes) external nonpayable returns (bytes4) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | address | undefined -| _2 | uint256[] | undefined -| _3 | uint256[] | undefined -| _4 | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - -### onERC1155Received - -```solidity -function onERC1155Received(address, address, uint256, uint256, bytes) external nonpayable returns (bytes4) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | address | undefined -| _2 | uint256 | undefined -| _3 | uint256 | undefined -| _4 | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - -### onERC721Received - -```solidity -function onERC721Received(address, address, uint256, bytes) external nonpayable returns (bytes4) -``` - - - -*See {IERC721Receiver-onERC721Received}. Always returns `IERC721Receiver.onERC721Received.selector`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | address | undefined -| _2 | uint256 | undefined -| _3 | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - - diff --git a/docs/VoteERC20.md b/docs/VoteERC20.md deleted file mode 100644 index 0d3e8344e..000000000 --- a/docs/VoteERC20.md +++ /dev/null @@ -1,991 +0,0 @@ -# VoteERC20 - - - - - - - - - -## Methods - -### BALLOT_TYPEHASH - -```solidity -function BALLOT_TYPEHASH() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### COUNTING_MODE - -```solidity -function COUNTING_MODE() external pure returns (string) -``` - - - -*See {IGovernor-COUNTING_MODE}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### castVote - -```solidity -function castVote(uint256 proposalId, uint8 support) external nonpayable returns (uint256) -``` - - - -*See {IGovernor-castVote}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| support | uint8 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### castVoteBySig - -```solidity -function castVoteBySig(uint256 proposalId, uint8 support, uint8 v, bytes32 r, bytes32 s) external nonpayable returns (uint256) -``` - - - -*See {IGovernor-castVoteBySig}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| support | uint8 | undefined -| v | uint8 | undefined -| r | bytes32 | undefined -| s | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### castVoteWithReason - -```solidity -function castVoteWithReason(uint256 proposalId, uint8 support, string reason) external nonpayable returns (uint256) -``` - - - -*See {IGovernor-castVoteWithReason}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| support | uint8 | undefined -| reason | string | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### contractType - -```solidity -function contractType() external pure returns (bytes32) -``` - - - -*Returns the module type of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### contractURI - -```solidity -function contractURI() external view returns (string) -``` - - - -*Returns the metadata URI of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### contractVersion - -```solidity -function contractVersion() external pure returns (uint8) -``` - - - -*Returns the version of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint8 | undefined - -### execute - -```solidity -function execute(address[] targets, uint256[] values, bytes[] calldatas, bytes32 descriptionHash) external payable returns (uint256) -``` - - - -*See {IGovernor-execute}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| targets | address[] | undefined -| values | uint256[] | undefined -| calldatas | bytes[] | undefined -| descriptionHash | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### getAllProposals - -```solidity -function getAllProposals() external view returns (struct VoteERC20.Proposal[] allProposals) -``` - - - -*Returns all proposals made.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| allProposals | VoteERC20.Proposal[] | undefined - -### getVotes - -```solidity -function getVotes(address account, uint256 blockNumber) external view returns (uint256) -``` - -Read the voting weight from the token's built in snapshot mechanism (see {IGovernor-getVotes}). - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| blockNumber | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### hasVoted - -```solidity -function hasVoted(uint256 proposalId, address account) external view returns (bool) -``` - - - -*See {IGovernor-hasVoted}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### hashProposal - -```solidity -function hashProposal(address[] targets, uint256[] values, bytes[] calldatas, bytes32 descriptionHash) external pure returns (uint256) -``` - - - -*See {IGovernor-hashProposal}. The proposal id is produced by hashing the RLC encoded `targets` array, the `values` array, the `calldatas` array and the descriptionHash (bytes32 which itself is the keccak256 hash of the description string). This proposal id can be produced from the proposal data which is part of the {ProposalCreated} event. It can even be computed in advance, before the proposal is submitted. Note that the chainId and the governor address are not part of the proposal id computation. Consequently, the same proposal (with same operation and same description) will have the same id if submitted on multiple governors accross multiple networks. This also means that in order to execute the same operation twice (on the same governor) the proposer will have to change the description in order to avoid proposal id conflicts.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| targets | address[] | undefined -| values | uint256[] | undefined -| calldatas | bytes[] | undefined -| descriptionHash | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### initialize - -```solidity -function initialize(string _name, string _contractURI, address[] _trustedForwarders, address _token, uint256 _initialVotingDelay, uint256 _initialVotingPeriod, uint256 _initialProposalThreshold, uint256 _initialVoteQuorumFraction) external nonpayable -``` - - - -*Initiliazes the contract, like a constructor.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _name | string | undefined -| _contractURI | string | undefined -| _trustedForwarders | address[] | undefined -| _token | address | undefined -| _initialVotingDelay | uint256 | undefined -| _initialVotingPeriod | uint256 | undefined -| _initialProposalThreshold | uint256 | undefined -| _initialVoteQuorumFraction | uint256 | undefined - -### isTrustedForwarder - -```solidity -function isTrustedForwarder(address forwarder) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| forwarder | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### name - -```solidity -function name() external view returns (string) -``` - - - -*See {IGovernor-name}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### onERC1155BatchReceived - -```solidity -function onERC1155BatchReceived(address, address, uint256[], uint256[], bytes) external nonpayable returns (bytes4) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | address | undefined -| _2 | uint256[] | undefined -| _3 | uint256[] | undefined -| _4 | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - -### onERC1155Received - -```solidity -function onERC1155Received(address, address, uint256, uint256, bytes) external nonpayable returns (bytes4) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | address | undefined -| _2 | uint256 | undefined -| _3 | uint256 | undefined -| _4 | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - -### onERC721Received - -```solidity -function onERC721Received(address, address, uint256, bytes) external nonpayable returns (bytes4) -``` - - - -*See {IERC721Receiver-onERC721Received}. Always returns `IERC721Receiver.onERC721Received.selector`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | address | undefined -| _2 | uint256 | undefined -| _3 | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes4 | undefined - -### proposalDeadline - -```solidity -function proposalDeadline(uint256 proposalId) external view returns (uint256) -``` - - - -*See {IGovernor-proposalDeadline}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### proposalIndex - -```solidity -function proposalIndex() external view returns (uint256) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### proposalSnapshot - -```solidity -function proposalSnapshot(uint256 proposalId) external view returns (uint256) -``` - - - -*See {IGovernor-proposalSnapshot}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### proposalThreshold - -```solidity -function proposalThreshold() external view returns (uint256) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### proposalVotes - -```solidity -function proposalVotes(uint256 proposalId) external view returns (uint256 againstVotes, uint256 forVotes, uint256 abstainVotes) -``` - - - -*Accessor to the internal vote counts.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| againstVotes | uint256 | undefined -| forVotes | uint256 | undefined -| abstainVotes | uint256 | undefined - -### proposals - -```solidity -function proposals(uint256) external view returns (uint256 proposalId, address proposer, uint256 startBlock, uint256 endBlock, string description) -``` - - - -*proposal index => Proposal* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined -| proposer | address | undefined -| startBlock | uint256 | undefined -| endBlock | uint256 | undefined -| description | string | undefined - -### propose - -```solidity -function propose(address[] targets, uint256[] values, bytes[] calldatas, string description) external nonpayable returns (uint256 proposalId) -``` - - - -*See {IGovernor-propose}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| targets | address[] | undefined -| values | uint256[] | undefined -| calldatas | bytes[] | undefined -| description | string | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -### quorum - -```solidity -function quorum(uint256 blockNumber) external view returns (uint256) -``` - - - -*Returns the quorum for a block number, in terms of number of votes: `supply * numerator / denominator`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| blockNumber | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### quorumDenominator - -```solidity -function quorumDenominator() external view returns (uint256) -``` - - - -*Returns the quorum denominator. Defaults to 100, but may be overridden.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### quorumNumerator - -```solidity -function quorumNumerator() external view returns (uint256) -``` - - - -*Returns the current quorum numerator. See {quorumDenominator}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### relay - -```solidity -function relay(address target, uint256 value, bytes data) external nonpayable -``` - - - -*Relays a transaction or function call to an arbitrary target. In cases where the governance executor is some contract other than the governor itself, like when using a timelock, this function can be invoked in a governance proposal to recover tokens or Ether that was sent to the governor contract by mistake. Note that if the executor is simply the governor itself, use of `relay` is redundant.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| target | address | undefined -| value | uint256 | undefined -| data | bytes | undefined - -### setContractURI - -```solidity -function setContractURI(string uri) external nonpayable -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| uri | string | undefined - -### setProposalThreshold - -```solidity -function setProposalThreshold(uint256 newProposalThreshold) external nonpayable -``` - - - -*Update the proposal threshold. This operation can only be performed through a governance proposal. Emits a {ProposalThresholdSet} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| newProposalThreshold | uint256 | undefined - -### setVotingDelay - -```solidity -function setVotingDelay(uint256 newVotingDelay) external nonpayable -``` - - - -*Update the voting delay. This operation can only be performed through a governance proposal. Emits a {VotingDelaySet} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| newVotingDelay | uint256 | undefined - -### setVotingPeriod - -```solidity -function setVotingPeriod(uint256 newVotingPeriod) external nonpayable -``` - - - -*Update the voting period. This operation can only be performed through a governance proposal. Emits a {VotingPeriodSet} event.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| newVotingPeriod | uint256 | undefined - -### state - -```solidity -function state(uint256 proposalId) external view returns (enum IGovernorUpgradeable.ProposalState) -``` - - - -*See {IGovernor-state}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | enum IGovernorUpgradeable.ProposalState | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### token - -```solidity -function token() external view returns (contract IVotesUpgradeable) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | contract IVotesUpgradeable | undefined - -### updateQuorumNumerator - -```solidity -function updateQuorumNumerator(uint256 newQuorumNumerator) external nonpayable -``` - - - -*Changes the quorum numerator. Emits a {QuorumNumeratorUpdated} event. Requirements: - Must be called through a governance proposal. - New numerator must be smaller or equal to the denominator.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| newQuorumNumerator | uint256 | undefined - -### version - -```solidity -function version() external view returns (string) -``` - - - -*See {IGovernor-version}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### votingDelay - -```solidity -function votingDelay() external view returns (uint256) -``` - - - -*See {IGovernor-votingDelay}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### votingPeriod - -```solidity -function votingPeriod() external view returns (uint256) -``` - - - -*See {IGovernor-votingPeriod}.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - - - -## Events - -### ProposalCanceled - -```solidity -event ProposalCanceled(uint256 proposalId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined | - -### ProposalCreated - -```solidity -event ProposalCreated(uint256 proposalId, address proposer, address[] targets, uint256[] values, string[] signatures, bytes[] calldatas, uint256 startBlock, uint256 endBlock, string description) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined | -| proposer | address | undefined | -| targets | address[] | undefined | -| values | uint256[] | undefined | -| signatures | string[] | undefined | -| calldatas | bytes[] | undefined | -| startBlock | uint256 | undefined | -| endBlock | uint256 | undefined | -| description | string | undefined | - -### ProposalExecuted - -```solidity -event ProposalExecuted(uint256 proposalId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| proposalId | uint256 | undefined | - -### ProposalThresholdSet - -```solidity -event ProposalThresholdSet(uint256 oldProposalThreshold, uint256 newProposalThreshold) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| oldProposalThreshold | uint256 | undefined | -| newProposalThreshold | uint256 | undefined | - -### QuorumNumeratorUpdated - -```solidity -event QuorumNumeratorUpdated(uint256 oldQuorumNumerator, uint256 newQuorumNumerator) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| oldQuorumNumerator | uint256 | undefined | -| newQuorumNumerator | uint256 | undefined | - -### VoteCast - -```solidity -event VoteCast(address indexed voter, uint256 proposalId, uint8 support, uint256 weight, string reason) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| voter `indexed` | address | undefined | -| proposalId | uint256 | undefined | -| support | uint8 | undefined | -| weight | uint256 | undefined | -| reason | string | undefined | - -### VotingDelaySet - -```solidity -event VotingDelaySet(uint256 oldVotingDelay, uint256 newVotingDelay) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| oldVotingDelay | uint256 | undefined | -| newVotingDelay | uint256 | undefined | - -### VotingPeriodSet - -```solidity -event VotingPeriodSet(uint256 oldVotingPeriod, uint256 newVotingPeriod) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| oldVotingPeriod | uint256 | undefined | -| newVotingPeriod | uint256 | undefined | - - - diff --git a/echidna.config.yml b/echidna.config.yml deleted file mode 100644 index 0bdc173bd..000000000 --- a/echidna.config.yml +++ /dev/null @@ -1,15 +0,0 @@ -#corpusDir: 'corpus' -#initialize: contracts/crytic/init.json -#testMode: benchmark -#testMode: optimization -#testMode: assertion -psender: "0x40000" -deployer: "0x40000" -#sender: ["0x40000", "0x50000", "0x50001"] -sender: ["0x40000"] -testLimit: 5000 -shrinkLimit: 500 - -cryticArgs: ["--solc-remaps", "@openzeppelin=node_modules/@openzeppelin @chainlink=node_modules/@chainlink", "--solc-args", "optimize optimize-runs=800 metadata-hash=none"] -#cryticArgs: ["--compile-force-framework", "hardhat"] -#codeSize: 0xfffffffffff diff --git a/foundry.toml b/foundry.toml index 62a66ebd3..464eeb8d6 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,25 +1,53 @@ -[default] -solc-version = "0.8.12" +[profile.default] +solc-version = "0.8.23" #auto_detect_solc = false cache = true evm_version = 'london' force = false -gas_reports = ["Multiwrap", "SignatureDrop"] +gas_reports = [ + "DropERC721Benchmark", + "DropERC20Benchmark", + "DropERC1155Benchmark", + "TokenERC20Benchmark", + "TokenERC721Benchmark", + "TokenERC1155Benchmark", + "MultiwrapBenchmark", + "SignatureDropBenchmark", + "AirdropERC20Benchmark", + "AirdropERC721Benchmark", + "AirdropERC1155Benchmark", + "NFTStakeBenchmark", + "EditionStakeBenchmark", + "TokenStakeBenchmark", + "PackBenchmark", + "PackVRFDirectBenchmark", + "AccountBenchmark", +] libraries = [] libs = ['lib'] optimizer = true -optimizer_runs = 600 +optimizer_runs = 20 out = 'artifacts_forge' remappings = [ - #' @chainlink=node_modules/@chainlink/contracts/src/', - '@chainlink/contracts/src=node_modules/@chainlink/contracts/src/', + '@uniswap/v3-core/contracts=lib/v3-core/contracts', + '@uniswap/v3-periphery/contracts=lib/v3-periphery/contracts', + '@uniswap/swap-router-contracts/contracts=lib/swap-router-contracts/contracts', + '@chainlink/=lib/chainlink/', + '@openzeppelin/contracts=lib/openzeppelin-contracts/contracts', + '@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/', '@ds-test=lib/ds-test/src/', - '@openzeppelin=node_modules/@openzeppelin/', '@std=lib/forge-std/src/', 'contracts/=contracts/', - 'erc721a-upgradeable/=node_modules/erc721a-upgradeable/', - 'erc721a/=node_modules/erc721a/', + 'erc721a-upgradeable/=lib/ERC721A-Upgradeable/', + 'erc721a/=lib/ERC721A/', + '@thirdweb-dev/dynamic-contracts/=lib/dynamic-contracts/', + 'lib/sstore2=lib/dynamic-contracts/lib/sstore2/', + 'solady/=lib/solady/', + '@seaport/=lib/seaport/contracts/', + 'seaport-types/=lib/seaport/lib/seaport-types/', + 'seaport-core/=lib/seaport/lib/seaport-core/' ] +fs_permissions = [{ access = "read-write", path = "./src/test/smart-wallet/utils"}] src = 'contracts' test = 'src/test' verbosity = 0 diff --git a/funding.json b/funding.json new file mode 100644 index 000000000..b246e7899 --- /dev/null +++ b/funding.json @@ -0,0 +1,5 @@ +{ + "opRetro": { + "projectId": "0xc6052138bbdae5976fa2866f46b0537182d4126c4bb97485738d1b43f2276134" + } +} diff --git a/gasreport.txt b/gasreport.txt new file mode 100644 index 000000000..ce02c209e --- /dev/null +++ b/gasreport.txt @@ -0,0 +1,207 @@ +No files changed, compilation skipped + +Ran 2 tests for src/test/benchmark/MultiwrapBenchmark.t.sol:MultiwrapBenchmarkTest +[PASS] test_benchmark_multiwrap_unwrap() (gas: 152040) +[PASS] test_benchmark_multiwrap_wrap() (gas: 480722) +Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 665.73ms (584.46µs CPU time) + +Ran 5 tests for src/test/benchmark/SignatureDropBenchmark.t.sol:SignatureDropBenchmarkTest +[PASS] test_benchmark_signatureDrop_claim_five_tokens() (gas: 185688) +[PASS] test_benchmark_signatureDrop_lazyMint() (gas: 147153) +[PASS] test_benchmark_signatureDrop_lazyMint_for_delayed_reveal() (gas: 249057) +[PASS] test_benchmark_signatureDrop_reveal() (gas: 49802) +[PASS] test_benchmark_signatureDrop_setClaimConditions() (gas: 100719) +Suite result: ok. 5 passed; 0 failed; 0 skipped; finished in 665.92ms (942.96µs CPU time) + +Ran 3 tests for src/test/benchmark/EditionStakeBenchmark.t.sol:EditionStakeBenchmarkTest +[PASS] test_benchmark_editionStake_claimRewards() (gas: 98765) +[PASS] test_benchmark_editionStake_stake() (gas: 203676) +[PASS] test_benchmark_editionStake_withdraw() (gas: 94296) +Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 666.96ms (533.96µs CPU time) + +Ran 3 tests for src/test/benchmark/NFTStakeBenchmark.t.sol:NFTStakeBenchmarkTest +[PASS] test_benchmark_nftStake_claimRewards() (gas: 99831) +[PASS] test_benchmark_nftStake_stake_five_tokens() (gas: 553577) +[PASS] test_benchmark_nftStake_withdraw() (gas: 96144) +Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 669.06ms (710.17µs CPU time) + +Ran 1 test for src/test/smart-wallet/utils/AABenchmarkPrepare.sol:AABenchmarkPrepare +[PASS] test_prepareBenchmarkFile() (gas: 2955770) +Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 677.51ms (13.32ms CPU time) + +Ran 1 test for src/test/benchmark/AirdropERC20Benchmark.t.sol:AirdropERC20BenchmarkTest +[PASS] test_benchmark_airdropERC20_airdrop() (gas: 32443785) +Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 688.85ms (17.93ms CPU time) + +Ran 1 test for src/test/benchmark/AirdropERC721Benchmark.t.sol:AirdropERC721BenchmarkTest +[PASS] test_benchmark_airdropERC721_airdrop() (gas: 42241588) +Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 701.05ms (26.57ms CPU time) + +Ran 3 tests for src/test/benchmark/PackBenchmark.t.sol:PackBenchmarkTest +[PASS] test_benchmark_pack_addPackContents() (gas: 312595) +[PASS] test_benchmark_pack_createPack() (gas: 1419379) +[PASS] test_benchmark_pack_openPack() (gas: 302612) +Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 181.87ms (2.36ms CPU time) + +Ran 3 tests for src/test/benchmark/TokenERC20Benchmark.t.sol:TokenERC20BenchmarkTest +[PASS] test_benchmark_tokenERC20_mintTo() (gas: 139513) +[PASS] test_benchmark_tokenERC20_mintWithSignature_pay_with_ERC20() (gas: 221724) +[PASS] test_benchmark_tokenERC20_mintWithSignature_pay_with_native_token() (gas: 228786) +Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 188.65ms (1.10ms CPU time) + +Ran 4 tests for src/test/benchmark/TokenERC721Benchmark.t.sol:TokenERC721BenchmarkTest +[PASS] test_benchmark_tokenERC721_burn() (gas: 40392) +[PASS] test_benchmark_tokenERC721_mintTo() (gas: 172834) +[PASS] test_benchmark_tokenERC721_mintWithSignature_pay_with_ERC20() (gas: 301844) +[PASS] test_benchmark_tokenERC721_mintWithSignature_pay_with_native_token() (gas: 308814) +Suite result: ok. 4 passed; 0 failed; 0 skipped; finished in 192.61ms (1.31ms CPU time) + +Ran 4 tests for src/test/benchmark/TokenERC1155Benchmark.t.sol:TokenERC1155BenchmarkTest +[PASS] test_benchmark_tokenERC1155_burn() (gas: 30352) +[PASS] test_benchmark_tokenERC1155_mintTo() (gas: 144229) +[PASS] test_benchmark_tokenERC1155_mintWithSignature_pay_with_ERC20() (gas: 307291) +[PASS] test_benchmark_tokenERC1155_mintWithSignature_pay_with_native_token() (gas: 318712) +Suite result: ok. 4 passed; 0 failed; 0 skipped; finished in 225.96ms (1.36ms CPU time) + +Ran 3 tests for src/test/benchmark/TokenStakeBenchmark.t.sol:TokenStakeBenchmarkTest +[PASS] test_benchmark_tokenStake_claimRewards() (gas: 101098) +[PASS] test_benchmark_tokenStake_stake() (gas: 195556) +[PASS] test_benchmark_tokenStake_withdraw() (gas: 104792) +Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 196.93ms (479.46µs CPU time) + +Ran 1 test for src/test/benchmark/AirdropERC1155Benchmark.t.sol:AirdropERC1155BenchmarkTest +[PASS] test_benchmark_airdropERC1155_airdrop() (gas: 38536544) +Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 341.15ms (19.66ms CPU time) + +Ran 3 tests for src/test/benchmark/PackVRFDirectBenchmark.t.sol:PackVRFDirectBenchmarkTest +[PASS] test_benchmark_packvrf_createPack() (gas: 1392387) +[PASS] test_benchmark_packvrf_openPack() (gas: 150677) +[PASS] test_benchmark_packvrf_openPackAndClaimRewards() (gas: 3621) +Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 201.65ms (2.27ms CPU time) + +Ran 3 tests for src/test/benchmark/DropERC1155Benchmark.t.sol:DropERC1155BenchmarkTest +[PASS] test_benchmark_dropERC1155_claim() (gas: 245552) +[PASS] test_benchmark_dropERC1155_lazyMint() (gas: 146425) +[PASS] test_benchmark_dropERC1155_setClaimConditions_five_conditions() (gas: 525725) +Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 1.20s (706.01ms CPU time) + +Ran 2 tests for src/test/benchmark/DropERC20Benchmark.t.sol:DropERC20BenchmarkTest +[PASS] test_benchmark_dropERC20_claim() (gas: 291508) +[PASS] test_benchmark_dropERC20_setClaimConditions_five_conditions() (gas: 530026) +Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 510.75ms (589.86ms CPU time) + +Ran 23 tests for src/test/benchmark/AirdropBenchmark.t.sol:AirdropBenchmarkTest +[PASS] test_benchmark_airdropClaim_erc1155() (gas: 105358) +[PASS] test_benchmark_airdropClaim_erc20() (gas: 109724) +[PASS] test_benchmark_airdropClaim_erc721() (gas: 108870) +[PASS] test_benchmark_airdropPush_erc1155ReceiverCompliant() (gas: 82427) +[PASS] test_benchmark_airdropPush_erc1155_10() (gas: 370062) +[PASS] test_benchmark_airdropPush_erc1155_100() (gas: 3266571) +[PASS] test_benchmark_airdropPush_erc1155_1000() (gas: 32348198) +[PASS] test_benchmark_airdropPush_erc20_10() (gas: 345649) +[PASS] test_benchmark_airdropPush_erc20_100() (gas: 2976236) +[PASS] test_benchmark_airdropPush_erc20_1000() (gas: 29352084) +[PASS] test_benchmark_airdropPush_erc721ReceiverCompliant() (gas: 86434) +[PASS] test_benchmark_airdropPush_erc721_10() (gas: 426498) +[PASS] test_benchmark_airdropPush_erc721_100() (gas: 3837162) +[PASS] test_benchmark_airdropPush_erc721_1000() (gas: 38107847) +[PASS] test_benchmark_airdropSignature_erc115_10() (gas: 416712) +[PASS] test_benchmark_airdropSignature_erc115_100() (gas: 3458091) +[PASS] test_benchmark_airdropSignature_erc115_1000() (gas: 34334256) +[PASS] test_benchmark_airdropSignature_erc20_10() (gas: 389286) +[PASS] test_benchmark_airdropSignature_erc20_100() (gas: 3138882) +[PASS] test_benchmark_airdropSignature_erc20_1000() (gas: 30936576) +[PASS] test_benchmark_airdropSignature_erc721_10() (gas: 470201) +[PASS] test_benchmark_airdropSignature_erc721_100() (gas: 4009643) +[PASS] test_benchmark_airdropSignature_erc721_1000() (gas: 39692110) +Suite result: ok. 23 passed; 0 failed; 0 skipped; finished in 1.21s (1.25s CPU time) + +Ran 5 tests for src/test/benchmark/DropERC721Benchmark.t.sol:DropERC721BenchmarkTest +[PASS] test_benchmark_dropERC721_claim_five_tokens() (gas: 273303) +[PASS] test_benchmark_dropERC721_lazyMint() (gas: 147052) +[PASS] test_benchmark_dropERC721_lazyMint_for_delayed_reveal() (gas: 248985) +[PASS] test_benchmark_dropERC721_reveal() (gas: 49433) +[PASS] test_benchmark_dropERC721_setClaimConditions_five_conditions() (gas: 529470) +Suite result: ok. 5 passed; 0 failed; 0 skipped; finished in 466.63ms (512.92ms CPU time) + +Ran 14 tests for src/test/benchmark/AccountBenchmark.t.sol:AccountBenchmarkTest +[PASS] test_state_accountReceivesNativeTokens() (gas: 34537) +[PASS] test_state_addAndWithdrawDeposit() (gas: 148780) +[PASS] test_state_contractMetadata() (gas: 114307) +[PASS] test_state_createAccount_viaEntrypoint() (gas: 458192) +[PASS] test_state_createAccount_viaFactory() (gas: 355822) +[PASS] test_state_executeBatchTransaction() (gas: 76066) +[PASS] test_state_executeBatchTransaction_viaAccountSigner() (gas: 488470) +[PASS] test_state_executeBatchTransaction_viaEntrypoint() (gas: 138443) +[PASS] test_state_executeTransaction() (gas: 68891) +[PASS] test_state_executeTransaction_viaAccountSigner() (gas: 471272) +[PASS] test_state_executeTransaction_viaEntrypoint() (gas: 128073) +[PASS] test_state_receiveERC1155NFT() (gas: 66043) +[PASS] test_state_receiveERC721NFT() (gas: 100196) +[PASS] test_state_transferOutsNativeTokens() (gas: 133673) +Suite result: ok. 14 passed; 0 failed; 0 skipped; finished in 1.32s (26.86ms CPU time) + + +Ran 19 test suites in 1.45s (10.97s CPU time): 84 tests passed, 0 failed, 0 skipped (84 total tests) +test_benchmark_packvrf_openPackAndClaimRewards() (gas: 0 (0.000%)) +test_benchmark_pack_createPack() (gas: 6511 (0.461%)) +test_benchmark_airdropERC721_airdrop() (gas: 329052 (0.785%)) +test_benchmark_packvrf_createPack() (gas: 12783 (0.927%)) +test_prepareBenchmarkFile() (gas: 29400 (1.005%)) +test_benchmark_airdropERC20_airdrop() (gas: 375372 (1.171%)) +test_benchmark_airdropERC1155_airdrop() (gas: 452972 (1.189%)) +test_benchmark_multiwrap_wrap() (gas: 7260 (1.533%)) +test_benchmark_nftStake_stake_five_tokens() (gas: 14432 (2.677%)) +test_benchmark_dropERC721_setClaimConditions_five_conditions() (gas: 28976 (5.789%)) +test_benchmark_dropERC20_setClaimConditions_five_conditions() (gas: 29168 (5.824%)) +test_state_createAccount_viaEntrypoint() (gas: 26152 (6.053%)) +test_state_createAccount_viaFactory() (gas: 21700 (6.495%)) +test_benchmark_dropERC1155_setClaimConditions_five_conditions() (gas: 33604 (6.828%)) +test_benchmark_tokenERC1155_mintWithSignature_pay_with_native_token() (gas: 22540 (7.610%)) +test_benchmark_tokenERC721_mintWithSignature_pay_with_native_token() (gas: 21900 (7.633%)) +test_benchmark_editionStake_stake() (gas: 18532 (10.010%)) +test_benchmark_dropERC721_lazyMint_for_delayed_reveal() (gas: 22836 (10.098%)) +test_benchmark_tokenERC20_mintWithSignature_pay_with_native_token() (gas: 21092 (10.155%)) +test_benchmark_signatureDrop_lazyMint_for_delayed_reveal() (gas: 23166 (10.255%)) +test_benchmark_tokenStake_stake() (gas: 18376 (10.371%)) +test_benchmark_tokenERC721_mintTo() (gas: 21282 (14.043%)) +test_benchmark_tokenERC1155_mintWithSignature_pay_with_ERC20() (gas: 40116 (15.015%)) +test_benchmark_tokenERC721_mintWithSignature_pay_with_ERC20() (gas: 39500 (15.057%)) +test_benchmark_tokenERC20_mintTo() (gas: 20927 (17.647%)) +test_benchmark_tokenERC1155_mintTo() (gas: 21943 (17.944%)) +test_benchmark_dropERC721_lazyMint() (gas: 22512 (18.076%)) +test_benchmark_dropERC1155_lazyMint() (gas: 22512 (18.168%)) +test_benchmark_signatureDrop_lazyMint() (gas: 22842 (18.375%)) +test_benchmark_tokenERC20_mintWithSignature_pay_with_ERC20() (gas: 38692 (21.139%)) +test_state_executeBatchTransaction_viaAccountSigner() (gas: 95688 (24.362%)) +test_state_executeTransaction_viaAccountSigner() (gas: 92640 (24.467%)) +test_benchmark_packvrf_openPack() (gas: 30724 (25.613%)) +test_benchmark_dropERC20_claim() (gas: 61003 (26.465%)) +test_state_receiveERC721NFT() (gas: 21572 (27.437%)) +test_benchmark_dropERC721_claim_five_tokens() (gas: 62336 (29.548%)) +test_benchmark_signatureDrop_claim_five_tokens() (gas: 45171 (32.146%)) +test_benchmark_dropERC1155_claim() (gas: 60520 (32.708%)) +test_benchmark_signatureDrop_setClaimConditions() (gas: 27020 (36.663%)) +test_benchmark_pack_addPackContents() (gas: 93407 (42.615%)) +test_benchmark_nftStake_claimRewards() (gas: 31544 (46.193%)) +test_benchmark_tokenStake_claimRewards() (gas: 33544 (49.655%)) +test_benchmark_editionStake_claimRewards() (gas: 33684 (51.757%)) +test_state_transferOutsNativeTokens() (gas: 51960 (63.588%)) +test_state_executeBatchTransaction_viaEntrypoint() (gas: 55528 (66.970%)) +test_state_receiveERC1155NFT() (gas: 26700 (67.865%)) +test_state_executeTransaction_viaEntrypoint() (gas: 52480 (69.424%)) +test_benchmark_multiwrap_unwrap() (gas: 63090 (70.927%)) +test_state_addAndWithdrawDeposit() (gas: 65448 (78.539%)) +test_state_executeBatchTransaction() (gas: 36192 (90.766%)) +test_state_executeTransaction() (gas: 33156 (92.783%)) +test_state_contractMetadata() (gas: 57800 (102.288%)) +test_benchmark_editionStake_withdraw() (gas: 47932 (103.382%)) +test_benchmark_pack_openPack() (gas: 160752 (113.317%)) +test_benchmark_tokenStake_withdraw() (gas: 57396 (121.099%)) +test_benchmark_nftStake_withdraw() (gas: 58068 (152.506%)) +test_state_accountReceivesNativeTokens() (gas: 23500 (212.920%)) +test_benchmark_dropERC721_reveal() (gas: 35701 (259.984%)) +test_benchmark_tokenERC721_burn() (gas: 31438 (351.106%)) +test_benchmark_signatureDrop_reveal() (gas: 39155 (367.756%)) +test_benchmark_tokenERC1155_burn() (gas: 24624 (429.888%)) +Overall gas change: 3375923 (2.652%) diff --git a/hardhat.config.ts b/hardhat.config.ts deleted file mode 100644 index 6f73039e4..000000000 --- a/hardhat.config.ts +++ /dev/null @@ -1,167 +0,0 @@ -import "@nomiclabs/hardhat-etherscan"; -import "@nomiclabs/hardhat-waffle"; -import "@typechain/hardhat"; -import dotenv from "dotenv"; -import "hardhat-abi-exporter"; -import "hardhat-contract-sizer"; -import "hardhat-gas-reporter"; -import "@primitivefi/hardhat-dodoc"; -import { HardhatUserConfig } from "hardhat/config"; -import { NetworkUserConfig } from "hardhat/types"; - -dotenv.config(); - -const chainIds = { - hardhat: 31337, - ganache: 1337, - mainnet: 1, - ropsten: 3, - rinkeby: 4, - goerli: 5, - kovan: 42, - avax: 43114, - avax_testnet: 43113, - fantom: 250, - fantom_testnet: 4002, - polygon: 137, - mumbai: 80001, - optimism: 10, - optimism_testnet: 69, - arbitrum: 42161, - arbitrum_testnet: 421611, -}; - -// Ensure that we have all the environment variables we need. -const testPrivateKey: string = process.env.TEST_PRIVATE_KEY || ""; -const alchemyKey: string = process.env.ALCHEMY_KEY || ""; -const explorerScanKey: string = process.env.SCAN_API_KEY || ""; - -function createTestnetConfig(network: keyof typeof chainIds): NetworkUserConfig { - if (!alchemyKey) { - throw new Error("Missing ALCHEMY_KEY"); - } - - const polygonNetworkName = network === "polygon" ? "mainnet" : "mumbai"; - - let nodeUrl = - chainIds[network] == 137 || chainIds[network] == 80001 - ? `https://polygon-${polygonNetworkName}.g.alchemy.com/v2/${alchemyKey}` - : `https://eth-${network}.alchemyapi.io/v2/${alchemyKey}`; - - switch (network) { - case "optimism": - nodeUrl = `https://opt-mainnet.g.alchemy.com/v2/${alchemyKey}`; - break; - case "optimism_testnet": - nodeUrl = `https://opt-kovan.g.alchemy.com/v2/${alchemyKey}`; - break; - case "arbitrum": - nodeUrl = `https://arb-mainnet.g.alchemy.com/v2/${alchemyKey}`; - break; - case "arbitrum_testnet": - nodeUrl = `https://arb-rinkeby.g.alchemy.com/v2/${alchemyKey}`; - break; - case "avax": - nodeUrl = "https://api.avax.network/ext/bc/C/rpc"; - break; - case "avax_testnet": - nodeUrl = "https://api.avax-test.network/ext/bc/C/rpc"; - break; - case "fantom": - nodeUrl = "https://rpc.ftm.tools"; - break; - case "fantom_testnet": - nodeUrl = "https://rpc.testnet.fantom.network"; - break; - } - - return { - chainId: chainIds[network], - url: nodeUrl, - accounts: [`${testPrivateKey}`], - }; -} - -const config: HardhatUserConfig = { - paths: { - artifacts: "./artifacts", - cache: "./cache", - sources: "./contracts", - tests: "./test", - }, - solidity: { - version: "0.8.12", - settings: { - metadata: { - bytecodeHash: "ipfs", - }, - // You should disable the optimizer when debugging - // https://hardhat.org/hardhat-network/#solidity-optimizer-support - optimizer: { - enabled: true, - runs: 600, - }, - }, - }, - abiExporter: { - flat: true, - }, - typechain: { - outDir: "typechain", - target: "ethers-v5", - }, - etherscan: { - apiKey: { - mainnet: process.env.ETHERSCAN_API_KEY || process.env.SCAN_API_KEY, - ropsten: process.env.ETHERSCAN_API_KEY || process.env.SCAN_API_KEY, - rinkeby: process.env.ETHERSCAN_API_KEY || process.env.SCAN_API_KEY, - goerli: process.env.ETHERSCAN_API_KEY || process.env.SCAN_API_KEY, - kovan: process.env.ETHERSCAN_API_KEY || process.env.SCAN_API_KEY, - polygon: process.env.POLYGONSCAN_API_KEY || process.env.SCAN_API_KEY, - polygonMumbai: process.env.POLYGONSCAN_API_KEY || process.env.SCAN_API_KEY, - opera: process.env.FANTOMSCAN_API_KEY || process.env.SCAN_API_KEY, - ftmTestnet: process.env.FANTOMSCAN_API_KEY || process.env.SCAN_API_KEY, - avalanche: process.env.SNOWTRACE_API_KEY || process.env.SCAN_API_KEY, - avalancheFujiTestnet: process.env.SNOWTRACE_API_KEY || process.env.SCAN_API_KEY, - optimisticEthereum: process.env.OPTIMISM_SCAN_API_KEY || process.env.SCAN_API_KEY, - optimisticKovan: process.env.OPTIMISM_SCAN_API_KEY || process.env.SCAN_API_KEY, - arbitrumOne: process.env.ARBITRUM_SCAN_API_KEY || process.env.SCAN_API_KEY, - arbitrumTestnet: process.env.ARBITRUM_SCAN_API_KEY || process.env.SCAN_API_KEY, - }, - }, - gasReporter: { - coinmarketcap: process.env.REPORT_GAS_COINMARKETCAP_API_KEY, - currency: "USD", - enabled: process.env.REPORT_GAS ? true : false, - }, - dodoc: { - runOnCompile: true, - }, -}; - -if (testPrivateKey) { - config.networks = { - mainnet: createTestnetConfig("mainnet"), - goerli: createTestnetConfig("goerli"), - rinkeby: createTestnetConfig("rinkeby"), - polygon: createTestnetConfig("polygon"), - mumbai: createTestnetConfig("mumbai"), - fantom: createTestnetConfig("fantom"), - fantom_testnet: createTestnetConfig("fantom_testnet"), - avax: createTestnetConfig("avax"), - avax_testnet: createTestnetConfig("avax_testnet"), - arbitrum: createTestnetConfig("arbitrum"), - arbitrum_testnet: createTestnetConfig("arbitrum_testnet"), - optimism: createTestnetConfig("optimism"), - optimism_testnet: createTestnetConfig("optimism_testnet"), - }; -} - -config.networks = { - ...config.networks, - hardhat: { - chainId: 1337, - }, -}; - -export default config; diff --git a/lib/ERC721A b/lib/ERC721A new file mode 160000 index 000000000..17fb77ffc --- /dev/null +++ b/lib/ERC721A @@ -0,0 +1 @@ +Subproject commit 17fb77ffce10bb9a2bb94cac1fea17e2bf9e8a27 diff --git a/lib/ERC721A-Upgradeable b/lib/ERC721A-Upgradeable new file mode 160000 index 000000000..80b4afb37 --- /dev/null +++ b/lib/ERC721A-Upgradeable @@ -0,0 +1 @@ +Subproject commit 80b4afb376ba1e886053c5aa82af852b3a09ba58 diff --git a/lib/chainlink b/lib/chainlink new file mode 160000 index 000000000..5d44bd4e8 --- /dev/null +++ b/lib/chainlink @@ -0,0 +1 @@ +Subproject commit 5d44bd4e8fa2bdc80228a0df891960d72246b645 diff --git a/lib/ds-test b/lib/ds-test index 9310e879d..e282159d5 160000 --- a/lib/ds-test +++ b/lib/ds-test @@ -1 +1 @@ -Subproject commit 9310e879db8ba3ea6d5c6489a579118fd264a3f5 +Subproject commit e282159d5170298eb2455a6c05280ab5a73a4ef0 diff --git a/lib/dynamic-contracts b/lib/dynamic-contracts new file mode 160000 index 000000000..14af36f8f --- /dev/null +++ b/lib/dynamic-contracts @@ -0,0 +1 @@ +Subproject commit 14af36f8f3af50d7d4ccfe6f16df589b27edd662 diff --git a/lib/forge-std b/lib/forge-std index 6f3b43cad..2f1126975 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 6f3b43cad7476a5aa7be5c5f27388f0ce40b0424 +Subproject commit 2f112697506eab12d433a65fdc31a639548fe365 diff --git a/lib/murky b/lib/murky new file mode 160000 index 000000000..40de6e801 --- /dev/null +++ b/lib/murky @@ -0,0 +1 @@ +Subproject commit 40de6e80117f39cda69d71b07b7c824adac91b29 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 000000000..dc44c9f1a --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit dc44c9f1a4c3b10af99492eed84f83ed244203f6 diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 000000000..2d081f24c --- /dev/null +++ b/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit 2d081f24cac1a867f6f73d512f2022e1fa987854 diff --git a/lib/seaport b/lib/seaport new file mode 160000 index 000000000..1d12e33b7 --- /dev/null +++ b/lib/seaport @@ -0,0 +1 @@ +Subproject commit 1d12e33b71b6988cbbe955373ddbc40a87bd5b16 diff --git a/lib/seaport-core b/lib/seaport-core new file mode 160000 index 000000000..d4e8c74ad --- /dev/null +++ b/lib/seaport-core @@ -0,0 +1 @@ +Subproject commit d4e8c74adc472b311ab64b5c9f9757b5bba57a15 diff --git a/lib/seaport-sol b/lib/seaport-sol new file mode 160000 index 000000000..040d00576 --- /dev/null +++ b/lib/seaport-sol @@ -0,0 +1 @@ +Subproject commit 040d005768abafe3308b5f996aca3fd843d9c20e diff --git a/lib/seaport-types b/lib/seaport-types new file mode 160000 index 000000000..25bae8ddf --- /dev/null +++ b/lib/seaport-types @@ -0,0 +1 @@ +Subproject commit 25bae8ddfa8709e5c51ab429fe06024e46a18f15 diff --git a/lib/solady b/lib/solady new file mode 160000 index 000000000..c6738e402 --- /dev/null +++ b/lib/solady @@ -0,0 +1 @@ +Subproject commit c6738e40225288842ce890cd265a305684e52c3d diff --git a/lib/swap-router-contracts b/lib/swap-router-contracts new file mode 160000 index 000000000..c696aada4 --- /dev/null +++ b/lib/swap-router-contracts @@ -0,0 +1 @@ +Subproject commit c696aada49b33c8e764e6f0bd0a0a56bd8aa455f diff --git a/lib/v3-core b/lib/v3-core new file mode 160000 index 000000000..e3589b192 --- /dev/null +++ b/lib/v3-core @@ -0,0 +1 @@ +Subproject commit e3589b192d0be27e100cd0daaf6c97204fdb1899 diff --git a/lib/v3-periphery b/lib/v3-periphery new file mode 160000 index 000000000..80f26c86c --- /dev/null +++ b/lib/v3-periphery @@ -0,0 +1 @@ +Subproject commit 80f26c86c57b8a5e4b913f42844d4c8bd274d058 diff --git a/manticore/weth.py b/manticore/weth.py deleted file mode 100644 index 65407a900..000000000 --- a/manticore/weth.py +++ /dev/null @@ -1,62 +0,0 @@ -from manticore.ethereum import ManticoreEVM, ABI -from manticore.core.smtlib import Operators, Z3Solver -from manticore.utils import config -from manticore.core.plugin import Plugin - -m = ManticoreEVM() - -# Disable the gas tracking -consts_evm = config.get_group("evm") -consts_evm.oog = "ignore" - -# Increase the solver timeout -config.get_group("smt").defaultunsat = False -config.get_group("smt").timeout = 3600 - -ETHER = 10 ** 18 - -deployer = m.create_account(balance=100 * ETHER, name="deployer") -user = m.create_account(balance=100 * ETHER, name="user") -attacker = m.create_account(balance=100 * ETHER, name="attacker") -print(f'[+] Created user wallet. deployer: {hex(deployer.address)}, user: {hex(user.address)}, attacker: {hex(attacker.address)}') - -contract = m.solidity_create_contract('src/test/mocks/WETH9.sol', contract_name='WETH9', owner=deployer, compile_args={ - "solc_remaps": "@openzeppelin=node_modules/@openzeppelin @chainlink=node_modules/@chainlink", - "solc_args": "optimize optimize-runs=800 metadata-hash=none" -}) -print(f'[+] Deployed contract. address: {hex(contract.address)}') - -print(f'[+] Declaring symbolic variables.') -x = m.make_symbolic_value() -v = m.make_symbolic_value() - -print(f'[+] Calling contract functions sequences.') - -# contrainsts -m.constrain(x > 0) - -# outline the transactions -m.transaction(caller=attacker, address=contract, value=v, data=m.make_symbolic_buffer(4+32*4)) -contract.withdraw(x, caller=attacker) - -print(f"[+] There are {m.count_all_states()} states. ({m.count_ready_states()} ready, {m.count_terminated_states()} terminated, {m.count_busy_states()} alive).") -m.take_snapshot() - -print(f'[+] Generating symbolic conditional transactions traces.') -num_state_found = 0 -for state in m.ready_states: - # withdrawing more than deposited - condition = x > v - if m.generate_testcase(state, only_if=condition, name="constraint"): - num_state_found += 1 -print(f"[+] {num_state_found} constraints test cases generated.") - -m.goto_snapshot() -m.take_snapshot() -print(f'[+] Finalizing transactions.') -m.finalize(only_alive_states=True) - -m.goto_snapshot() -m.take_snapshot() -print(f"[+] Global coverage: {contract.address:x} - {m.global_coverage(contract)}%") -print(f'[+] Results in workspace: {m.workspace}') diff --git a/package.json b/package.json index 9a2dd1ff1..4f6f888c2 100644 --- a/package.json +++ b/package.json @@ -1,69 +1,63 @@ { "name": "@thirdweb-dev/contracts", "description": "", - "version": "2.3.0-6", + "version": "3.11.4", "license": "Apache-2.0", "source": "typechain/index.ts", "files": [ "/contracts/**/*.sol" ], "devDependencies": { - "@chainlink/contracts": "^0.4.0", - "@nomiclabs/hardhat-ethers": "^2.0.5", - "@nomiclabs/hardhat-etherscan": "^3.0.3", - "@nomiclabs/hardhat-waffle": "^2.0.3", - "@openzeppelin/contracts": "4.5.0", - "@openzeppelin/contracts-upgradeable": "4.5.1", - "@primitivefi/hardhat-dodoc": "^0.1.3", - "@typechain/ethers-v5": "^10.0.0", - "@typechain/hardhat": "^4.0.0", + "@openzeppelin/contracts": "^4.9.6", + "@openzeppelin/contracts-upgradeable": "^4.9.6", + "@thirdweb-dev/dynamic-contracts": "^1.2.4", + "@thirdweb-dev/merkletree": "^0.2.6", + "@typechain/ethers-v5": "^10.2.1", "@types/fs-extra": "^9.0.13", - "@types/mocha": "^9.1.0", - "@types/node": "^17.0.21", - "@typescript-eslint/eslint-plugin": "^5.13.0", - "@typescript-eslint/parser": "^5.13.0", - "dotenv": "^16.0.0", + "@types/mocha": "^9.1.1", + "@types/node": "^17.0.45", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "dotenv": "^16.3.1", "erc721a": "3.3.0", "erc721a-upgradeable": "^3.3.0", - "eslint": "^8.10.0", - "eslint-config-prettier": "^8.5.0", - "ethereum-waffle": "^3.4.0", - "fs-extra": "^10.0.1", - "hardhat": "^2.9.0", - "hardhat-abi-exporter": "^2.4.1", - "hardhat-contract-sizer": "^2.5.0", - "hardhat-gas-reporter": "^1.0.8", + "eslint": "^8.54.0", + "eslint-config-prettier": "^8.10.0", + "ethers": "^5.7.2", + "fs-extra": "^10.1.0", "keccak256": "^1.0.6", - "merkletreejs": "^0.2.31", - "mocha": "^9.2.1", - "prettier": "^2.5.1", - "prettier-plugin-solidity": "^1.0.0-beta.19", - "solhint": "^3.3.7", + "mocha": "^9.2.2", + "prettier": "^2.8.8", + "prettier-plugin-solidity": "^1.2.0", + "solady": "0.0.180", + "solhint": "^3.6.2", "solhint-plugin-prettier": "^0.0.5", - "ts-node": "^10.6.0", - "tslib": "^2.3.1", - "tsup": "^5.11.11", - "typechain": "^8.0.0", - "typescript": "^4.4.4" + "ts-node": "^10.9.1", + "tslib": "^2.6.2", + "tsup": "^5.12.9", + "typechain": "^8.3.2", + "typescript": "^4.9.5" }, "peerDependencies": { "ethers": "^5.0.0" }, "resolutions": { - "typescript": "^4.4.4" + "typescript": "^5.3.2" }, "scripts": { - "clean": "hardhat clean && rm -rf abi/ && rm -rf artifacts/ && rm -rf dist/ && rm -rf typechain/", - "compile": "hardhat compile", + "clean": "forge clean && rm -rf abi/ && rm -rf artifacts_forge/ && rm -rf contract_artifacts && rm -rf dist/ && rm -rf typechain/", + "compile": "forge build && npx ts-node scripts/package-release.ts", "lint": "solhint \"contracts/**/*.sol\"", - "prettier": "prettier --config .prettierrc --write \"{contracts,src}/**/*.{js,json,sol,ts}\"", - "prettier:list-different": "prettier --config .prettierrc --list-different \"**/*.{js,json,sol,ts}\"", - "prettier:contracts": "prettier --config .prettierrc --list-different \"{contracts,src}/**/*.sol\"", - "test": "hardhat test", - "typechain": "hardhat typechain", + "prettier": "prettier --config .prettierrc --write --plugin=prettier-plugin-solidity '{contracts,src}/**/*.sol'", + "prettier:list-different": "prettier --config .prettierrc --plugin=prettier-plugin-solidity --list-different '**/*.sol'", + "prettier:contracts": "prettier --config .prettierrc --plugin=prettier-plugin-solidity --list-different '{contracts,src}/**/*.sol'", + "test": "forge test", + "typechain": "typechain --target ethers-v5 --out-dir ./typechain artifacts_forge/**/*.json", "build": "yarn clean && yarn compile", - "forge:build": "forge build --hardhat", - "forge:test": "forge test --hardhat", - "export-abi": "hardhat export-abi" + "forge:build": "forge build", + "forge:test": "forge test", + "gas": "forge snapshot --isolate --mc Benchmark --gas-report --diff .gas-snapshot > gasreport.txt", + "forge:snapshot": "forge snapshot --check", + "aabenchmark": "forge test --mc AABenchmarkPrepare && forge test --mc ProfileThirdwebAccount -vvv" } } diff --git a/release.sh b/release.sh index 6af49096d..4ecc4eb49 100755 --- a/release.sh +++ b/release.sh @@ -35,9 +35,7 @@ echo "### Build finished. Copying abis." rm -rf contracts/abi mkdir -p contracts/abi # copy all abis to contracts/abi -find artifacts/contracts ! -iregex ".*([a-zA-Z0-9_]).json" -exec cp {} contracts/abi 2>/dev/null \; -# remove non-abi files -rm contracts/abi/*.dbg.json +find contract_artifacts ! -iregex ".*([a-zA-Z0-9_]).json" -exec cp {} contracts/abi 2>/dev/null \; echo "### Copying README." # copy root README to contracts folder cp README.md contracts/README.md @@ -51,6 +49,10 @@ np --any-branch --no-tests fi # delete copied README rm README.md +# delete copied README +rm -rf node_modules +# delete copied README +rm -rf abi # back to root folder cd - echo "### Done." \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index d298aa104..000000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -manticore==0.3.7 diff --git a/scripts/addImplementation.ts b/scripts/addImplementation.ts deleted file mode 100644 index 854b923c7..000000000 --- a/scripts/addImplementation.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { ethers } from "hardhat"; - -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { TWFactory } from "typechain"; - -async function main() { - - const [caller]: SignerWithAddress[] = await ethers.getSigners(); - - console.log("\nCaller address: ", caller.address); - - const twFactoryAddress: string = "0x5DBC7B840baa9daBcBe9D2492E45D7244B54A2A0"; // replace - const twFactory: TWFactory = await ethers.getContractAt("TWFactory", twFactoryAddress); - - const hasFactoryRole = await twFactory.hasRole( - ethers.utils.solidityKeccak256(["string"], ["FACTORY_ROLE"]), - caller.address - ) - if(!hasFactoryRole) { - throw new Error("Caller does not have FACTORY_ROLE on new factory"); - } - - const implementations: string[] = []; // replace - const data = implementations.map((impl) => twFactory.interface.encodeFunctionData("addImplementation", [impl])); - - const tx = await twFactory.multicall(data); - console.log("Adding implementations: ", tx.hash); - - await tx.wait(); - - console.log("Done."); -} - -main() - .then(() => process.exit(0)) - .catch((e) => { - console.error(e) - process.exit(1) - }) \ No newline at end of file diff --git a/scripts/bumpNonce.ts b/scripts/bumpNonce.ts deleted file mode 100644 index bb0d1cf7c..000000000 --- a/scripts/bumpNonce.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { ethers } from "hardhat"; - -async function printNonce() { - const [deployer]: SignerWithAddress[] = await ethers.getSigners(); - - console.log("\nNonce of deployer:\nAddress: ", deployer.address, "\nNonce: ", await deployer.getTransactionCount("latest")); -} - -async function bumpNonce(factor: number) { - - const [deployer]: SignerWithAddress[] = await ethers.getSigners(); - - console.log("\nCurrent nonce of deployer:\nAddress: ", deployer.address, "\nNonce: ", await deployer.getTransactionCount("latest")); - - for(let i = 0; i < factor; i += 1) { - - const tx = await deployer.sendTransaction({ - to: deployer.address, - value: 1 - }); - - console.log("\nBumping nonce at tx: ", tx.hash); - - await tx.wait() - - console.log("New nonce: ", await deployer.getTransactionCount("latest")); - } - - console.log("Done. Nonce of deployer: ", await deployer.getTransactionCount("latest")); -} - -// printNonce() -// .then(() => process.exit(0)) -// .catch((e) => { -// console.error(e) -// process.exit(1) -// }) - -// bumpNonce(1) -// .then(() => process.exit(0)) -// .catch((e) => { -// console.error(e) -// process.exit(1) -// }) \ No newline at end of file diff --git a/scripts/data/preBuiltDeploys.ts b/scripts/data/preBuiltDeploys.ts deleted file mode 100644 index 572d84b68..000000000 --- a/scripts/data/preBuiltDeploys.ts +++ /dev/null @@ -1,37 +0,0 @@ -import hre, { ethers } from "hardhat"; -import { TWRegistry } from "typechain"; -import { AddedEvent } from "typechain/contracts/TWRegistry"; - -// ==================================== - -// REPLACE according to desired date range. -const startBlock: number = 0; -const endBlock: number = 0; - -async function main() { - - const chainId: number = hre.network.config.chainId as number; - console.log(`\nGetting the number of pre-built contracts deployed:\nChain: ${chainId}\nStart block: ${startBlock} End block: ${endBlock}`) - - const twRegistry: TWRegistry = await ethers.getContractAt("TWRegistry", "0x7c487845f98938Bb955B1D5AD069d9a30e4131fd"); - - const filter = twRegistry.filters.Added(); - let events: AddedEvent[]; - - if(!startBlock && !endBlock) { - events = await twRegistry.queryFilter(filter); - } else if (!endBlock) { - events = await twRegistry.queryFilter(filter, startBlock); - } else { - events = await twRegistry.queryFilter(filter, startBlock, endBlock); - } - - console.log("Total number of pre-built contracts deployed: ", events.length); -} - -main() - .then(() => process.exit(0)) - .catch(e => { - console.error(e); - process.exit(1); - }); \ No newline at end of file diff --git a/scripts/data/preBuiltDeploysForWallet.ts b/scripts/data/preBuiltDeploysForWallet.ts deleted file mode 100644 index cc9dc9fc0..000000000 --- a/scripts/data/preBuiltDeploysForWallet.ts +++ /dev/null @@ -1,41 +0,0 @@ -import hre, { ethers } from "hardhat"; -import { TWRegistry } from "typechain"; -import { AddedEvent } from "typechain/contracts/TWRegistry"; - -// ==================================== - -// REPLACE according to desired date range. -const startBlock: number = 0; -const endBlock: number = 0; - -// REPLACE -const deployer: string = "0x38abaC1B42ebC9429CB3c9E242dee5eA1104be5d"; - -async function main() { - - const chainId: number = hre.network.config.chainId as number; - console.log(`\nGetting the number of pre-built contracts deployed:\nChain: ${chainId}\nStart block: ${startBlock} End block: ${endBlock}\nDeployer: ${deployer}`) - - const twRegistry: TWRegistry = await ethers.getContractAt("TWRegistry", "0x7c487845f98938Bb955B1D5AD069d9a30e4131fd"); - - const filter = twRegistry.filters.Added(deployer); - let events: AddedEvent[]; - - if(!startBlock && !endBlock) { - events = await twRegistry.queryFilter(filter); - } else if (!endBlock) { - events = await twRegistry.queryFilter(filter, startBlock); - } else { - events = await twRegistry.queryFilter(filter, startBlock, endBlock); - } - - console.log("Total deploys by deployer: ", events.length); - console.log("Deployment addresses: ", events.map(x => x.args.deployment)); -} - -main() - .then(() => process.exit(0)) - .catch(e => { - console.error(e); - process.exit(1); - }); \ No newline at end of file diff --git a/scripts/deploy-prebuilt-deterministic/bootstrap-on-a-chain.ts b/scripts/deploy-prebuilt-deterministic/bootstrap-on-a-chain.ts new file mode 100644 index 000000000..6d38efed3 --- /dev/null +++ b/scripts/deploy-prebuilt-deterministic/bootstrap-on-a-chain.ts @@ -0,0 +1,173 @@ +import "dotenv/config"; +import { + ThirdwebSDK, + computeCloneFactoryAddress, + deployContractDeterministic, + deployCreate2Factory, + deployWithThrowawayDeployer, + fetchAndCacheDeployMetadata, + getCreate2FactoryAddress, + getDeploymentInfo, + getThirdwebContractAddress, + isContractDeployed, + resolveAddress, +} from "@thirdweb-dev/sdk"; +import { Signer } from "ethers"; +import { apiMap, chainIdApiKey, contractsToDeploy } from "./constants"; + +////// To run this script: `npx ts-node scripts/deploy-prebuilt-deterministic/bootstrap-on-a-chain.ts` ////// +///// MAKE SURE TO PUT IN THE RIGHT CONTRACT NAME HERE AFTER PUBLISHING IT ///// +//// THE CONTRACT SHOULD BE PUBLISHED WITH THE NEW PUBLISH FLOW //// + +const publisherKey: string = process.env.THIRDWEB_PUBLISHER_PRIVATE_KEY as string; +const deployerKey: string = process.env.PRIVATE_KEY as string; + +const polygonSDK = ThirdwebSDK.fromPrivateKey(publisherKey, "polygon"); + +const chainId = "8453"; // update here +const networkName = "base"; // update here + +async function main() { + const publisher = await polygonSDK.wallet.getAddress(); + + const sdk = ThirdwebSDK.fromPrivateKey(deployerKey, chainId); // can also hardcode the chain here + const signer = sdk.getSigner() as Signer; + + console.log("balance: ", await sdk.wallet.balance()); + + // Deploy CREATE2 factory (if not already exists) + const create2FactoryAddress = await getCreate2FactoryAddress(sdk.getProvider()); + if (await isContractDeployed(create2FactoryAddress, sdk.getProvider())) { + console.log(`-- Create2 factory already present at ${create2FactoryAddress}\n`); + } else { + console.log(`-- Deploying Create2 factory at ${create2FactoryAddress}\n`); + await deployCreate2Factory(signer, {}); + } + + // TWStatelessFactory (Clone factory) + const cloneFactoryAddress = await computeCloneFactoryAddress(sdk.getProvider(), sdk.storage, create2FactoryAddress); + if (await isContractDeployed(cloneFactoryAddress, sdk.getProvider())) { + console.log(`-- TWCloneFactory present at ${cloneFactoryAddress}\n`); + } + + for (const publishedContractName of contractsToDeploy) { + const latest = await polygonSDK.getPublisher().getLatest(publisher, publishedContractName); + + if (latest && latest.metadataUri) { + const { extendedMetadata } = await fetchAndCacheDeployMetadata(latest?.metadataUri, polygonSDK.storage); + + const isNetworkEnabled = + extendedMetadata?.networksForDeployment?.networksEnabled.includes(parseInt(chainId)) || + extendedMetadata?.networksForDeployment?.allNetworks; + + if (extendedMetadata?.networksForDeployment && !isNetworkEnabled) { + console.log(`Deployment of ${publishedContractName} disabled on ${networkName}\n`); + continue; + } + + console.log(`Deploying ${publishedContractName} on ${networkName}`); + + // const chainId = (await sdk.getProvider().getNetwork()).chainId; + + try { + const implAddr = await getThirdwebContractAddress(publishedContractName, parseInt(chainId), sdk.storage); + if (implAddr) { + console.log(`implementation ${implAddr} already deployed on chainId: ${chainId}`); + console.log(); + continue; + } + } catch (error) {} + + try { + // any evm deployment flow + + // get deployment info for any evm + const deploymentInfo = await getDeploymentInfo( + latest.metadataUri, + sdk.storage, + sdk.getProvider(), + create2FactoryAddress, + ); + + const implementationAddress = deploymentInfo.find(i => i.type === "implementation")?.transaction + .predictedAddress as string; + + const isDeployed = await isContractDeployed(implementationAddress, sdk.getProvider()); + if (isDeployed) { + console.log(`implementation ${implementationAddress} already deployed on chainId: ${chainId}`); + console.log(); + continue; + } + + console.log("Deploying as", await signer?.getAddress()); + // filter out already deployed contracts (data is empty) + const transactionsToSend = deploymentInfo.filter(i => i.transaction.data && i.transaction.data.length > 0); + const transactionsforDirectDeploy = transactionsToSend + .filter(i => { + return i.type !== "infra"; + }) + .map(i => i.transaction); + const transactionsForThrowawayDeployer = transactionsToSend + .filter(i => { + return i.type === "infra"; + }) + .map(i => i.transaction); + + // deploy via throwaway deployer, multiple infra contracts in one transaction + if (transactionsForThrowawayDeployer.length > 0) { + console.log("-- Deploying Infra"); + await deployWithThrowawayDeployer(signer, transactionsForThrowawayDeployer, {}); + } + + const resolvedImplementationAddress = await resolveAddress(implementationAddress); + + console.log(`-- Deploying ${publishedContractName} at ${resolvedImplementationAddress}`); + // send each transaction directly to Create2 factory + // process txns one at a time + for (const tx of transactionsforDirectDeploy) { + try { + await deployContractDeterministic(signer, tx, {}); + } catch (e) { + console.debug(`Error deploying contract at ${tx.predictedAddress}`, (e as any)?.message); + } + } + console.log(); + } catch (e) { + console.log("Error while deploying: ", e); + console.log(); + continue; + } + } else { + console.log("No previous release found"); + return; + } + } + + console.log("Deployments done."); + console.log(); + + console.log("---------- Verification ---------"); + console.log(); + for (const publishedContractName of contractsToDeploy) { + try { + await sdk.verifier.verifyThirdwebContract( + publishedContractName, + apiMap[parseInt(chainId)], + chainIdApiKey[parseInt(chainId)] as string, + ); + console.log(); + } catch (error) { + console.log(error); + console.log(); + } + } + + console.log("All done."); +} + +main() + .then(() => process.exit(0)) + .catch(e => { + console.error(e); + process.exit(1); + }); diff --git a/scripts/deploy-prebuilt-deterministic/bootstrap-verify.ts b/scripts/deploy-prebuilt-deterministic/bootstrap-verify.ts new file mode 100644 index 000000000..992bc0fc5 --- /dev/null +++ b/scripts/deploy-prebuilt-deterministic/bootstrap-verify.ts @@ -0,0 +1,35 @@ +import { ThirdwebSDK } from "@thirdweb-dev/sdk"; + +import { apiMap, chainIdApiKey, contractsToDeploy } from "./constants"; + +////// To run this script: `npx ts-node scripts/deploy-prebuilt-deterministic/bootstrap-verify.ts` ////// +const chainId = "8453"; // update here + +async function main() { + console.log("---------- Verification ---------"); + console.log(); + + const sdk = new ThirdwebSDK(chainId); + for (const publishedContractName of contractsToDeploy) { + try { + await sdk.verifier.verifyThirdwebContract( + publishedContractName, + apiMap[parseInt(chainId)], + chainIdApiKey[parseInt(chainId)] as string, + ); + console.log(); + } catch (error) { + console.log(error); + console.log(); + } + } + + console.log("All done."); +} + +main() + .then(() => process.exit(0)) + .catch(e => { + console.error(e); + process.exit(1); + }); diff --git a/scripts/deploy-prebuilt-deterministic/constants.ts b/scripts/deploy-prebuilt-deterministic/constants.ts new file mode 100644 index 000000000..d17207c71 --- /dev/null +++ b/scripts/deploy-prebuilt-deterministic/constants.ts @@ -0,0 +1,125 @@ +import { + Arbitrum, + ArbitrumGoerli, + Avalanche, + AvalancheFuji, + Base, + BaseGoerli, + CeloAlfajoresTestnet, + Ethereum, + Goerli, + Linea, + LineaTestnet, + Mumbai, + Optimism, + OptimismGoerli, + Polygon, + Sepolia, +} from "@thirdweb-dev/chains"; +import { ChainId } from "@thirdweb-dev/sdk"; +import dotenv from "dotenv"; + +dotenv.config(); + +export const DEFAULT_CHAINS = [ + Ethereum, + Goerli, + Sepolia, + Polygon, + Mumbai, + Optimism, + OptimismGoerli, + Arbitrum, + ArbitrumGoerli, + Avalanche, + AvalancheFuji, + Base, + BaseGoerli, + Linea, + LineaTestnet, + CeloAlfajoresTestnet, +]; + +export const chainIdToName: Record = { + [ChainId.Mumbai]: "mumbai", + [ChainId.Goerli]: "goerli", + [ChainId.Polygon]: "polygon", + [ChainId.Mainnet]: "mainnet", + [ChainId.Optimism]: "optimism", + [ChainId.OptimismGoerli]: "optimism-goerli", + [ChainId.Arbitrum]: "arbitrum", + [ChainId.ArbitrumGoerli]: "arbitrum-goerli", + [ChainId.Avalanche]: "avalanche", + [ChainId.AvalancheFujiTestnet]: "avalanche-testnet", + [84531]: "base-goerli", + [8453]: "base", +}; + +export const chainIdApiKey: Record = { + [ChainId.Mumbai]: process.env.POLYGONSCAN_API_KEY || process.env.SCAN_API_KEY, + [ChainId.Goerli]: process.env.ETHERSCAN_API_KEY || process.env.SCAN_API_KEY, + [Sepolia.chainId]: process.env.ETHERSCAN_API_KEY || process.env.SCAN_API_KEY, + [ChainId.Polygon]: process.env.POLYGONSCAN_API_KEY || process.env.SCAN_API_KEY, + [ChainId.Mainnet]: process.env.ETHERSCAN_API_KEY || process.env.SCAN_API_KEY, + [ChainId.Optimism]: process.env.OPTIMISM_SCAN_API_KEY || process.env.SCAN_API_KEY, + [ChainId.OptimismGoerli]: process.env.OPTIMISM_SCAN_API_KEY || process.env.SCAN_API_KEY, + [ChainId.Arbitrum]: process.env.ARBITRUM_SCAN_API_KEY || process.env.SCAN_API_KEY, + [ChainId.ArbitrumGoerli]: process.env.ARBITRUM_SCAN_API_KEY || process.env.SCAN_API_KEY, + [ChainId.Fantom]: process.env.FANTOMSCAN_API_KEY || process.env.SCAN_API_KEY, + [ChainId.FantomTestnet]: process.env.FANTOMSCAN_API_KEY || process.env.SCAN_API_KEY, + [ChainId.Avalanche]: process.env.SNOWTRACE_API_KEY || process.env.SCAN_API_KEY, + [ChainId.AvalancheFujiTestnet]: process.env.SNOWTRACE_API_KEY || process.env.SCAN_API_KEY, + [ChainId.BinanceSmartChainMainnet]: process.env.BINANCE_SCAN_API_KEY || process.env.SCAN_API_KEY, + [ChainId.BinanceSmartChainTestnet]: process.env.BINANCE_SCAN_API_KEY || process.env.SCAN_API_KEY, + [Base.chainId]: process.env.BASE_SCAN_API_KEY || process.env.SCAN_API_KEY, + [BaseGoerli.chainId]: process.env.BASE_SCAN_API_KEY || process.env.SCAN_API_KEY, + [Linea.chainId]: process.env.LINEA_SCAN_API_KEY || process.env.SCAN_API_KEY, + [LineaTestnet.chainId]: process.env.LINEA_SCAN_API_KEY || process.env.SCAN_API_KEY, +}; + +export const apiMap: Record = { + 1: "https://api.etherscan.io/api", + 5: "https://api-goerli.etherscan.io/api", + [Sepolia.chainId]: "https://api-sepolia.etherscan.io/api", + 10: "https://api-optimistic.etherscan.io/api", + 56: "https://api.bscscan.com/api", + 97: "https://api-testnet.bscscan.com/api", + 137: "https://api.polygonscan.com/api", + 250: "https://api.ftmscan.com/api", + 420: "https://api-goerli-optimistic.etherscan.io/api", + 4002: "https://api-testnet.ftmscan.com/api", + 42161: "https://api.arbiscan.io/api", + 43113: "https://api-testnet.snowtrace.io/api", + 43114: "https://api.snowtrace.io/api", + 421613: "https://api-goerli.arbiscan.io/api", + 80001: "https://api-testnet.polygonscan.com/api", + 84531: "https://api-goerli.basescan.org/api", + 8453: "https://api.basescan.org/api", + [Linea.chainId]: "https://api.lineascan.build/api", + [LineaTestnet.chainId]: "https://api-testnet.lineascan.build/api", +}; + +export const contractsToDeploy = [ + "OpenEditionERC721", + "DropERC721", + "DropERC1155", + "DropERC20", + "TokenERC20", + "TokenERC721", + "TokenERC1155", + "Split", + "VoteERC20", + "NFTStake", + "TokenStake", + "EditionStake", + "AirdropERC20", + "AirdropERC721", + "AirdropERC1155", + "DirectListingsLogic", + "EnglishAuctionsLogic", + "OffersLogic", + "MarketplaceV3", + // "Forwarder", + // "TWCloneFactory", + // "PluginMap", +]; diff --git a/scripts/deploy-prebuilt-deterministic/deploy-deterministic-std-chains.ts b/scripts/deploy-prebuilt-deterministic/deploy-deterministic-std-chains.ts new file mode 100644 index 000000000..2e961fa87 --- /dev/null +++ b/scripts/deploy-prebuilt-deterministic/deploy-deterministic-std-chains.ts @@ -0,0 +1,179 @@ +import "dotenv/config"; +import { + ThirdwebSDK, + computeCloneFactoryAddress, + deployContractDeterministic, + deployCreate2Factory, + deployWithThrowawayDeployer, + fetchAndCacheDeployMetadata, + getCreate2FactoryAddress, + getDeploymentInfo, + getThirdwebContractAddress, + isContractDeployed, + resolveAddress, +} from "@thirdweb-dev/sdk"; +import { Signer } from "ethers"; +import { DEFAULT_CHAINS, apiMap, chainIdApiKey } from "./constants"; + +////// To run this script: `npx ts-node scripts/deploy-prebuilt-deterministic/deploy-deterministic-std-chains.ts` ////// +///// MAKE SURE TO PUT IN THE RIGHT CONTRACT NAME HERE AFTER PUBLISHING IT ///// +//// THE CONTRACT SHOULD BE PUBLISHED WITH THE NEW PUBLISH FLOW //// +const publishedContractName = "MarketplaceV3"; +const publisherAddress: string = "deployer.thirdweb.eth"; +const deployerKey: string = process.env.PRIVATE_KEY as string; +const secretKey: string = process.env.THIRDWEB_SECRET_KEY as string; + +const polygonSDK = new ThirdwebSDK("polygon", { secretKey }); + +async function main() { + const latest = await polygonSDK.getPublisher().getLatest(publisherAddress, publishedContractName); + + if (latest && latest.metadataUri) { + const { extendedMetadata } = await fetchAndCacheDeployMetadata(latest?.metadataUri, polygonSDK.storage); + + for (const chain of DEFAULT_CHAINS) { + const isNetworkEnabled = + extendedMetadata?.networksForDeployment?.networksEnabled.includes(chain.chainId) || + extendedMetadata?.networksForDeployment?.allNetworks; + + if (extendedMetadata?.networksForDeployment && !isNetworkEnabled) { + console.log(`Deployment of ${publishedContractName} disabled on ${chain.slug}\n`); + continue; + } + + console.log(`Deploying ${publishedContractName} on ${chain.slug}`); + const sdk = ThirdwebSDK.fromPrivateKey(deployerKey, chain, { secretKey }); // can also hardcode the chain here + const signer = sdk.getSigner() as Signer; + // const chainId = (await sdk.getProvider().getNetwork()).chainId; + + try { + const implAddr = await getThirdwebContractAddress( + publishedContractName, + chain.chainId, + sdk.storage, + "latest", + sdk.options.clientId, + sdk.options.secretKey, + ); + if (implAddr) { + console.log(`implementation ${implAddr} already deployed on chainId: ${chain.slug}`); + console.log(); + continue; + } + } catch (error) { + // no-op + } + + try { + console.log("Deploying as", await sdk.wallet.getAddress()); + console.log("Balance", await sdk.wallet.balance().then(b => b.displayValue)); + // any evm deployment flow + + // Deploy CREATE2 factory (if not already exists) + const create2FactoryAddress = await getCreate2FactoryAddress(sdk.getProvider()); + if (await isContractDeployed(create2FactoryAddress, sdk.getProvider())) { + console.log(`-- Create2 factory already present at ${create2FactoryAddress}`); + } else { + console.log(`-- Deploying Create2 factory at ${create2FactoryAddress}`); + await deployCreate2Factory(signer, {}); + } + + // TWStatelessFactory (Clone factory) + const cloneFactoryAddress = await computeCloneFactoryAddress( + sdk.getProvider(), + sdk.storage, + create2FactoryAddress, + sdk.options.clientId, + sdk.options.secretKey, + ); + if (await isContractDeployed(cloneFactoryAddress, sdk.getProvider())) { + console.log(`-- TWCloneFactory already present at ${cloneFactoryAddress}`); + } + + // get deployment info for any evm + const deploymentInfo = await getDeploymentInfo( + latest.metadataUri, + sdk.storage, + sdk.getProvider(), + create2FactoryAddress, + sdk.options.clientId, + sdk.options.secretKey, + ); + + const implementationAddress = deploymentInfo.find(i => i.type === "implementation")?.transaction + .predictedAddress as string; + + // filter out already deployed contracts (data is empty) + const transactionsToSend = deploymentInfo.filter(i => i.transaction.data && i.transaction.data.length > 0); + const transactionsforDirectDeploy = transactionsToSend + .filter(i => { + return i.type !== "infra"; + }) + .map(i => i.transaction); + const transactionsForThrowawayDeployer = transactionsToSend + .filter(i => { + return i.type === "infra"; + }) + .map(i => i.transaction); + + // deploy via throwaway deployer, multiple infra contracts in one transaction + if (transactionsForThrowawayDeployer.length > 0) { + console.log("-- Deploying Infra"); + await deployWithThrowawayDeployer(signer, transactionsForThrowawayDeployer, {}); + } + + const resolvedImplementationAddress = await resolveAddress(implementationAddress); + + console.log(`-- Deploying ${publishedContractName} at ${resolvedImplementationAddress}`); + // send each transaction directly to Create2 factory + // process txns one at a time + for (const tx of transactionsforDirectDeploy) { + try { + await deployContractDeterministic(signer, tx, {}); + } catch (e) { + console.debug(`Error deploying contract at ${tx.predictedAddress}`, (e as any)?.message); + } + } + console.log(); + } catch (e) { + console.log("Error while deploying: ", e); + console.log(); + continue; + } + } + + console.log("Deployments done."); + console.log(); + console.log("---------- Verification ---------"); + console.log(); + for (const chain of DEFAULT_CHAINS) { + const sdk = new ThirdwebSDK(chain, { + secretKey, + }); + console.log("Verifying on: ", chain.slug); + try { + await sdk.verifier.verifyThirdwebContract( + publishedContractName, + apiMap[chain.chainId], + chainIdApiKey[chain.chainId] as string, + ); + console.log(); + } catch (error) { + console.log(error); + console.log(); + } + } + } else { + console.log("No previous release found"); + return; + } + + console.log("All done."); +} + +main() + .then(() => process.exit(0)) + .catch(e => { + console.error(e); + process.exit(1); + }); diff --git a/scripts/deploy-prebuilt-deterministic/verify.ts b/scripts/deploy-prebuilt-deterministic/verify.ts new file mode 100644 index 000000000..e8ba37f4d --- /dev/null +++ b/scripts/deploy-prebuilt-deterministic/verify.ts @@ -0,0 +1,42 @@ +import { ThirdwebSDK } from "@thirdweb-dev/sdk"; + +import { DEFAULT_CHAINS, apiMap, chainIdApiKey } from "./constants"; + +////// To run this script: `npx ts-node scripts/deploy-prebuilt-deterministic/verify.ts` ////// +const deployedContractName = "AccountExtension"; +const secretKey: string = process.env.THIRDWEB_SECRET_KEY as string; + +async function main() { + console.log("---------- Verification ---------"); + console.log(); + for (const chain of DEFAULT_CHAINS) { + const sdk = new ThirdwebSDK(chain, { + secretKey, + }); + console.log("Network: ", chain.slug); + try { + await sdk.verifier.verifyThirdwebContract( + deployedContractName, + apiMap[chain.chainId], + chainIdApiKey[chain.chainId] as string, + ); + console.log(); + } catch (error) { + if ((error as Error)?.message?.includes("already verified")) { + console.log("Already verified"); + } else { + console.log(error); + } + console.log(); + } + } + + console.log("All done."); +} + +main() + .then(() => process.exit(0)) + .catch(e => { + console.error(e); + process.exit(1); + }); diff --git a/scripts/deploy/byocSetup.ts b/scripts/deploy/byocSetup.ts deleted file mode 100644 index 106d38fa4..000000000 --- a/scripts/deploy/byocSetup.ts +++ /dev/null @@ -1,51 +0,0 @@ -import hre, { ethers } from "hardhat"; - -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; - -import { ContractPublisher } from "typechain"; - -/** - * - * Deploys the contract publisher and verifies the contract. - */ - -async function verify(address: string, args: any[]) { - try { - return await hre.run("verify:verify", { - address: address, - constructorArguments: args, - }); - } catch (e) { - console.log(address, args, e); - } -} - -async function main() { - const [deployer]: SignerWithAddress[] = await ethers.getSigners(); - console.log("Deployer address:", deployer.address); - - const trustedForwarder: string = "0xc82BbE41f2cF04e3a8efA18F7032BDD7f6d98a81"; - - const contractPublisher: ContractPublisher = await ethers - .getContractFactory("ContractPublisher") - .then(f => f.deploy(trustedForwarder)); - console.log( - "Deploying ContractPublisher at tx: ", - contractPublisher.deployTransaction.hash, - " address: ", - contractPublisher.address, - ); - await contractPublisher.deployTransaction.wait(); - console.log("Deployed ContractPublisher"); - - console.log("\nDone. Now verifying contracts:"); - - await verify(contractPublisher.address, [trustedForwarder]); -} - -main() - .then(() => process.exit(0)) - .catch(e => { - console.error(e); - process.exit(1); - }); diff --git a/scripts/deploy/dropERC1155.ts b/scripts/deploy/dropERC1155.ts deleted file mode 100644 index 2e0ac72cc..000000000 --- a/scripts/deploy/dropERC1155.ts +++ /dev/null @@ -1,42 +0,0 @@ -import hre, { ethers } from "hardhat"; - -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; - -import { TWFactory, DropERC1155 } from "typechain"; - -async function main() { - const [caller]: SignerWithAddress[] = await ethers.getSigners(); - - const dropERC1155: DropERC1155 = await ethers.getContractFactory("DropERC1155").then(f => f.deploy()); - console.log( - "Deploying DropERC1155 \ntransaction: ", - dropERC1155.deployTransaction.hash, - "\naddress: ", - dropERC1155.address, - ); - - await dropERC1155.deployTransaction.wait(); - - console.log("\n"); - - console.log("Verifying contract."); - await verify(dropERC1155.address, []); -} - -async function verify(address: string, args: any[]) { - try { - return await hre.run("verify:verify", { - address: address, - constructorArguments: args, - }); - } catch (e) { - console.log(address, args, e); - } -} - -main() - .then(() => process.exit(0)) - .catch(e => { - console.error(e); - process.exit(1); - }); diff --git a/scripts/deploy/dropERC20.ts b/scripts/deploy/dropERC20.ts deleted file mode 100644 index dfae94028..000000000 --- a/scripts/deploy/dropERC20.ts +++ /dev/null @@ -1,40 +0,0 @@ -import hre, { ethers } from "hardhat"; - -import { DropERC20 } from "typechain"; - -async function main() { - - const dropERC20: DropERC20 = await ethers.getContractFactory("DropERC20").then(f => f.deploy()); - - console.log( - "Deploying DropERC20 \ntransaction: ", - dropERC20.deployTransaction.hash, - "\naddress: ", - dropERC20.address, - ); - - await dropERC20.deployTransaction.wait(); - - console.log("\n"); - - console.log("Verifying contract."); - await verify(dropERC20.address, []); -} - -async function verify(address: string, args: any[]) { - try { - return await hre.run("verify:verify", { - address: address, - constructorArguments: args, - }); - } catch (e) { - console.log(address, args, e); - } -} - -main() - .then(() => process.exit(0)) - .catch(e => { - console.error(e); - process.exit(1); - }); diff --git a/scripts/deploy/dropERC721.ts b/scripts/deploy/dropERC721.ts deleted file mode 100644 index 4b3b5d6a9..000000000 --- a/scripts/deploy/dropERC721.ts +++ /dev/null @@ -1,40 +0,0 @@ -import hre, { ethers } from "hardhat"; - -import { DropERC721 } from "typechain"; - -async function main() { - - const dropERC721: DropERC721 = await ethers.getContractFactory("DropERC721").then(f => f.deploy()); - - console.log( - "Deploying DropERC721 \ntransaction: ", - dropERC721.deployTransaction.hash, - "\naddress: ", - dropERC721.address, - ); - - await dropERC721.deployTransaction.wait(); - - console.log("\n"); - - console.log("Verifying contract."); - await verify(dropERC721.address, []); -} - -async function verify(address: string, args: any[]) { - try { - return await hre.run("verify:verify", { - address: address, - constructorArguments: args, - }); - } catch (e) { - console.log(address, args, e); - } -} - -main() - .then(() => process.exit(0)) - .catch(e => { - console.error(e); - process.exit(1); - }); diff --git a/scripts/deploy/fullV2Setup.ts b/scripts/deploy/fullV2Setup.ts deleted file mode 100644 index c16b9abaa..000000000 --- a/scripts/deploy/fullV2Setup.ts +++ /dev/null @@ -1,217 +0,0 @@ -import hre, { ethers } from "hardhat"; - -import { - DropERC1155, - DropERC20, - DropERC721, - Marketplace, - Multiwrap, - SignatureDrop, - Split, - TokenERC1155, - TokenERC20, - TokenERC721, - TWFee, - VoteERC20, -} from "typechain"; -import { nativeTokenWrapper } from "../../utils/nativeTokenWrapper"; - -async function verify(address: string, args: any[]) { - try { - return await hre.run("verify:verify", { - address: address, - constructorArguments: args, - }); - } catch (e) { - console.log(address, args, e); - } -} - -async function main() { - // Deploy FeeType - const options = { - //maxFeePerGas: ethers.utils.parseUnits("50", "gwei"), - //maxPriorityFeePerGas: ethers.utils.parseUnits("50", "gwei"), - //gasPrice: ethers.utils.parseUnits("100", "gwei"), - gasLimit: 5_000_000, - }; - - const trustedForwarder = await (await ethers.getContractFactory("Forwarder")).deploy(options); - // const trustedForwarder = await ethers.getContractAt("Forwarder", "0x8cbc8B5d71702032904750A66AEfE8B603eBC538"); - console.log("Deploying Trusted Forwarder at tx: ", trustedForwarder.deployTransaction?.hash); - await trustedForwarder.deployed(); - console.log("Trusted Forwarder address: ", trustedForwarder.address); - const trustedForwarderAddress: string = trustedForwarder.address; - - // // Deploy TWRegistry - const thirdwebRegistry = await ( - await ethers.getContractFactory("TWRegistry") - ).deploy(trustedForwarderAddress, options); - // const thirdwebRegistry = await ethers.getContractAt("TWRegistry", "0x7c487845f98938Bb955B1D5AD069d9a30e4131fd"); - console.log("Deploying TWRegistry at tx: ", thirdwebRegistry.deployTransaction?.hash); - await thirdwebRegistry.deployed(); - console.log("TWRegistry address: ", thirdwebRegistry.address); - - // Deploy TWFactory and TWRegistry - const thirdwebFactory = await ( - await ethers.getContractFactory("TWFactory") - ).deploy(trustedForwarderAddress, thirdwebRegistry.address, options); - // const thirdwebFactory = await ethers.getContractAt("TWFactory", "0xd24b3de085CFd8c54b94feAD08a7962D343E6DE0"); - console.log("Deploying TWFactory at tx: ", thirdwebFactory.deployTransaction?.hash); - await thirdwebFactory.deployed(); - console.log("TWFactory address: ", thirdwebFactory.address); - - // Deploy TWFee - const thirdwebFee: TWFee = await ethers - .getContractFactory("TWFee") - .then(f => f.deploy(trustedForwarderAddress, thirdwebFactory.address, options)); - // const thirdwebFee = await ethers.getContractAt("TWFee", "0x8C4B615040Ebd2618e8fC3B20ceFe9abAfdEb0ea"); - console.log("Deploying TWFee at tx: ", thirdwebFee.deployTransaction?.hash); - await thirdwebFee.deployed(); - console.log("TWFee address: ", thirdwebFee.address); - - // Deploy a test implementation: Drop721 - const drop721: DropERC721 = await ethers - .getContractFactory("DropERC721") - .then(f => f.deploy(options)) - .then(f => f.deployed()); - // const drop721 = await ethers.getContractAt("DropERC721", "0xcF4c511551aE4dab1F997866FC3900cd2aaeC40D"); - console.log("Deploying DropERC721 at tx: ", drop721.deployTransaction?.hash); - console.log("DropERC721 address: ", drop721.address); - - // Deploy a test implementation: Drop1155 - const drop1155: DropERC1155 = await ethers - .getContractFactory("DropERC1155") - .then(f => f.deploy(options)) - .then(f => f.deployed()); - console.log("Deploying Drop1155 at tx: ", drop1155.deployTransaction.hash); - console.log("Drop1155 address: ", drop1155.address); - // const drop1155 = await ethers.getContractAt("DropERC1155", "0xb0435b47ad26115A39c59735b814f3769F07C2c1"); - - // Deploy a test implementation: DropERC20 - const drop20: DropERC20 = await ethers - .getContractFactory("DropERC20") - .then(f => f.deploy(options)) - .then(f => f.deployed()); - console.log("Deploying DropERC20 at tx: ", drop20.deployTransaction.hash); - console.log("DropERC20 address: ", drop20.address); - // const drop20 = await ethers.getContractAt("DropERC20", "0x5bB3DCac11fa075D4f362Bb2D0De93fA821f1dA9"); - - // Deploy a test implementation: TokenERC20 - const tokenERC20: TokenERC20 = await ethers - .getContractFactory("TokenERC20") - .then(f => f.deploy(thirdwebFee.address, options)) - .then(f => f.deployed()); - console.log("Deploying TokenERC20 at tx: ", tokenERC20.deployTransaction.hash); - console.log("TokenERC20 address: ", tokenERC20.address); - // const tokenERC20 = await ethers.getContractAt("TokenERC20", "0x0E1d366475709eF677275E4161a20456cAc2071f"); - - // Deploy a test implementation: TokenERC721 - const tokenERC721: TokenERC721 = await ethers - .getContractFactory("TokenERC721") - .then(f => f.deploy(thirdwebFee.address, options)) - .then(f => f.deployed()); - console.log("Deploying TokenERC721 at tx: ", tokenERC721.deployTransaction.hash); - console.log("TokenERC721 address: ", tokenERC721.address); - // const tokenERC721 = await ethers.getContractAt("TokenERC721", "0x7185fBf58db5F3Df186197406CEc2E253A1f5fE6"); - - // Deploy a test implementation: TokenERC1155 - const tokenERC1155: TokenERC1155 = await ethers - .getContractFactory("TokenERC1155") - .then(f => f.deploy(thirdwebFee.address, options)) - .then(f => f.deployed()); - console.log("Deploying TokenERC1155 at tx: ", tokenERC1155.deployTransaction.hash); - console.log("TokenERC1155 address: ", tokenERC1155.address); - // const tokenERC1155 = await ethers.getContractAt("TokenERC1155", "0xe9D53b11d6531b0E56990755a7856889FC59848d"); - - const split: Split = await ethers - .getContractFactory("Split") - .then(f => f.deploy(thirdwebFee.address, options)) - .then(f => f.deployed()); - console.log("Deploying Split at tx: ", split.deployTransaction.hash); - console.log("Split address: ", split.address); - // const split = await ethers.getContractAt("Split", "0x83cCFAaA705Bf3B734B50121d47b82D58aE91796"); - - const marketplace: Marketplace = await ethers - .getContractFactory("Marketplace") - .then(f => f.deploy(nativeTokenWrapper[ethers.provider.network.chainId], thirdwebFee.address, options)) - .then(f => f.deployed()); - console.log("Deploying Marketplace at tx: ", marketplace.deployTransaction.hash); - console.log("Marketplace address: ", marketplace.address); - // const marketplace = await ethers.getContractAt("Marketplace", "0x7181acA2A01Bc98596b1d5375C97389F0d136B2b"); - - const vote: VoteERC20 = await ethers - .getContractFactory("VoteERC20") - .then(f => f.deploy(options)) - .then(f => f.deployed()); - console.log("Deploying vote at tx: ", vote.deployTransaction.hash); - console.log("Vote address: ", vote.address); - // const vote = await ethers.getContractAt("VoteERC20", "0x8F18067D118B1DD1D7a503B0b6Ed255341068029"); - - // Multiwrap - const multiwrap: Multiwrap = await ethers - .getContractFactory("Multiwrap") - .then(f => f.deploy(nativeTokenWrapper[ethers.provider.network.chainId], options)) - .then(f => f.deployed()); - console.log("Deploying Multiwrap at tx: ", multiwrap.deployTransaction.hash); - console.log("Multiwrap address: ", multiwrap.address); - // const multiwrap = await ethers.getContractAt("Multiwrap", "0x10C06F8B3695813276b4A921C06bb3b122aAf9d2"); - - // Signature Drop - const sigdrop: SignatureDrop = await ethers - .getContractFactory("SignatureDrop") - .then(f => f.deploy(options)) - .then(f => f.deployed()); - console.log("Deploying SignatureDrop at tx: ", sigdrop.deployTransaction.hash); - console.log("SignatureDrop address: ", sigdrop.address); - // const sigdrop = await ethers.getContractAt("SignatureDrop", "0xAC4190bFF783B19812137c38E7d3c724b655f1d5"); - - // TODO Pack - - const tx = await thirdwebFactory.multicall( - [ - thirdwebFactory.interface.encodeFunctionData("addImplementation", [drop721.address]), - thirdwebFactory.interface.encodeFunctionData("addImplementation", [drop1155.address]), - thirdwebFactory.interface.encodeFunctionData("addImplementation", [drop20.address]), - thirdwebFactory.interface.encodeFunctionData("addImplementation", [tokenERC20.address]), - thirdwebFactory.interface.encodeFunctionData("addImplementation", [tokenERC721.address]), - thirdwebFactory.interface.encodeFunctionData("addImplementation", [tokenERC1155.address]), - thirdwebFactory.interface.encodeFunctionData("addImplementation", [split.address]), - thirdwebFactory.interface.encodeFunctionData("addImplementation", [marketplace.address]), - thirdwebFactory.interface.encodeFunctionData("addImplementation", [vote.address]), - thirdwebFactory.interface.encodeFunctionData("addImplementation", [multiwrap.address]), - thirdwebFactory.interface.encodeFunctionData("addImplementation", [sigdrop.address]), - ], - options, - ); - console.log("Adding implementations at tx: ", tx.hash); - await tx.wait(); - - const tx2 = await thirdwebRegistry.grantRole(await thirdwebRegistry.OPERATOR_ROLE(), thirdwebFactory.address); - await tx2.wait(); - console.log("grant role: ", tx2.hash); - - console.log("DONE. Now verifying contracts..."); - - await verify(thirdwebRegistry.address, [trustedForwarderAddress]); - await verify(thirdwebFactory.address, [trustedForwarderAddress, thirdwebRegistry.address]); - await verify(thirdwebFee.address, [trustedForwarderAddress, thirdwebFactory.address]); - await verify(drop721.address, []); - await verify(drop1155.address, []); - await verify(drop20.address, []); - await verify(tokenERC20.address, [thirdwebFee.address]); - await verify(tokenERC721.address, [thirdwebFee.address]); - await verify(tokenERC1155.address, [thirdwebFee.address]); - await verify(split.address, [thirdwebFee.address]); - await verify(marketplace.address, [nativeTokenWrapper[ethers.provider.network.chainId], thirdwebFee.address]); - await verify(vote.address, []); - await verify(multiwrap.address, [nativeTokenWrapper[ethers.provider.network.chainId]]); - await verify(sigdrop.address, []); -} - -main() - .then(() => process.exit(0)) - .catch(e => { - console.error(e); - process.exit(1); - }); diff --git a/scripts/deploy/marketplace.ts b/scripts/deploy/marketplace.ts deleted file mode 100644 index 247719592..000000000 --- a/scripts/deploy/marketplace.ts +++ /dev/null @@ -1,68 +0,0 @@ -import hre, { ethers } from "hardhat"; - -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { TWFactory, Marketplace } from "typechain"; - -import { nativeTokenWrapper } from "../../utils/nativeTokenWrapper"; - -async function main() { - - const chainId: number = hre.network.config.chainId as number; - - const [caller]: SignerWithAddress[] = await ethers.getSigners(); - - const nativeTokenWrapperAddress: string = nativeTokenWrapper[chainId]; - const twFeeAddress: string = ethers.constants.AddressZero; // replace - const twFactoryAddress: string = ethers.constants.AddressZero; // replace Fantom: 0x97EA0Fcc552D5A8Fb5e9101316AAd0D62Ea0876B rest: 0x5DBC7B840baa9daBcBe9D2492E45D7244B54A2A0 - - const twFactory: TWFactory = await ethers.getContractAt("TWFactory", twFactoryAddress); - - const hasFactoryRole = await twFactory.hasRole( - ethers.utils.solidityKeccak256(["string"], ["FACTORY_ROLE"]), - caller.address, - ); - if (!hasFactoryRole) { - throw new Error("Caller does not have FACTORY_ROLE on factory"); - } - const marketplace: Marketplace = await ethers - .getContractFactory("Marketplace") - .then(f => f.deploy(nativeTokenWrapperAddress, twFeeAddress, { gasPrice: ethers.utils.parseUnits("300", "gwei") })); - - console.log( - "Deploying Marketplace \ntransaction: ", - marketplace.deployTransaction.hash, - "\naddress: ", - marketplace.address, - ); - - await marketplace.deployed(); - - console.log("\n"); - - const addImplementationTx = await twFactory.addImplementation(marketplace.address); - console.log("Adding Marketplace implementation to TWFactory: ", addImplementationTx.hash); - await addImplementationTx.wait(); - - console.log("\n"); - - console.log("Verifying contract."); - await verify(marketplace.address, [nativeTokenWrapperAddress, twFeeAddress]); -} - -async function verify(address: string, args: any[]) { - try { - return await hre.run("verify:verify", { - address: address, - constructorArguments: args, - }); - } catch (e) { - console.log(address, args, e); - } -} - -main() - .then(() => process.exit(0)) - .catch(e => { - console.error(e); - process.exit(1); - }); diff --git a/scripts/deploy/multiwrap.ts b/scripts/deploy/multiwrap.ts deleted file mode 100644 index 61820fdd4..000000000 --- a/scripts/deploy/multiwrap.ts +++ /dev/null @@ -1,40 +0,0 @@ -import hre, { ethers } from "hardhat"; - -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { Multiwrap } from "typechain"; -import { nativeTokenWrapper } from "../../utils/nativeTokenWrapper"; - -async function main() { - - const [caller]: SignerWithAddress[] = await ethers.getSigners(); - - const chainId: number = hre.network.config.chainId as number; - const nativeTokenWrapperAddress: string = nativeTokenWrapper[chainId]; - - const multiwrap: Multiwrap = await ethers.getContractFactory("Multiwrap").then(f => f.deploy(nativeTokenWrapperAddress)); - console.log("Deploying Multiwrap \ntransaction: ", multiwrap.deployTransaction.hash, "\naddress: ", multiwrap.address); - await multiwrap.deployTransaction.wait(); - - console.log("\n") - - console.log("Verifying contract.") - await verify(multiwrap.address, [nativeTokenWrapperAddress]); -} - -async function verify(address: string, args: any[]) { - try { - return await hre.run("verify:verify", { - address: address, - constructorArguments: args, - }); - } catch (e) { - console.log(address, args, e); - } -} - -main() - .then(() => process.exit(0)) - .catch((e) => { - console.error(e) - process.exit(1) - }) \ No newline at end of file diff --git a/scripts/deploy/pack.ts b/scripts/deploy/pack.ts deleted file mode 100644 index 535888a61..000000000 --- a/scripts/deploy/pack.ts +++ /dev/null @@ -1,35 +0,0 @@ -import hre, { ethers } from "hardhat"; -import { Pack } from "typechain"; -import { nativeTokenWrapper } from "../../utils/nativeTokenWrapper"; - -async function main() { - - const chainId: number = hre.network.config.chainId as number; - const nativeTokenWrapperAddress: string = nativeTokenWrapper[chainId]; - - const pack: Pack = await ethers.getContractFactory("Pack").then(f => f.deploy(nativeTokenWrapperAddress)); - console.log("Deploying Pack \ntransaction: ", pack.deployTransaction.hash, "\naddress: ", pack.address); - await pack.deployTransaction.wait(); - console.log("\n"); - - console.log("Verifying contract"); - await verify(pack.address, [nativeTokenWrapperAddress]); -} - -async function verify(address: string, args: any[]) { - try { - return await hre.run("verify:verify", { - address: address, - constructorArguments: args, - }); - } catch (e) { - console.log(address, args, e); - } -} - -main() -.then(() => process.exit(0)) -.catch((e) => { - console.error(e); - process.exit(1); -}) \ No newline at end of file diff --git a/scripts/deploy/signatureDrop.ts b/scripts/deploy/signatureDrop.ts deleted file mode 100644 index e5e140dda..000000000 --- a/scripts/deploy/signatureDrop.ts +++ /dev/null @@ -1,39 +0,0 @@ -import hre, { ethers } from "hardhat"; - -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; - -import { SignatureDrop } from "typechain"; - -async function main() { - - const [caller]: SignerWithAddress[] = await ethers.getSigners(); - - console.log("\n") - - const sigdrop: SignatureDrop = await ethers.getContractFactory("SignatureDrop").then(f => f.deploy()); - console.log("Deploying SignatureDrop \ntransaction: ", sigdrop.deployTransaction.hash, "\naddress: ", sigdrop.address); - await sigdrop.deployTransaction.wait(); - - console.log("\n") - - console.log("Verifying contract.") - await verify(sigdrop.address, []); -} - -async function verify(address: string, args: any[]) { - try { - return await hre.run("verify:verify", { - address: address, - constructorArguments: args, - }); - } catch (e) { - console.log(address, args, e); - } -} - -main() - .then(() => process.exit(0)) - .catch((e) => { - console.error(e) - process.exit(1) - }) \ No newline at end of file diff --git a/scripts/deploy/twFactory.ts b/scripts/deploy/twFactory.ts deleted file mode 100644 index b3b89dad1..000000000 --- a/scripts/deploy/twFactory.ts +++ /dev/null @@ -1,70 +0,0 @@ -import hre, { ethers } from "hardhat"; - -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; - -import { TWFactory, TWRegistry } from "typechain"; - -/** - * Note: this script deploys a new instance of TWFactory + verifies it on block explorer. - */ - -async function main() { - const [caller]: SignerWithAddress[] = await ethers.getSigners(); - - console.log("\nCaller address: ", caller.address); - console.log("\n"); - - const forwarderAddress: string = ethers.constants.AddressZero; // replace - const registryAddress: string = ethers.constants.AddressZero; // replace - - const twRegistry: TWRegistry = await ethers.getContractAt("TWRegistry", registryAddress); - - const isAdminOnRegistry: boolean = await twRegistry.hasRole( - ethers.utils.solidityKeccak256(["string"], ["DEFAULT_ADMIN_ROLE"]), - caller.address, - ); - if (!isAdminOnRegistry) { - throw new Error("Caller not admin on registry"); - } - - const twFactory: TWFactory = await ethers - .getContractFactory("TWFactory") - .then(f => f.deploy(forwarderAddress, registryAddress)); - - console.log( - "Deploying TWFactory \ntransaction: ", - twFactory.deployTransaction.hash, - "\naddress: ", - twFactory.address, - ); - - console.log("\n"); - - const grantRoleTx = await twRegistry.grantRole( - ethers.utils.solidityKeccak256(["string"], ["OPERATOR_ROLE"]), - twFactory.address, - ); - console.log("Granting OPERATOR_ROLE to factory on registry at tx: ", grantRoleTx.hash); - await grantRoleTx.wait(); - - console.log("Verifying contract."); - await verify(twFactory.address, [forwarderAddress, registryAddress]); -} - -async function verify(address: string, args: any[]) { - try { - return await hre.run("verify:verify", { - address: address, - constructorArguments: args, - }); - } catch (e) { - console.log(address, args, e); - } -} - -main() - .then(() => process.exit(0)) - .catch(e => { - console.error(e); - process.exit(1); - }); diff --git a/scripts/grantRoleTwRegistry.ts b/scripts/grantRoleTwRegistry.ts deleted file mode 100644 index 28c8e3a81..000000000 --- a/scripts/grantRoleTwRegistry.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { ethers } from "hardhat"; - -import { Wallet } from "@ethersproject/wallet"; -import { BytesLike } from "@ethersproject/bytes"; - -import { TWRegistry } from "typechain"; - -async function main() { - const twRegistryAddress: string = ethers.constants.AddressZero; // replace - - const twRegistry: TWRegistry = await ethers.getContractAt("TWRegistry", twRegistryAddress); - - const currentAdminPkey: string = ""; // DO NOT COMMIT - const currentAdmin: Wallet = new ethers.Wallet(currentAdminPkey, ethers.provider); - - const receiverOfRole: string = ethers.constants.AddressZero; - const role: BytesLike = ""; // replace - - const isAdminOnRegistry: boolean = await twRegistry.hasRole( - await twRegistry.DEFAULT_ADMIN_ROLE(), - currentAdmin.address, - ); - if (!isAdminOnRegistry) { - throw new Error("Caller provided is not admin on registry"); - } - - const grantRoleTx = await twRegistry - .connect(currentAdmin) - .grantRole(role, receiverOfRole); - - console.log(`\nGranting admin role to ${receiverOfRole} at tx: ${grantRoleTx.hash}`); - - await grantRoleTx.wait(); - - console.log("Done."); -} - -main() - .then(() => process.exit(0)) - .catch(e => { - console.error(e); - process.exit(1); - }); diff --git a/scripts/migrateTwFactory.ts b/scripts/migrateTwFactory.ts deleted file mode 100644 index e2115caf4..000000000 --- a/scripts/migrateTwFactory.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { ethers } from "hardhat"; - -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { TWFactory } from "typechain"; - -import contractTypes from "utils/contractTypes"; -import { BytesLike } from "@ethersproject/bytes"; - -async function main() { - const [caller]: SignerWithAddress[] = await ethers.getSigners(); - - console.log("\nCaller address: ", caller.address); - - const currentTWFactoryAddress: string = ethers.constants.AddressZero; // replace - const currentTWFactory: TWFactory = await ethers.getContractAt("TWFactory", currentTWFactoryAddress); - - const newTWFactoryAddress: string = ethers.constants.AddressZero; // replace - const newTWFactory: TWFactory = await ethers.getContractAt("TWFactory", newTWFactoryAddress); - - console.log("\nCurrent factory: ", currentTWFactoryAddress, "\nNew factory: ", newTWFactoryAddress); - - const hasFactoryRole = await newTWFactory.hasRole( - ethers.utils.solidityKeccak256(["string"], ["FACTORY_ROLE"]), - caller.address, - ); - if (!hasFactoryRole) { - throw new Error("Caller does not have FACTORY_ROLE on new factory"); - } - - const migratedContractTypes: string[] = []; - const nonMigratedContractTypes: string[] = []; - - for (const contractType of contractTypes) { - console.log(`\nMigrating ${contractType}`); - - const contractTypeBytes: BytesLike = ethers.utils.formatBytes32String(contractType); - - const currentVersion: number = (await currentTWFactory.currentVersion(contractTypeBytes)).toNumber(); - if (currentVersion == 0) { - console.log(`No current implementation available for ${contractType}`); - nonMigratedContractTypes.push(contractType); - continue; - } - - const implementation = await currentTWFactory.implementation(contractTypeBytes, currentVersion); - const addImplementationTx = await newTWFactory.addImplementation(implementation); - console.log(`Migrating implementation of ${contractType} at tx: `, addImplementationTx.hash); - - await addImplementationTx.wait(); - - const implementationOnNewFactory = await newTWFactory.getLatestImplementation(contractTypeBytes); - - if (ethers.utils.getAddress(implementationOnNewFactory) != ethers.utils.getAddress(implementation)) { - console.log("Something went wrong. Failed to migrate contract."); - nonMigratedContractTypes.push(contractType); - } else { - migratedContractTypes.push(contractType); - console.log("Done."); - } - } - - console.log( - "\nMigration complete:\nMigrated contract types; ", - migratedContractTypes, - "\nDid not migrate: ", - nonMigratedContractTypes, - ); -} - -main() - .then(() => process.exit(0)) - .catch(e => { - console.error(e); - process.exit(1); - }); diff --git a/scripts/package-release.ts b/scripts/package-release.ts new file mode 100644 index 000000000..8babd386d --- /dev/null +++ b/scripts/package-release.ts @@ -0,0 +1,59 @@ +import * as fs from "fs-extra"; +import * as path from "path"; + +// Define the paths for the directories +const artifactsForgeDir = path.join(__dirname, "..", "artifacts_forge"); +const contractsDir = path.join(__dirname, "..", "contracts"); +const contractArtifactsDir = path.join(__dirname, "..", "contract_artifacts"); + +const specialCases: string[] = [ + "IRouterState.sol", + "BaseRouter.sol", + "ExtensionManager.sol", + "MockContractPublisher.sol", +]; + +async function getAllSolidityFiles(dir: string): Promise { + const dirents = await fs.readdir(dir, { withFileTypes: true }); + const files = await Promise.all( + dirents.map(dirent => { + const res = path.join(dir, dirent.name); + return dirent.isDirectory() ? getAllSolidityFiles(res) : res; + }), + ); + // Flatten the array and filter for .sol files + return files + .flat() + .filter(file => file.endsWith(".sol")) + .map(file => path.basename(file)); +} + +async function main() { + // Create the contract_artifacts directory + await fs.ensureDir(contractArtifactsDir); + + // Get all directories within artifacts_forge that match *.sol + const artifactDirs = await fs.readdir(artifactsForgeDir); + const validArtifactDirs = artifactDirs.filter(dir => dir.endsWith(".sol")); + + // Get all .sol filenames within contracts (recursively) + const validContractFiles = await getAllSolidityFiles(contractsDir); + + // Check if directory-name matches any Solidity file name from contracts + for (const artifactDir of validArtifactDirs) { + // Removing the .sol extension from the directory name to match with file names + const artifactName = path.basename(artifactDir, ".sol"); + + if (validContractFiles.includes(artifactName + ".sol") || specialCases.includes(artifactName + ".sol")) { + const sourcePath = path.join(artifactsForgeDir, artifactDir); + const destinationPath = path.join(contractArtifactsDir, artifactDir); + await fs.copy(sourcePath, destinationPath); + } + } + + console.log("Done copying matching directories."); +} + +main().catch(error => { + console.error("An error occurred:", error); +}); diff --git a/scripts/release/add_implementations_from_release.ts b/scripts/release/add_implementations_from_release.ts new file mode 100644 index 000000000..a1ee0e31b --- /dev/null +++ b/scripts/release/add_implementations_from_release.ts @@ -0,0 +1,81 @@ +import "dotenv/config"; +import { SUPPORTED_CHAIN_ID, ThirdwebSDK } from "@thirdweb-dev/sdk"; +import { readFileSync } from "fs"; +import { chainIdToName } from "./constants"; + +////// To run this script: `npx ts-node scripts/release/add_implementations_from_release.ts` ////// +///// MAKE SURE TO PUT IN THE RIGHT CONTRACT NAME HERE AFTER CREATING A RELEASE FOR IT ///// +//// THE RELEASE SHOULD HAVE THE IMPLEMENTATIONS ALREADY DEPLOYED AND RECORDED (via dashboard) //// +const releasedContractName = "Multiwrap"; +const privateKey: string = process.env.THIRDWEB_PUBLISHER_PRIVATE_KEY as string; + +const polygonSDK = ThirdwebSDK.fromPrivateKey(privateKey, "polygon"); + +async function main() { + const releaser = await polygonSDK.wallet.getAddress(); + console.log("Releasing as", releaser); + + const latest = await polygonSDK.getPublisher().getLatest(releaser, releasedContractName); + + if (latest && latest.metadataUri) { + console.log(latest); + const prev = await polygonSDK.getPublisher().fetchPublishedContractInfo(latest); + + console.log("Fetched latest version", prev); + const prevReleaseMetadata = prev.publishedMetadata; + + const implementations = prev.publishedMetadata.factoryDeploymentData?.implementationAddresses; + console.log("Implementations", implementations); + + if (!implementations) { + console.log("No implementations to approve"); + return; + } + + // Adding implementations + console.log("Adding implementations..."); + for (const [chainId, implementation] of Object.entries(implementations)) { + const chainName = chainIdToName[parseInt(chainId) as SUPPORTED_CHAIN_ID]; + + if (!chainName) { + console.log("No chainName found for chain: ", chainId); + continue; + } + + const chainSDK = ThirdwebSDK.fromPrivateKey(privateKey, chainName); + const factoryAddr = prevReleaseMetadata?.factoryDeploymentData?.factoryAddresses?.[chainId]; + if (implementation && factoryAddr) { + const factory = await chainSDK.getContractFromAbi( + factoryAddr, + JSON.parse(readFileSync("artifacts_forge/TWFactory.sol/TWFactory.json", "utf-8")).abi, + ); + const approved = await factory.call("approval", implementation); + if (!approved) { + try { + console.log("Adding implementation", implementation, "on", chainName, "to", factoryAddr); + await factory.call("addImplementation", implementation); + } catch (e) { + console.log("Failed to add implementation on", chainName, e); + } + } else { + console.log("Implementation", implementation, "already approved on", chainName); + } + } else { + console.log("No implementation or factory address for", chainName); + } + } + } else { + console.log("No previous release found"); + return; + } + + console.log("All done."); + console.log("Release page:", `https://thirdweb.com/${releaser}/${releasedContractName}`); +} + +main() + .then(() => process.exit(0)) + .catch(e => { + console.error(e); + process.exit(1); + }); diff --git a/scripts/release/approve_implementations_from_release.ts b/scripts/release/approve_implementations_from_release.ts new file mode 100644 index 000000000..862057795 --- /dev/null +++ b/scripts/release/approve_implementations_from_release.ts @@ -0,0 +1,81 @@ +import "dotenv/config"; +import { SUPPORTED_CHAIN_ID, ThirdwebSDK } from "@thirdweb-dev/sdk"; +import { readFileSync } from "fs"; +import { chainIdToName } from "./constants"; + +////// To run this script: `npx ts-node scripts/release/approve_implementations_from_release.ts` ////// +///// MAKE SURE TO PUT IN THE RIGHT CONTRACT NAME HERE AFTER CREATING A RELEASE FOR IT ///// +//// THE RELEASE SHOULD HAVE THE IMPLEMENTATIONS ALREADY DEPLOYED AND RECORDED (via dashboard) //// +const releasedContractName = "TokenERC721"; +const privateKey: string = process.env.THIRDWEB_PUBLISHER_PRIVATE_KEY as string; + +const polygonSDK = ThirdwebSDK.fromPrivateKey(privateKey, "polygon"); + +async function main() { + const releaser = await polygonSDK.wallet.getAddress(); + console.log("Releasing as", releaser); + + const latest = await polygonSDK.getPublisher().getLatest(releaser, releasedContractName); + + if (latest && latest.metadataUri) { + console.log(latest); + const prev = await polygonSDK.getPublisher().fetchPublishedContractInfo(latest); + + console.log("Fetched latest version", prev); + const prevReleaseMetadata = prev.publishedMetadata; + + const implementations = prev.publishedMetadata.factoryDeploymentData?.implementationAddresses; + console.log("Implementations", implementations); + + if (!implementations) { + console.log("No implementations to approve"); + return; + } + + // Approving implementations + console.log("Approving implementations..."); + for (const [chainId, implementation] of Object.entries(implementations)) { + const chainName = chainIdToName[parseInt(chainId) as SUPPORTED_CHAIN_ID]; + + if (!chainName) { + console.log("No chainName found for chain: ", chainId); + continue; + } + + const chainSDK = ThirdwebSDK.fromPrivateKey(privateKey, chainName); + const factoryAddr = prevReleaseMetadata?.factoryDeploymentData?.factoryAddresses?.[chainId]; + if (implementation && factoryAddr) { + const factory = await chainSDK.getContractFromAbi( + factoryAddr, + JSON.parse(readFileSync("artifacts_forge/TWFactory.sol/TWFactory.json", "utf-8")).abi, + ); + const approved = await factory.call("approval", implementation); + if (!approved) { + try { + console.log("Approving implementation", implementation, "on", chainName, "to", factoryAddr); + await factory.call("approveImplementation", implementation, true); + } catch (e) { + console.log("Failed to approve implementation on", chainName, e); + } + } else { + console.log("Implementation", implementation, "already approved on", chainName); + } + } else { + console.log("No implementation or factory address for", chainName); + } + } + } else { + console.log("No previous release found"); + return; + } + + console.log("All done."); + console.log("Release page:", `https://thirdweb.com/${releaser}/${releasedContractName}`); +} + +main() + .then(() => process.exit(0)) + .catch(e => { + console.error(e); + process.exit(1); + }); diff --git a/scripts/release/constants.ts b/scripts/release/constants.ts new file mode 100644 index 000000000..d61d9d9cd --- /dev/null +++ b/scripts/release/constants.ts @@ -0,0 +1,50 @@ +import { ChainId, CONTRACT_ADDRESSES } from "@thirdweb-dev/sdk"; + +export const nativeTokenWrapper: Record = { + 1: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // mainnet + 5: "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6", // goerli + 137: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", // polygon + 80001: "0x9c3C9283D3e44854697Cd22D3Faa240Cfb032889", // mumbai + 43114: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", // avalanche + 43113: "0xd00ae08403B9bbb9124bB305C09058E32C39A48c", // avalanche fuji testnet + 250: "0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83", // fantom + 4002: "0xf1277d1Ed8AD466beddF92ef448A132661956621", // fantom testnet + 10: "0x4200000000000000000000000000000000000006", // optimism + 420: "0x4200000000000000000000000000000000000006", // optimism goerli + 42161: "0x82af49447d8a07e3bd95bd0d56f35241523fbab1", // arbitrum + 421613: "0xe39Ab88f8A4777030A534146A9Ca3B52bd5D43A3", // arbitrum goerli +}; + +export const chainIdToName: Record = { + [ChainId.Mumbai]: "mumbai", + [ChainId.Goerli]: "goerli", + [ChainId.Polygon]: "polygon", + [ChainId.Mainnet]: "mainnet", + [ChainId.Optimism]: "optimism", + [ChainId.OptimismGoerli]: "optimism-goerli", + [ChainId.Arbitrum]: "arbitrum", + [ChainId.ArbitrumGoerli]: "arbitrum-goerli", + [ChainId.Fantom]: "fantom", + [ChainId.FantomTestnet]: "fantom-testnet", + [ChainId.Avalanche]: "avalanche", + [ChainId.AvalancheFujiTestnet]: "avalanche-testnet", + [ChainId.BinanceSmartChainMainnet]: "binance", + [ChainId.BinanceSmartChainTestnet]: "binance-testnet", +}; + +export const defaultFactories: Record = { + [ChainId.Mainnet]: CONTRACT_ADDRESSES[ChainId.Mainnet].twFactory, + [ChainId.Goerli]: CONTRACT_ADDRESSES[ChainId.Goerli].twFactory, + [ChainId.Polygon]: CONTRACT_ADDRESSES[ChainId.Polygon].twFactory, + [ChainId.Mumbai]: CONTRACT_ADDRESSES[ChainId.Mumbai].twFactory, + [ChainId.Fantom]: CONTRACT_ADDRESSES[ChainId.Fantom].twFactory, + [ChainId.FantomTestnet]: CONTRACT_ADDRESSES[ChainId.FantomTestnet].twFactory, + [ChainId.Optimism]: CONTRACT_ADDRESSES[ChainId.Optimism].twFactory, + [ChainId.OptimismGoerli]: CONTRACT_ADDRESSES[ChainId.OptimismGoerli].twFactory, + [ChainId.Arbitrum]: CONTRACT_ADDRESSES[ChainId.Arbitrum].twFactory, + [ChainId.ArbitrumGoerli]: CONTRACT_ADDRESSES[ChainId.ArbitrumGoerli].twFactory, + [ChainId.Avalanche]: CONTRACT_ADDRESSES[ChainId.Avalanche].twFactory, + [ChainId.AvalancheFujiTestnet]: CONTRACT_ADDRESSES[ChainId.AvalancheFujiTestnet].twFactory, + [ChainId.BinanceSmartChainMainnet]: CONTRACT_ADDRESSES[ChainId.BinanceSmartChainMainnet].twFactory, + [ChainId.BinanceSmartChainTestnet]: CONTRACT_ADDRESSES[ChainId.BinanceSmartChainTestnet].twFactory, +}; diff --git a/scripts/revokeRoleTwRegistry.ts b/scripts/revokeRoleTwRegistry.ts deleted file mode 100644 index 7bfa85a22..000000000 --- a/scripts/revokeRoleTwRegistry.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ethers } from "hardhat"; - -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { BytesLike } from "@ethersproject/bytes"; - -import { TWRegistry } from "typechain"; - -async function main() { - const [roleHolder]: SignerWithAddress[] = await ethers.getSigners(); - - const twRegistryAddress: string = ethers.constants.AddressZero; // replace - const twRegistry: TWRegistry = await ethers.getContractAt("TWRegistry", twRegistryAddress); - - const isAdminOnRegistry: boolean = await twRegistry.hasRole( - await twRegistry.DEFAULT_ADMIN_ROLE(), - roleHolder.address, - ); - if (!isAdminOnRegistry) { - throw new Error("Caller provided is not admin on registry"); - } else { - console.log("Caller provided is admin on registry. Revoking role now."); - } - - const role: BytesLike = ""; // replace - - const revokeRoleTx = await twRegistry - .connect(roleHolder) - .revokeRole(role, roleHolder.address); - - console.log(`\nRevoking admin role from ${roleHolder.address} at tx: ${revokeRoleTx.hash}`); - - await revokeRoleTx.wait(); - - console.log("Done."); -} - -main() - .then(() => process.exit(0)) - .catch(e => { - console.error(e); - process.exit(1); - }); diff --git a/scripts/transferBalance.ts b/scripts/transferBalance.ts deleted file mode 100644 index 1653e2c70..000000000 --- a/scripts/transferBalance.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { ethers } from "hardhat"; - -async function main() { - - const receiver: string = ethers.constants.AddressZero; // replace - - const [caller]: SignerWithAddress[] = await ethers.getSigners(); - - console.log(`\nTransferring balance from ${caller.address} to ${receiver}`); - - const balance = await ethers.provider.getBalance(caller.address); - const gasPrice = ethers.utils.parseUnits("0", "gwei"); // replace - const cost = gasPrice.mul(21_000); - - const tx = await caller.sendTransaction({ - to: receiver, - gasPrice: gasPrice, - value: balance.sub(cost) - }); - console.log("Transferring balance: ", tx.hash); - await tx.wait(); - - console.log("Done."); -} - -main() - .then(() => process.exit(0)) - .catch((e) => { - console.error(e) - process.exit(1) - }) \ No newline at end of file diff --git a/scripts/verify.ts b/scripts/verify.ts deleted file mode 100644 index 051df68c8..000000000 --- a/scripts/verify.ts +++ /dev/null @@ -1,26 +0,0 @@ -import hre from "hardhat"; - -async function main() { - await verify( - "", - [] - ) -} - -async function verify(address: string, args: any[]) { - try { - return await hre.run("verify:verify", { - address: address, - constructorArguments: args, - }); - } catch (e) { - console.log(address, args, e); - } -} - -main() - .then(() => process.exit(0)) - .catch((e) => { - console.error(e) - process.exit(1) - }) \ No newline at end of file diff --git a/scripts/verify_contract.ts b/scripts/verify_contract.ts deleted file mode 100644 index f3894c576..000000000 --- a/scripts/verify_contract.ts +++ /dev/null @@ -1,66 +0,0 @@ -import hre, { ethers } from "hardhat"; - -async function verify() { - const txhashs: string[] = []; - for (const txhash of txhashs) { - const types = [ - "TWFee", - "TWRegistry", - "TWFactory", - "TWProxy", - "TokenERC20", - "TokenERC721", - "TokenERC1155", - "DropERC721", - "DropERC1155", - "VoteERC20", - "Split", - "Marketplace", - ]; - - const tx = await ethers.provider.getTransaction(txhash); - const txdata = tx.data; - const address = (tx as any).creates; - - console.log("txhash", txhash, address, txdata.length); - - for (const type of types) { - let contract; - try { - contract = await ethers.getContractFactory(type); - } catch (e) { - console.log("invalid artifacts", type); - continue; - } - const { bytecode } = await hre.artifacts.readArtifact(type); - - // make sure that deployed bytecode matches with contract bytecode - if (txdata.indexOf(bytecode) === -1) { - //console.log("txdata", txdata, txdata.length); - //console.log("bytecode", bytecode, bytecode.length); - console.log("invalid contract bytecode", type); - continue; - } - - const paramsData = `0x${txdata.substring(bytecode.length)}`; - const paramsDeployed = ethers.utils.defaultAbiCoder.decode(contract.interface.deploy.inputs, paramsData); - const paramsArguments = paramsDeployed.length ? paramsDeployed.toString().split(",") : []; - try { - await hre.run("verify:verify", { - address, - constructorArguments: [...paramsArguments], - }); - } catch (e) { - console.error(e); - } - break; - } - } -} - -verify() - .then(() => process.exit(0)) - .catch(err => { - console.error(err); - process.exit(1); - }); diff --git a/src/test/ContractPublisher.t.sol b/src/test/ContractPublisher.t.sol index e227925f8..e80fe7d43 100644 --- a/src/test/ContractPublisher.t.sol +++ b/src/test/ContractPublisher.t.sol @@ -2,12 +2,12 @@ pragma solidity ^0.8.0; // Target contracts -import { ContractPublisher } from "contracts/ContractPublisher.sol"; -import "contracts/interfaces/IContractPublisher.sol"; -import "contracts/TWRegistry.sol"; +import { ContractPublisher } from "contracts/infra/ContractPublisher.sol"; +import "contracts/infra/interface/IContractPublisher.sol"; +import "contracts/infra/TWRegistry.sol"; // Test helpers -import { BaseTest } from "./utils/BaseTest.sol"; +import { BaseTest, MockContractPublisher } from "./utils/BaseTest.sol"; import "@openzeppelin/contracts/utils/Create2.sol"; contract MockCustomContract { @@ -114,6 +114,84 @@ contract ContractPublisherTest is BaseTest, IContractPublisherData { // assertEq(customContract.implementation, address(0)); // } + function test_state_setPrevPublisher() public { + // === when prevPublisher address is address(0) + vm.prank(factoryAdmin); + byoc.setPrevPublisher(IContractPublisher(address(0))); + + assertEq(byoc.getAllPublishedContracts(publisher).length, 0); + assertEq(address(byoc.prevPublisher()), address(0)); + + string memory contractId = "MyContract"; + vm.prank(publisher); + byoc.publishContract( + publisher, + contractId, + publishMetadataUri, + compilerMetadataUri, + keccak256(type(MockCustomContract).creationCode), + address(0) + ); + IContractPublisher.CustomContractInstance[] memory contracts = byoc.getAllPublishedContracts(publisher); + assertEq(contracts.length, 1); + assertEq(contracts[0].contractId, "MyContract"); + + // === when prevPublisher address is set to MockPublisher + address mock = address(new MockContractPublisher()); + vm.prank(factoryAdmin); + byoc.setPrevPublisher(IContractPublisher(mock)); + + contracts = byoc.getAllPublishedContracts(publisher); + assertEq(contracts.length, 2); + assertEq(address(byoc.prevPublisher()), mock); + assertEq(contracts[0].contractId, "MockContract"); + assertEq(contracts[1].contractId, "MyContract"); + } + + function test_revert_setPrevPublisher() public { + vm.expectRevert("Not authorized"); + byoc.setPrevPublisher(IContractPublisher(address(0))); + } + + function test_state_setPublisherProfileUri() public { + address user = address(0x123); + string memory uriOne = "ipfs://one"; + string memory uriTwo = "ipfs://two"; + + // user updating for self + vm.prank(user); + byoc.setPublisherProfileUri(user, uriOne); + assertEq(byoc.getPublisherProfileUri(user), uriOne); + + // random caller + vm.prank(address(0x345)); + vm.expectRevert("Registry paused or caller not authorized"); + byoc.setPublisherProfileUri(user, uriOne); + + // MIGRATION_ROLE holder updating for a user + vm.prank(factoryAdmin); + byoc.setPublisherProfileUri(user, uriTwo); + assertEq(byoc.getPublisherProfileUri(user), uriTwo); + } + + function test_state_setPublisherProfileUri_whenPaused() public { + vm.prank(factoryAdmin); + byoc.setPause(true); + address user = address(0x123); + string memory uriOne = "ipfs://one"; + string memory uriTwo = "ipfs://two"; + + // user updating for self + vm.prank(user); + vm.expectRevert("Registry paused or caller not authorized"); + byoc.setPublisherProfileUri(user, uriOne); + + // MIGRATION_ROLE holder updating for a user + vm.prank(factoryAdmin); + byoc.setPublisherProfileUri(user, uriTwo); + assertEq(byoc.getPublisherProfileUri(user), uriTwo); + } + function test_publish_revert_unapprovedCaller() public { string memory contractId = "MyContract"; @@ -149,6 +227,59 @@ contract ContractPublisherTest is BaseTest, IContractPublisherData { ); } + function test_publish_multiple_versions() public { + string memory contractId = "MyContract"; + + vm.prank(publisher); + byoc.publishContract( + publisher, + contractId, + publishMetadataUri, + compilerMetadataUri, + keccak256(type(MockCustomContract).creationCode), + address(0) + ); + string[] memory resolved = byoc.getPublishedUriFromCompilerUri(compilerMetadataUri); + assertEq(resolved.length, 1); + assertEq(resolved[0], publishMetadataUri); + + string memory otherUri = "ipfs://abcd"; + vm.prank(publisher); + byoc.publishContract( + publisher, + contractId, + publishMetadataUri, + otherUri, + keccak256(type(MockCustomContract).creationCode), + address(0) + ); + + string[] memory resolved2 = byoc.getPublishedUriFromCompilerUri(otherUri); + assertEq(resolved2.length, 1); + assertEq(resolved2[0], publishMetadataUri); + } + + function test_read_from_linked_publisher() public { + IContractPublisher.CustomContractInstance[] memory contracts = byoc.getAllPublishedContracts(publisher); + assertEq(contracts.length, 1); + assertEq(contracts[0].contractId, "MockContract"); + + string memory contractId = "MyContract"; + vm.prank(publisher); + byoc.publishContract( + publisher, + contractId, + publishMetadataUri, + compilerMetadataUri, + keccak256(type(MockCustomContract).creationCode), + address(0) + ); + IContractPublisher.CustomContractInstance[] memory contracts2 = byoc.getAllPublishedContracts(publisher); + assertEq(contracts2.length, 2); + assertEq(contracts2[0].contractId, "MockContract"); + assertEq(contracts2[1].contractId, "MyContract"); + } + // Deprecated // function test_publish_emit_ContractPublished() public { // string memory contractId = "MyContract"; @@ -179,18 +310,42 @@ contract ContractPublisherTest is BaseTest, IContractPublisherData { // ); // } - function test_unpublish() public { + function test_unpublish_state() public { string memory contractId = "MyContract"; - vm.prank(publisher); + vm.startPrank(publisher); byoc.publishContract( publisher, contractId, - publishMetadataUri, + "publish URI 1", + compilerMetadataUri, + keccak256(type(MockCustomContract).creationCode), + address(0) + ); + byoc.publishContract( + publisher, + contractId, + "publish URI 2", compilerMetadataUri, keccak256(type(MockCustomContract).creationCode), address(0) ); + byoc.publishContract( + publisher, + contractId, + "publish URI 3", + compilerMetadataUri, + keccak256(type(MockCustomContract).creationCode), + address(0) + ); + + vm.stopPrank(); + + IContractPublisher.CustomContractInstance[] memory allCustomContractsBefore = byoc.getPublishedContractVersions( + publisher, + contractId + ); + assertEq(allCustomContractsBefore.length, 3); vm.prank(publisher); byoc.unpublishContract(publisher, contractId); @@ -204,6 +359,36 @@ contract ContractPublisherTest is BaseTest, IContractPublisherData { assertEq(customContract.publishMetadataUri, ""); assertEq(customContract.bytecodeHash, bytes32(0)); assertEq(customContract.implementation, address(0)); + + IContractPublisher.CustomContractInstance[] memory allCustomContracts = byoc.getPublishedContractVersions( + publisher, + contractId + ); + + assertEq(allCustomContracts.length, 0); + + vm.prank(publisher); + byoc.publishContract( + publisher, + contractId, + "publish URI 4", + compilerMetadataUri, + keccak256(type(MockCustomContract).creationCode), + address(0) + ); + + IContractPublisher.CustomContractInstance memory customContractRepublish = byoc.getPublishedContract( + publisher, + contractId + ); + + assertEq(customContractRepublish.contractId, contractId); + assertEq(customContractRepublish.publishMetadataUri, "publish URI 4"); + + IContractPublisher.CustomContractInstance[] memory allCustomContractsRepublish = byoc + .getPublishedContractVersions(publisher, contractId); + + assertEq(allCustomContractsRepublish.length, 1); } // Deprecated diff --git a/src/test/EvolvingNFT.t.sol b/src/test/EvolvingNFT.t.sol new file mode 100644 index 000000000..8a695b420 --- /dev/null +++ b/src/test/EvolvingNFT.t.sol @@ -0,0 +1,1208 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { IExtension } from "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { EvolvingNFT } from "contracts/prebuilts/evolving-nfts/EvolvingNFT.sol"; +import { EvolvingNFTLogic } from "contracts/prebuilts/evolving-nfts/EvolvingNFTLogic.sol"; +import { RulesEngineExtension } from "contracts/prebuilts/evolving-nfts/extension/RulesEngineExtension.sol"; + +import { IDrop } from "contracts/extension/interface/IDrop.sol"; +import { Drop } from "contracts/extension/upgradeable/Drop.sol"; +import { SharedMetadataBatch } from "contracts/extension/upgradeable/SharedMetadataBatch.sol"; +import { ISharedMetadataBatch } from "contracts/extension/interface/ISharedMetadataBatch.sol"; +import { RulesEngine, IRulesEngine } from "contracts/extension/upgradeable/RulesEngine.sol"; +import { NFTMetadataRenderer } from "contracts/lib/NFTMetadataRenderer.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { PermissionsEnumerable as DynamicPermissionsEnumerable } from "contracts/extension/upgradeable/PermissionsEnumerable.sol"; + +// Test imports +import "erc721a-upgradeable/contracts/IERC721AUpgradeable.sol"; +import { Strings } from "contracts/lib/Strings.sol"; +import { Permissions } from "contracts/extension/Permissions.sol"; +import { IERC721 } from "./mocks/MockERC721.sol"; +import "./utils/BaseTest.sol"; + +contract EvolvingNFTTest is BaseTest { + using Strings for uint256; + using Strings for address; + + event SharedMetadataUpdated( + bytes32 indexed id, + string name, + string description, + string imageURI, + string animationURI + ); + + address public evolvingNFT; + + mapping(uint256 => ISharedMetadataBatch.SharedMetadataInfo) public sharedMetadataBatch; + + bytes private emptyEncodedBytes = abi.encode("", ""); + + // Scores + uint256 private score1 = 10; + uint256 private score2 = 40; + uint256 private score3 = 100; + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + + // Setting up default extension. + IExtension.Extension memory evolvingNftExtension; + IExtension.Extension memory permissionsExtension; + IExtension.Extension memory rulesEngineExtension; + + evolvingNftExtension.metadata = IExtension.ExtensionMetadata({ + name: "EvolvingNFTLogic", + metadataURI: "ipfs://EvolvingNFTLogic", + implementation: address(new EvolvingNFTLogic()) + }); + permissionsExtension.metadata = IExtension.ExtensionMetadata({ + name: "Permissions", + metadataURI: "ipfs://Permissions", + implementation: address(new DynamicPermissionsEnumerable()) + }); + rulesEngineExtension.metadata = IExtension.ExtensionMetadata({ + name: "RulesEngine", + metadataURI: "ipfs://RulesEngine", + implementation: address(new RulesEngineExtension()) + }); + + evolvingNftExtension.functions = new IExtension.ExtensionFunction[](11); + rulesEngineExtension.functions = new IExtension.ExtensionFunction[](4); + permissionsExtension.functions = new IExtension.ExtensionFunction[](4); + + rulesEngineExtension.functions[0] = IExtension.ExtensionFunction( + RulesEngine.getScore.selector, + "getScore(address)" + ); + rulesEngineExtension.functions[1] = IExtension.ExtensionFunction( + RulesEngine.createRuleThreshold.selector, + "createRuleThreshold((address,uint8,uint256,uint256,uint256))" + ); + rulesEngineExtension.functions[2] = IExtension.ExtensionFunction( + RulesEngine.deleteRule.selector, + "deleteRule(bytes32)" + ); + rulesEngineExtension.functions[3] = IExtension.ExtensionFunction( + RulesEngine.getRulesEngineOverride.selector, + "getRulesEngineOverride()" + ); + evolvingNftExtension.functions[0] = IExtension.ExtensionFunction( + IDrop.claim.selector, + "claim(address,uint256,address,uint256,(bytes32[],uint256,uint256,address),bytes)" + ); + evolvingNftExtension.functions[1] = IExtension.ExtensionFunction( + SharedMetadataBatch.setSharedMetadata.selector, + "setSharedMetadata((string,string,string,string),bytes32)" + ); + evolvingNftExtension.functions[2] = IExtension.ExtensionFunction( + IDrop.setClaimConditions.selector, + "setClaimConditions((uint256,uint256,uint256,uint256,bytes32,uint256,address,string)[],bool)" + ); + evolvingNftExtension.functions[3] = IExtension.ExtensionFunction( + EvolvingNFTLogic.tokenURI.selector, + "tokenURI(uint256)" + ); + evolvingNftExtension.functions[4] = IExtension.ExtensionFunction( + IERC721Upgradeable.transferFrom.selector, + "transferFrom(address,address,uint256)" + ); + evolvingNftExtension.functions[5] = IExtension.ExtensionFunction(IERC721.ownerOf.selector, "ownerOf(uint256)"); + evolvingNftExtension.functions[6] = IExtension.ExtensionFunction( + Drop.getSupplyClaimedByWallet.selector, + "getSupplyClaimedByWallet(uint256,address)" + ); + evolvingNftExtension.functions[7] = IExtension.ExtensionFunction( + Drop.getActiveClaimConditionId.selector, + "getActiveClaimConditionId()" + ); + evolvingNftExtension.functions[8] = IExtension.ExtensionFunction( + Drop.getClaimConditionById.selector, + "getClaimConditionById(uint256)" + ); + evolvingNftExtension.functions[9] = IExtension.ExtensionFunction( + Drop.claimCondition.selector, + "claimCondition()" + ); + evolvingNftExtension.functions[10] = IExtension.ExtensionFunction( + SharedMetadataBatch.deleteSharedMetadata.selector, + "deleteSharedMetadata(bytes32)" + ); + permissionsExtension.functions[0] = IExtension.ExtensionFunction( + Permissions.renounceRole.selector, + "renounceRole(bytes32,address)" + ); + permissionsExtension.functions[1] = IExtension.ExtensionFunction( + Permissions.revokeRole.selector, + "revokeRole(bytes32,address)" + ); + permissionsExtension.functions[2] = IExtension.ExtensionFunction( + Permissions.grantRole.selector, + "grantRole(bytes32,address)" + ); + permissionsExtension.functions[3] = IExtension.ExtensionFunction( + Permissions.hasRole.selector, + "hasRole(bytes32,address)" + ); + + IExtension.Extension[] memory extensions = new IExtension.Extension[](3); + extensions[0] = evolvingNftExtension; + extensions[1] = permissionsExtension; + extensions[2] = rulesEngineExtension; + + address evolvingNftImpl = address(new EvolvingNFT(extensions)); + + vm.prank(deployer); + evolvingNFT = address( + new TWProxy( + evolvingNftImpl, + abi.encodeCall( + EvolvingNFT.initialize, + (deployer, NAME, SYMBOL, CONTRACT_URI, forwarders(), saleRecipient, royaltyRecipient, royaltyBps) + ) + ) + ); + + assertEq(Permissions(evolvingNFT).hasRole(0x00, deployer), true); + + sharedMetadataBatch[0] = ISharedMetadataBatch.SharedMetadataInfo({ + name: "Default", + description: "Default metadata", + imageURI: "https://default.com/1", + animationURI: "https://default.com/1" + }); + + sharedMetadataBatch[score1] = ISharedMetadataBatch.SharedMetadataInfo({ + name: "Test 1", + description: "Test 1", + imageURI: "https://test.com/1", + animationURI: "https://test.com/1" + }); + + sharedMetadataBatch[score1 + score2] = ISharedMetadataBatch.SharedMetadataInfo({ + name: "Test 2", + description: "Test 2", + imageURI: "https://test.com/2", + animationURI: "https://test.com/2" + }); + + sharedMetadataBatch[score1 + score2 + score3] = ISharedMetadataBatch.SharedMetadataInfo({ + name: "Test 3", + description: "Test 3", + imageURI: "https://test.com/3", + animationURI: "https://test.com/3" + }); + + vm.deal(deployer, 1_000 ether); + } + + /*/////////////////////////////////////////////////////////////// + Rules test + //////////////////////////////////////////////////////////////*/ + + function test_state_evolvingNFT() public { + /** + * Set shared metadata for the following scores: + * + * default: `0` + * NFT owner owns no relevant tokens. + * score_1: `10` + * NFT owner owns 10 `MockERC20` tokens. + * score_1 + score_2: `50` + * NFT owner additionally owns 1 `MockERC721` NFT. + * score_1 + score_2 + score_3: `150` + * NFT owner addtionally owns 5 `MockERC1155` NFTs of tokenID 3. + */ + + // Set shared metadata + vm.startPrank(deployer); + SharedMetadataBatch(evolvingNFT).setSharedMetadata(sharedMetadataBatch[0], bytes32(0)); + SharedMetadataBatch(evolvingNFT).setSharedMetadata(sharedMetadataBatch[score1], bytes32(score1)); + SharedMetadataBatch(evolvingNFT).setSharedMetadata( + sharedMetadataBatch[score1 + score2], + bytes32(score1 + score2) + ); + SharedMetadataBatch(evolvingNFT).setSharedMetadata( + sharedMetadataBatch[score1 + score2 + score3], + bytes32(score1 + score2 + score3) + ); + vm.stopPrank(); + + // Set rules + vm.prank(deployer); + RulesEngine(evolvingNFT).createRuleThreshold( + IRulesEngine.RuleTypeThreshold({ + token: address(erc20), + tokenType: IRulesEngine.TokenType.ERC20, + tokenId: 0, + balance: 10, + score: score1 + }) + ); + vm.prank(deployer); + RulesEngine(evolvingNFT).createRuleThreshold( + IRulesEngine.RuleTypeThreshold({ + token: address(erc721), + tokenType: IRulesEngine.TokenType.ERC721, + tokenId: 0, + balance: 1, + score: score2 + }) + ); + vm.prank(deployer); + RulesEngine(evolvingNFT).createRuleThreshold( + IRulesEngine.RuleTypeThreshold({ + token: address(erc1155), + tokenType: IRulesEngine.TokenType.ERC1155, + tokenId: 3, + balance: 5, + score: score3 + }) + ); + + // `Receiver` mints token + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "0"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + IDrop.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 0; + alp.currency = address(erc20); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + IDrop.ClaimCondition[] memory conditions = new IDrop.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 0; + conditions[0].currency = address(erc20); + vm.prank(deployer); + IDrop(evolvingNFT).setClaimConditions(conditions, false); + + vm.prank(receiver, receiver); + IDrop(evolvingNFT).claim(receiver, 1, address(erc20), 0, alp, ""); // claims for free, because allowlist price is 0 + + // NFT should return default metadata. + string memory uri0 = EvolvingNFTLogic(evolvingNFT).tokenURI(1); + assertEq( + uri0, + NFTMetadataRenderer.createMetadataEdition({ + name: sharedMetadataBatch[0].name, + description: sharedMetadataBatch[0].description, + imageURI: sharedMetadataBatch[0].imageURI, + animationURI: sharedMetadataBatch[0].animationURI, + tokenOfEdition: 1 + }) + ); + + // NFT should return 1st tier of metadata. + vm.prank(deployer); + erc20.mint(receiver, 10 ether); + assertEq(RulesEngine(evolvingNFT).getScore(receiver), uint256(bytes32(score1))); + + string memory uri1 = EvolvingNFTLogic(evolvingNFT).tokenURI(1); + assertEq( + uri1, + NFTMetadataRenderer.createMetadataEdition({ + name: sharedMetadataBatch[score1].name, + description: sharedMetadataBatch[score1].description, + imageURI: sharedMetadataBatch[score1].imageURI, + animationURI: sharedMetadataBatch[score1].animationURI, + tokenOfEdition: 1 + }) + ); + + // NFT should return 2nd tier of metadata. + vm.prank(deployer); + erc721.mint(receiver, 1); + assertEq(RulesEngine(evolvingNFT).getScore(receiver), uint256(bytes32(score1 + score2))); + + string memory uri2 = EvolvingNFTLogic(evolvingNFT).tokenURI(1); + assertEq( + uri2, + NFTMetadataRenderer.createMetadataEdition({ + name: sharedMetadataBatch[score1 + score2].name, + description: sharedMetadataBatch[score1 + score2].description, + imageURI: sharedMetadataBatch[score1 + score2].imageURI, + animationURI: sharedMetadataBatch[score1 + score2].animationURI, + tokenOfEdition: 1 + }) + ); + + // NFT should return 3rd tier of metadata. + vm.prank(deployer); + erc1155.mint(receiver, 3, 5, ""); + assertEq(RulesEngine(evolvingNFT).getScore(receiver), uint256(bytes32(score1 + score2 + score3))); + + string memory uri3 = EvolvingNFTLogic(evolvingNFT).tokenURI(1); + assertEq( + uri3, + NFTMetadataRenderer.createMetadataEdition({ + name: sharedMetadataBatch[score1 + score2 + score3].name, + description: sharedMetadataBatch[score1 + score2 + score3].description, + imageURI: sharedMetadataBatch[score1 + score2 + score3].imageURI, + animationURI: sharedMetadataBatch[score1 + score2 + score3].animationURI, + tokenOfEdition: 1 + }) + ); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: misc. + //////////////////////////////////////////////////////////////*/ + + /** + * note: Tests whether contract reverts when a non-holder renounces a role. + */ + function test_revert_nonHolder_renounceRole() public { + address caller = address(0x123); + bytes32 role = keccak256("MINTER_ROLE"); + + vm.prank(caller); + vm.expectRevert( + abi.encodePacked( + "Permissions: account ", + Strings.toHexString(uint160(caller), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ); + + Permissions(evolvingNFT).renounceRole(role, caller); + } + + /** + * note: Tests whether contract reverts when a role admin revokes a role for a non-holder. + */ + function test_revert_revokeRoleForNonHolder() public { + address target = address(0x123); + bytes32 role = keccak256("MINTER_ROLE"); + + vm.prank(deployer); + vm.expectRevert( + abi.encodePacked( + "Permissions: account ", + Strings.toHexString(uint160(target), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ); + + Permissions(evolvingNFT).revokeRole(role, target); + } + + /** + * @dev Tests whether contract reverts when a role is granted to an existent role holder. + */ + function test_revert_grant_role_to_account_with_role() public { + bytes32 role = keccak256("ABC_ROLE"); + address receiver = getActor(0); + + vm.startPrank(deployer); + + Permissions(evolvingNFT).grantRole(role, receiver); + + vm.expectRevert("Can only grant to non holders"); + Permissions(evolvingNFT).grantRole(role, receiver); + + vm.stopPrank(); + } + + /** + * @dev Tests contract state for Transfer role. + */ + function test_state_grant_transferRole() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + + // check if admin and address(0) have transfer role in the beginning + bool checkAddressZero = Permissions(evolvingNFT).hasRole(role, address(0)); + bool checkAdmin = Permissions(evolvingNFT).hasRole(role, deployer); + assertTrue(checkAddressZero); + assertTrue(checkAdmin); + + // check if transfer role can be granted to a non-holder + address receiver = getActor(0); + vm.startPrank(deployer); + Permissions(evolvingNFT).grantRole(role, receiver); + + // expect revert when granting to a holder + vm.expectRevert("Can only grant to non holders"); + Permissions(evolvingNFT).grantRole(role, receiver); + + // check if receiver has transfer role + bool checkReceiver = Permissions(evolvingNFT).hasRole(role, receiver); + assertTrue(checkReceiver); + + // check if role is correctly revoked + Permissions(evolvingNFT).revokeRole(role, receiver); + checkReceiver = Permissions(evolvingNFT).hasRole(role, receiver); + assertFalse(checkReceiver); + Permissions(evolvingNFT).revokeRole(role, address(0)); + checkAddressZero = Permissions(evolvingNFT).hasRole(role, address(0)); + assertFalse(checkAddressZero); + + vm.stopPrank(); + } + + /** + * note: Testing transfer of tokens when transfer-role is restricted + */ + function test_claim_transferRole() public { + assertEq(Permissions(evolvingNFT).hasRole(0x00, deployer), true); + + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + IDrop.AllowlistProof memory alp; + alp.proof = proofs; + + IDrop.ClaimCondition[] memory conditions = new IDrop.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + SharedMetadataBatch(evolvingNFT).setSharedMetadata(sharedMetadataBatch[0], bytes32(0)); + vm.prank(deployer); + IDrop(evolvingNFT).setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + IDrop(evolvingNFT).claim(receiver, 1, address(0), 0, alp, ""); + + // revoke transfer role from address(0) + vm.prank(deployer); + Permissions(evolvingNFT).revokeRole(keccak256("TRANSFER_ROLE"), address(0)); + vm.prank(receiver); + vm.expectRevert(bytes("!T")); + IERC721(evolvingNFT).transferFrom(receiver, address(123), 1); + } + + function test_claimCondition_with_startTimestamp() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + IDrop.AllowlistProof memory alp; + alp.proof = proofs; + + IDrop.ClaimCondition[] memory conditions = new IDrop.ClaimCondition[](1); + conditions[0].startTimestamp = 100; + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + SharedMetadataBatch(evolvingNFT).setSharedMetadata(sharedMetadataBatch[0], bytes32(0)); + + vm.prank(deployer); + IDrop(evolvingNFT).setClaimConditions(conditions, false); + + vm.warp(99); + vm.prank(getActor(5), getActor(5)); + vm.expectRevert("!CONDITION."); + IDrop(evolvingNFT).claim(receiver, 1, address(0), 0, alp, ""); + + vm.warp(100); + vm.prank(getActor(4), getActor(4)); + IDrop(evolvingNFT).claim(receiver, 1, address(0), 0, alp, ""); + } + + /*/////////////////////////////////////////////////////////////// + Set Shared Metadata Tests + //////////////////////////////////////////////////////////////*/ + + /* + * note: Testing state changes; set shared metadata for tokens. + */ + function test_state_sharedMetadata() public { + // SET METADATA + vm.prank(deployer); + SharedMetadataBatch(evolvingNFT).setSharedMetadata(sharedMetadataBatch[score1], bytes32(0)); + + // CLAIM 1 TOKEN + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "0"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + IDrop.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 0; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + IDrop.ClaimCondition[] memory conditions = new IDrop.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + IDrop(evolvingNFT).setClaimConditions(conditions, false); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + IDrop(evolvingNFT).claim(receiver, 100, address(erc20), 0, alp, ""); + + string memory uri = EvolvingNFTLogic(evolvingNFT).tokenURI(1); + assertEq( + uri, + NFTMetadataRenderer.createMetadataEdition({ + name: sharedMetadataBatch[score1].name, + description: sharedMetadataBatch[score1].description, + imageURI: sharedMetadataBatch[score1].imageURI, + animationURI: sharedMetadataBatch[score1].animationURI, + tokenOfEdition: 1 + }) + ); + } + + /** + * note: Testing revert condition; an address without MINTER_ROLE calls setSharedMetadata function. + */ + function test_revert_setSharedMetadata_MINTER_ROLE() public { + vm.expectRevert(); + SharedMetadataBatch(evolvingNFT).setSharedMetadata(sharedMetadataBatch[0], bytes32(0)); + } + + /** + * note: Testing event emission; shared metadata set. + */ + function test_event_setSharedMetadata_SharedMetadataUpdated() public { + vm.startPrank(deployer); + + vm.expectEmit(false, false, false, false); + emit SharedMetadataUpdated( + bytes32(0), + sharedMetadataBatch[score1].name, + sharedMetadataBatch[score1].description, + sharedMetadataBatch[score1].imageURI, + sharedMetadataBatch[score1].animationURI + ); + SharedMetadataBatch(evolvingNFT).setSharedMetadata(sharedMetadataBatch[score1], bytes32(0)); + + vm.stopPrank(); + } + + /*/////////////////////////////////////////////////////////////// + Claim Tests + //////////////////////////////////////////////////////////////*/ + + /** + * note: Testing revert condition; exceed max claimable supply. + */ + function test_revert_claimCondition_exceedMaxClaimableSupply() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + IDrop.AllowlistProof memory alp; + alp.proof = proofs; + + IDrop.ClaimCondition[] memory conditions = new IDrop.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 200; + + vm.prank(deployer); + SharedMetadataBatch(evolvingNFT).setSharedMetadata(sharedMetadataBatch[0], bytes32(0)); + vm.prank(deployer); + IDrop(evolvingNFT).setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + IDrop(evolvingNFT).claim(receiver, 100, address(0), 0, alp, ""); + + vm.expectRevert("!MaxSupply"); + vm.prank(getActor(6), getActor(6)); + IDrop(evolvingNFT).claim(receiver, 1, address(0), 0, alp, ""); + } + + /** + * note: Testing quantity limit restriction when no allowlist present. + */ + function test_fuzz_claim_noAllowlist(uint256 x) public { + vm.assume(x != 0); + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + IDrop.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = x; + + IDrop.ClaimCondition[] memory conditions = new IDrop.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + SharedMetadataBatch(evolvingNFT).setSharedMetadata(sharedMetadataBatch[0], bytes32(0)); + + vm.prank(deployer); + IDrop(evolvingNFT).setClaimConditions(conditions, false); + + bytes memory errorQty = "!Qty"; + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(errorQty); + IDrop(evolvingNFT).claim(receiver, 0, address(0), 0, alp, ""); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(errorQty); + IDrop(evolvingNFT).claim(receiver, 101, address(0), 0, alp, ""); + + vm.prank(deployer); + IDrop(evolvingNFT).setClaimConditions(conditions, true); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(errorQty); + IDrop(evolvingNFT).claim(receiver, 101, address(0), 0, alp, ""); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price set to 0 + */ + function test_state_claim_allowlisted_SetQuantityZeroPrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "0"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + IDrop.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 0; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + IDrop.ClaimCondition[] memory conditions = new IDrop.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + SharedMetadataBatch(evolvingNFT).setSharedMetadata(sharedMetadataBatch[0], bytes32(0)); + vm.prank(deployer); + IDrop(evolvingNFT).setClaimConditions(conditions, false); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + IDrop(evolvingNFT).claim(receiver, 100, address(erc20), 0, alp, ""); // claims for free, because allowlist price is 0 + assertEq( + Drop(evolvingNFT).getSupplyClaimedByWallet(Drop(evolvingNFT).getActiveClaimConditionId(), receiver), + 100 + ); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price set to non-zero value + */ + function test_state_claim_allowlisted_SetQuantityPrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "5"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + IDrop.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 5; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + IDrop.ClaimCondition[] memory conditions = new IDrop.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + SharedMetadataBatch(evolvingNFT).setSharedMetadata(sharedMetadataBatch[0], bytes32(0)); + vm.prank(deployer); + IDrop(evolvingNFT).setClaimConditions(conditions, false); + + vm.prank(receiver, receiver); + vm.expectRevert("!PriceOrCurrency"); + IDrop(evolvingNFT).claim(receiver, 100, address(erc20), 0, alp, ""); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(evolvingNFT, 10000); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + IDrop(evolvingNFT).claim(receiver, 100, address(erc20), 5, alp, ""); + assertEq( + Drop(evolvingNFT).getSupplyClaimedByWallet(Drop(evolvingNFT).getActiveClaimConditionId(), receiver), + 100 + ); + assertEq(erc20.balanceOf(receiver), 10000 - 500); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price not set; should default to general price and currency + */ + function test_state_claim_allowlisted_SetQuantityDefaultPrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = Strings.toString(type(uint256).max); // this implies that general price is applicable + inputs[4] = "0x0000000000000000000000000000000000000000"; + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + IDrop.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = type(uint256).max; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + IDrop.ClaimCondition[] memory conditions = new IDrop.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + SharedMetadataBatch(evolvingNFT).setSharedMetadata(sharedMetadataBatch[0], bytes32(0)); + vm.prank(deployer); + IDrop(evolvingNFT).setClaimConditions(conditions, false); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(evolvingNFT, 10000); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + IDrop(evolvingNFT).claim(receiver, 100, address(erc20), 10, alp, ""); + assertEq( + Drop(evolvingNFT).getSupplyClaimedByWallet(Drop(evolvingNFT).getActiveClaimConditionId(), receiver), + 100 + ); + assertEq(erc20.balanceOf(receiver), 10000 - 1000); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to 0 => should default to general limit + * - allowlist price set to some value different than general price + */ + function test_state_claim_allowlisted_DefaultQuantitySomePrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "0"; // this implies that general limit is applicable + inputs[3] = "5"; + inputs[4] = "0x0000000000000000000000000000000000000000"; // general currency will be applicable + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + IDrop.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 0; + alp.pricePerToken = 5; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + IDrop.ClaimCondition[] memory conditions = new IDrop.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + SharedMetadataBatch(evolvingNFT).setSharedMetadata(sharedMetadataBatch[0], bytes32(0)); + vm.prank(deployer); + IDrop(evolvingNFT).setClaimConditions(conditions, false); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(evolvingNFT, 10000); + + bytes memory errorQty = "!Qty"; + vm.prank(receiver, receiver); + vm.expectRevert(errorQty); + IDrop(evolvingNFT).claim(receiver, 100, address(erc20), 5, alp, ""); // trying to claim more than general limit + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + IDrop(evolvingNFT).claim(receiver, 10, address(erc20), 5, alp, ""); + assertEq( + Drop(evolvingNFT).getSupplyClaimedByWallet(Drop(evolvingNFT).getActiveClaimConditionId(), receiver), + 10 + ); + assertEq(erc20.balanceOf(receiver), 10000 - 50); + } + + function test_fuzz_claim_merkleProof(uint256 x) public { + vm.assume(x > 10 && x < 500); + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = Strings.toString(x); + inputs[3] = "0"; + inputs[4] = "0x0000000000000000000000000000000000000000"; + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + IDrop.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = x; + alp.pricePerToken = 0; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + + // bytes32[] memory proofs = new bytes32[](0); + + IDrop.ClaimCondition[] memory conditions = new IDrop.ClaimCondition[](1); + conditions[0].maxClaimableSupply = x; + conditions[0].quantityLimitPerWallet = 1; + conditions[0].merkleRoot = root; + + vm.prank(deployer); + SharedMetadataBatch(evolvingNFT).setSharedMetadata(sharedMetadataBatch[0], bytes32(0)); + vm.prank(deployer); + IDrop(evolvingNFT).setClaimConditions(conditions, false); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + IDrop(evolvingNFT).claim(receiver, x - 5, address(0), 0, alp, ""); + assertEq( + Drop(evolvingNFT).getSupplyClaimedByWallet(Drop(evolvingNFT).getActiveClaimConditionId(), receiver), + x - 5 + ); + + bytes memory errorQty = "!Qty"; + + vm.prank(receiver, receiver); + vm.expectRevert(errorQty); + IDrop(evolvingNFT).claim(receiver, 6, address(0), 0, alp, ""); + + vm.prank(receiver, receiver); + IDrop(evolvingNFT).claim(receiver, 5, address(0), 0, alp, ""); + assertEq( + Drop(evolvingNFT).getSupplyClaimedByWallet(Drop(evolvingNFT).getActiveClaimConditionId(), receiver), + x + ); + + vm.prank(receiver, receiver); + vm.expectRevert(errorQty); + IDrop(evolvingNFT).claim(receiver, 5, address(0), 0, alp, ""); // quantity limit already claimed + } + + /** + * note: Testing state changes; reset eligibility of claim conditions and claiming again for same condition id. + */ + function test_state_claimCondition_resetEligibility() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + IDrop.AllowlistProof memory alp; + alp.proof = proofs; + + IDrop.ClaimCondition[] memory conditions = new IDrop.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + SharedMetadataBatch(evolvingNFT).setSharedMetadata(sharedMetadataBatch[0], bytes32(0)); + + vm.prank(deployer); + IDrop(evolvingNFT).setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + IDrop(evolvingNFT).claim(receiver, 100, address(0), 0, alp, ""); + + bytes memory errorQty = "!Qty"; + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(errorQty); + IDrop(evolvingNFT).claim(receiver, 100, address(0), 0, alp, ""); + + vm.prank(deployer); + IDrop(evolvingNFT).setClaimConditions(conditions, true); + + vm.prank(getActor(5), getActor(5)); + IDrop(evolvingNFT).claim(receiver, 100, address(0), 0, alp, ""); + } + + /*/////////////////////////////////////////////////////////////// + setClaimConditions + //////////////////////////////////////////////////////////////*/ + + function test_claimCondition_startIdAndCount() public { + vm.startPrank(deployer); + + uint256 currentStartId = 0; + uint256 count = 0; + + IDrop.ClaimCondition[] memory conditions = new IDrop.ClaimCondition[](2); + conditions[0].startTimestamp = 0; + conditions[0].maxClaimableSupply = 10; + conditions[1].startTimestamp = 1; + conditions[1].maxClaimableSupply = 10; + + IDrop(evolvingNFT).setClaimConditions(conditions, false); + (currentStartId, count) = Drop(evolvingNFT).claimCondition(); + assertEq(currentStartId, 0); + assertEq(count, 2); + + IDrop(evolvingNFT).setClaimConditions(conditions, false); + (currentStartId, count) = Drop(evolvingNFT).claimCondition(); + assertEq(currentStartId, 0); + assertEq(count, 2); + + IDrop(evolvingNFT).setClaimConditions(conditions, true); + (currentStartId, count) = Drop(evolvingNFT).claimCondition(); + assertEq(currentStartId, 2); + assertEq(count, 2); + + IDrop(evolvingNFT).setClaimConditions(conditions, true); + (currentStartId, count) = Drop(evolvingNFT).claimCondition(); + assertEq(currentStartId, 4); + assertEq(count, 2); + } + + function test_claimCondition_startPhase() public { + vm.startPrank(deployer); + + uint256 activeConditionId = 0; + + IDrop.ClaimCondition[] memory conditions = new IDrop.ClaimCondition[](3); + conditions[0].startTimestamp = 10; + conditions[0].maxClaimableSupply = 11; + conditions[0].quantityLimitPerWallet = 12; + conditions[1].startTimestamp = 20; + conditions[1].maxClaimableSupply = 21; + conditions[1].quantityLimitPerWallet = 22; + conditions[2].startTimestamp = 30; + conditions[2].maxClaimableSupply = 31; + conditions[2].quantityLimitPerWallet = 32; + IDrop(evolvingNFT).setClaimConditions(conditions, false); + + vm.expectRevert("!CONDITION."); + Drop(evolvingNFT).getActiveClaimConditionId(); + + vm.warp(10); + activeConditionId = Drop(evolvingNFT).getActiveClaimConditionId(); + assertEq(activeConditionId, 0); + assertEq(Drop(evolvingNFT).getClaimConditionById(activeConditionId).startTimestamp, 10); + assertEq(Drop(evolvingNFT).getClaimConditionById(activeConditionId).maxClaimableSupply, 11); + assertEq(Drop(evolvingNFT).getClaimConditionById(activeConditionId).quantityLimitPerWallet, 12); + + vm.warp(20); + activeConditionId = Drop(evolvingNFT).getActiveClaimConditionId(); + assertEq(activeConditionId, 1); + assertEq(Drop(evolvingNFT).getClaimConditionById(activeConditionId).startTimestamp, 20); + assertEq(Drop(evolvingNFT).getClaimConditionById(activeConditionId).maxClaimableSupply, 21); + assertEq(Drop(evolvingNFT).getClaimConditionById(activeConditionId).quantityLimitPerWallet, 22); + + vm.warp(30); + activeConditionId = Drop(evolvingNFT).getActiveClaimConditionId(); + assertEq(activeConditionId, 2); + assertEq(Drop(evolvingNFT).getClaimConditionById(activeConditionId).startTimestamp, 30); + assertEq(Drop(evolvingNFT).getClaimConditionById(activeConditionId).maxClaimableSupply, 31); + assertEq(Drop(evolvingNFT).getClaimConditionById(activeConditionId).quantityLimitPerWallet, 32); + + vm.warp(40); + assertEq(Drop(evolvingNFT).getActiveClaimConditionId(), 2); + } + + /*/////////////////////////////////////////////////////////////// + Audit POC tests + //////////////////////////////////////////////////////////////*/ + + function test_state_incorrectTokenUri() public { + // Set shared metadata + vm.startPrank(deployer); + SharedMetadataBatch(evolvingNFT).setSharedMetadata(sharedMetadataBatch[0], bytes32(0)); + SharedMetadataBatch(evolvingNFT).setSharedMetadata(sharedMetadataBatch[score1], bytes32(score1)); + SharedMetadataBatch(evolvingNFT).setSharedMetadata( + sharedMetadataBatch[score1 + score2], + bytes32(score1 + score2) + ); + SharedMetadataBatch(evolvingNFT).setSharedMetadata( + sharedMetadataBatch[score1 + score2 + score3], + bytes32(score1 + score2 + score3) + ); + + // Delete metadata at index "score1" + // Now the order of metadata ids is: 0, 150, 50 + SharedMetadataBatch(evolvingNFT).deleteSharedMetadata(bytes32(score1)); + vm.stopPrank(); + + // Set rules + vm.prank(deployer); + RulesEngine(evolvingNFT).createRuleThreshold( + IRulesEngine.RuleTypeThreshold({ + token: address(erc20), + tokenType: IRulesEngine.TokenType.ERC20, + tokenId: 0, + balance: 10, + score: score1 + score2 + score3 + }) + ); + + // `Receiver` mints token + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "0"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + IDrop.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 0; + alp.currency = address(erc20); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + IDrop.ClaimCondition[] memory conditions = new IDrop.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 0; + conditions[0].currency = address(erc20); + vm.prank(deployer); + IDrop(evolvingNFT).setClaimConditions(conditions, false); + + vm.prank(receiver, receiver); + IDrop(evolvingNFT).claim(receiver, 1, address(erc20), 0, alp, ""); // claims for free, because allowlist price is 0 + + // NFT should return metadata of a rule at "score1 + score2 + score3" + // It used to return metadata for "score1 + score2", but now this is fixed. + erc20.mint(receiver, 10 ether); + string memory uri = EvolvingNFTLogic(evolvingNFT).tokenURI(1); + assertEq( + uri, + NFTMetadataRenderer.createMetadataEdition({ + name: sharedMetadataBatch[score1 + score2 + score3].name, + description: sharedMetadataBatch[score1 + score2 + score3].description, + imageURI: sharedMetadataBatch[score1 + score2 + score3].imageURI, + animationURI: sharedMetadataBatch[score1 + score2 + score3].animationURI, + tokenOfEdition: 1 + }) + ); + } +} diff --git a/src/test/Forwarder.t.sol b/src/test/Forwarder.t.sol new file mode 100644 index 000000000..953c5e756 --- /dev/null +++ b/src/test/Forwarder.t.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { Forwarder } from "contracts/infra/forwarder/Forwarder.sol"; +import { ForwarderConsumer } from "contracts/infra/forwarder/ForwarderConsumer.sol"; + +import "./utils/BaseTest.sol"; + +contract ForwarderTest is BaseTest { + ForwarderConsumer public consumer; + + uint256 public userPKey = 1020; + address public user; + address public relayer = address(0x4567); + + bytes32 internal typehashForwardRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + user = vm.addr(userPKey); + consumer = new ForwarderConsumer(forwarders()); + + typehashForwardRequest = keccak256( + "ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data)" + ); + nameHash = keccak256(bytes("GSNv2 Forwarder")); + versionHash = keccak256(bytes("0.0.1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256(abi.encode(typehashEip712, nameHash, versionHash, block.chainid, forwarder)); + + vm.label(user, "End user"); + vm.label(forwarder, "Forwarder"); + vm.label(relayer, "Relayer"); + vm.label(address(consumer), "Consumer"); + } + + /*/////////////////////////////////////////////////////////////// + Regular `Forwarder`: chainId in typehash + //////////////////////////////////////////////////////////////*/ + + function signForwarderRequest( + Forwarder.ForwardRequest memory forwardRequest, + uint256 privateKey + ) internal view returns (bytes memory) { + bytes memory encodedRequest = abi.encode( + typehashForwardRequest, + forwardRequest.from, + forwardRequest.to, + forwardRequest.value, + forwardRequest.gas, + forwardRequest.nonce, + keccak256(forwardRequest.data) + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, typedDataHash); + bytes memory signature = abi.encodePacked(r, s, v); + + return signature; + } + + function test_state_forwarder() public { + Forwarder.ForwardRequest memory forwardRequest; + + forwardRequest.from = user; + forwardRequest.to = address(consumer); + forwardRequest.value = 0; + forwardRequest.gas = 100_000; + forwardRequest.nonce = Forwarder(forwarder).getNonce(user); + forwardRequest.data = abi.encodeCall(ForwarderConsumer.setCaller, ()); + + bytes memory signature = signForwarderRequest(forwardRequest, userPKey); + vm.prank(relayer); + Forwarder(forwarder).execute(forwardRequest, signature); + + assertEq(consumer.caller(), user); + } +} diff --git a/src/test/ForwarderChainlessDomain.t.sol b/src/test/ForwarderChainlessDomain.t.sol new file mode 100644 index 000000000..bf4561aba --- /dev/null +++ b/src/test/ForwarderChainlessDomain.t.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { ForwarderConsumer } from "contracts/infra/forwarder/ForwarderConsumer.sol"; +import { ForwarderChainlessDomain } from "contracts/infra/forwarder/ForwarderChainlessDomain.sol"; + +import "./utils/BaseTest.sol"; + +contract ForwarderChainlessDomainTest is BaseTest { + address[] public forwarderChainlessDomain; + ForwarderConsumer public consumer; + + uint256 public userPKey = 1020; + address public user; + address public relayer = address(0x4567); + + bytes32 internal typehashForwardRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + user = vm.addr(userPKey); + consumer = new ForwarderConsumer(forwarders()); + + forwarderChainlessDomain.push(address(new ForwarderChainlessDomain())); + consumer = new ForwarderConsumer(forwarderChainlessDomain); + + typehashForwardRequest = keccak256( + "ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data,uint256 chainid)" + ); + nameHash = keccak256(bytes("GSNv2 Forwarder")); + versionHash = keccak256(bytes("0.0.1")); + typehashEip712 = keccak256("EIP712Domain(string name,string version,address verifyingContract)"); + domainSeparator = keccak256(abi.encode(typehashEip712, nameHash, versionHash, forwarderChainlessDomain[0])); + + vm.label(user, "End user"); + vm.label(forwarder, "Forwarder"); + vm.label(relayer, "Relayer"); + vm.label(address(consumer), "Consumer"); + } + + /*/////////////////////////////////////////////////////////////// + Updated `Forwarder`: chainId in ForwardRequest, not typehash. + //////////////////////////////////////////////////////////////*/ + + function signForwarderRequest( + ForwarderChainlessDomain.ForwardRequest memory forwardRequest, + uint256 privateKey + ) internal view returns (bytes memory) { + bytes memory encodedRequest = abi.encode( + typehashForwardRequest, + forwardRequest.from, + forwardRequest.to, + forwardRequest.value, + forwardRequest.gas, + forwardRequest.nonce, + keccak256(forwardRequest.data), + forwardRequest.chainid + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, typedDataHash); + bytes memory signature = abi.encodePacked(r, s, v); + + return signature; + } + + function test_state_forwarderChainlessDomain() public { + ForwarderChainlessDomain.ForwardRequest memory forwardRequest; + + forwardRequest.from = user; + forwardRequest.to = address(consumer); + forwardRequest.value = 0; + forwardRequest.gas = 100_000; + forwardRequest.nonce = ForwarderChainlessDomain(forwarderChainlessDomain[0]).getNonce(user); + forwardRequest.data = abi.encodeCall(ForwarderConsumer.setCaller, ()); + forwardRequest.chainid = block.chainid; + + bytes memory signature = signForwarderRequest(forwardRequest, userPKey); + vm.prank(relayer); + ForwarderChainlessDomain(forwarderChainlessDomain[0]).execute(forwardRequest, signature); + + assertEq(consumer.caller(), user); + } +} diff --git a/src/test/LoyaltyCard.t.sol b/src/test/LoyaltyCard.t.sol new file mode 100644 index 000000000..f9e26498b --- /dev/null +++ b/src/test/LoyaltyCard.t.sol @@ -0,0 +1,356 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import "./utils/BaseTest.sol"; +import "contracts/infra/TWProxy.sol"; +import { Strings } from "contracts/lib/Strings.sol"; +import { LoyaltyCard, NFTMetadata } from "contracts/prebuilts/loyalty/LoyaltyCard.sol"; + +contract LoyaltyCardTest is BaseTest { + LoyaltyCard internal loyaltyCard; + using Strings for uint256; + + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + LoyaltyCard.MintRequest _mintrequest; + bytes _signature; + + address recipient; + + function setUp() public override { + super.setUp(); + + address loyaltyCardImpl = address(new LoyaltyCard()); + + vm.prank(signer); + loyaltyCard = LoyaltyCard( + address( + new TWProxy( + loyaltyCardImpl, + abi.encodeCall( + LoyaltyCard.initialize, + ( + signer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ); + + recipient = address(0x123); + erc20.mint(recipient, 1_000); + vm.deal(recipient, 1_000); + + typehashMintRequest = keccak256( + "MintRequest(address to,address royaltyRecipient,uint256 royaltyBps,address primarySaleRecipient,string uri,uint256 quantity,uint256 pricePerToken,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + nameHash = keccak256(bytes("SignatureMintERC721")); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256( + abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(loyaltyCard)) + ); + + _mintrequest.to = recipient; + _mintrequest.royaltyRecipient = royaltyRecipient; + _mintrequest.royaltyBps = royaltyBps; + _mintrequest.primarySaleRecipient = saleRecipient; + _mintrequest.uri = "ipfs://"; + _mintrequest.quantity = 1; + _mintrequest.pricePerToken = 0; + _mintrequest.currency = address(0); + _mintrequest.validityStartTimestamp = 1000; + _mintrequest.validityEndTimestamp = 2000; + _mintrequest.uid = bytes32(0); + + _signature = signMintRequest(_mintrequest, privateKey); + } + + function signMintRequest( + LoyaltyCard.MintRequest memory _request, + uint256 _privateKey + ) internal view returns (bytes memory) { + bytes memory encodedRequest = abi.encode( + typehashMintRequest, + _request.to, + _request.royaltyRecipient, + _request.royaltyBps, + _request.primarySaleRecipient, + keccak256(bytes(_request.uri)), + _request.quantity, + _request.pricePerToken, + _request.currency, + _request.validityStartTimestamp, + _request.validityEndTimestamp, + _request.uid + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, typedDataHash); + bytes memory sig = abi.encodePacked(r, s, v); + + return sig; + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `mintTo` + //////////////////////////////////////////////////////////////*/ + + function test_state_mintTo() public { + string memory _tokenURI = "tokenURI"; + + uint256 nextTokenId = loyaltyCard.nextTokenIdToMint(); + uint256 currentTotalSupply = loyaltyCard.totalSupply(); + uint256 currentBalanceOfRecipient = loyaltyCard.balanceOf(recipient); + + vm.prank(signer); + loyaltyCard.mintTo(recipient, _tokenURI); + + assertEq(loyaltyCard.nextTokenIdToMint(), nextTokenId + 1); + assertEq(loyaltyCard.tokenURI(nextTokenId), _tokenURI); + assertEq(loyaltyCard.totalSupply(), currentTotalSupply + 1); + assertEq(loyaltyCard.balanceOf(recipient), currentBalanceOfRecipient + 1); + assertEq(loyaltyCard.ownerOf(nextTokenId), recipient); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `setTokenURI` + //////////////////////////////////////////////////////////////*/ + + function test_state_setTokenURI() public { + string memory _tokenURI = "tokenURI"; + + vm.prank(signer); + uint256 tokenIdMinted = loyaltyCard.mintTo(recipient, _tokenURI); + + assertEq(_tokenURI, loyaltyCard.tokenURI(tokenIdMinted)); + + assertEq(loyaltyCard.hasRole(keccak256("METADATA_ROLE"), signer), true); + + string memory newURI = "newURI"; + + vm.prank(signer); + loyaltyCard.setTokenURI(tokenIdMinted, newURI); + + assertEq(newURI, loyaltyCard.tokenURI(tokenIdMinted)); + + vm.prank(signer); + loyaltyCard.renounceRole(keccak256("METADATA_ROLE"), signer); + + vm.expectRevert(); + vm.prank(signer); + loyaltyCard.setTokenURI(tokenIdMinted, _tokenURI); + + vm.expectRevert(); + vm.prank(signer); + loyaltyCard.grantRole(keccak256("METADATA_ROLE"), signer); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: cancel / revoke loyalty + //////////////////////////////////////////////////////////////*/ + + function test_state_cancelLoyalty() public { + string memory _tokenURI = "tokenURI"; + + vm.prank(signer); + uint256 tokenIdMinted = loyaltyCard.mintTo(recipient, _tokenURI); + + assertEq(loyaltyCard.ownerOf(tokenIdMinted), recipient); + + vm.prank(recipient); + loyaltyCard.setApprovalForAll(signer, true); + + vm.prank(signer); + loyaltyCard.cancel(tokenIdMinted); + + vm.expectRevert(); + loyaltyCard.ownerOf(tokenIdMinted); + } + + function test_state_revokeLoyalty() public { + string memory _tokenURI = "tokenURI"; + + vm.prank(signer); + uint256 tokenIdMinted = loyaltyCard.mintTo(recipient, _tokenURI); + + assertEq(loyaltyCard.ownerOf(tokenIdMinted), recipient); + + address burner = address(0x123456); + vm.prank(signer); + loyaltyCard.grantRole(keccak256("REVOKE_ROLE"), burner); + + vm.prank(signer); + loyaltyCard.renounceRole(keccak256("REVOKE_ROLE"), signer); + + vm.expectRevert(); + vm.prank(signer); + loyaltyCard.revoke(tokenIdMinted); + + vm.prank(burner); + loyaltyCard.revoke(tokenIdMinted); + + vm.expectRevert(); + loyaltyCard.ownerOf(tokenIdMinted); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `mintWithSignature` + //////////////////////////////////////////////////////////////*/ + + function test_state_mintWithSignature_ZeroPrice() public { + vm.warp(1000); + + uint256 nextTokenId = loyaltyCard.nextTokenIdToMint(); + uint256 currentTotalSupply = loyaltyCard.totalSupply(); + uint256 currentBalanceOfRecipient = loyaltyCard.balanceOf(recipient); + + loyaltyCard.mintWithSignature(_mintrequest, _signature); + + assertEq(loyaltyCard.nextTokenIdToMint(), nextTokenId + _mintrequest.quantity); + assertEq(loyaltyCard.tokenURI(nextTokenId), string(abi.encodePacked(_mintrequest.uri))); + assertEq(loyaltyCard.totalSupply(), currentTotalSupply + _mintrequest.quantity); + assertEq(loyaltyCard.balanceOf(recipient), currentBalanceOfRecipient + _mintrequest.quantity); + assertEq(loyaltyCard.ownerOf(nextTokenId), recipient); + } + + function test_state_mintWithSignature_NonZeroPrice_ERC20() public { + vm.warp(1000); + + _mintrequest.pricePerToken = 1; + _mintrequest.currency = address(erc20); + _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(recipient); + erc20.approve(address(loyaltyCard), 1); + + uint256 nextTokenId = loyaltyCard.nextTokenIdToMint(); + uint256 currentTotalSupply = loyaltyCard.totalSupply(); + uint256 currentBalanceOfRecipient = loyaltyCard.balanceOf(recipient); + + vm.prank(recipient); + loyaltyCard.mintWithSignature(_mintrequest, _signature); + + assertEq(loyaltyCard.nextTokenIdToMint(), nextTokenId + _mintrequest.quantity); + assertEq(loyaltyCard.tokenURI(nextTokenId), string(abi.encodePacked(_mintrequest.uri))); + assertEq(loyaltyCard.totalSupply(), currentTotalSupply + _mintrequest.quantity); + assertEq(loyaltyCard.balanceOf(recipient), currentBalanceOfRecipient + _mintrequest.quantity); + assertEq(loyaltyCard.ownerOf(nextTokenId), recipient); + } + + function test_state_mintWithSignature_NonZeroPrice_NativeToken() public { + vm.warp(1000); + + _mintrequest.pricePerToken = 1; + _mintrequest.currency = address(NATIVE_TOKEN); + _signature = signMintRequest(_mintrequest, privateKey); + + uint256 nextTokenId = loyaltyCard.nextTokenIdToMint(); + uint256 currentTotalSupply = loyaltyCard.totalSupply(); + uint256 currentBalanceOfRecipient = loyaltyCard.balanceOf(recipient); + + vm.deal(recipient, 1); + + vm.prank(recipient); + loyaltyCard.mintWithSignature{ value: 1 }(_mintrequest, _signature); + + assertEq(loyaltyCard.nextTokenIdToMint(), nextTokenId + _mintrequest.quantity); + assertEq(loyaltyCard.tokenURI(nextTokenId), string(abi.encodePacked(_mintrequest.uri))); + assertEq(loyaltyCard.totalSupply(), currentTotalSupply + _mintrequest.quantity); + assertEq(loyaltyCard.balanceOf(recipient), currentBalanceOfRecipient + _mintrequest.quantity); + assertEq(loyaltyCard.ownerOf(nextTokenId), recipient); + } + + function test_revert_mintWithSignature_InvalidMsgValue() public { + vm.warp(1000); + + _mintrequest.pricePerToken = 1; + _mintrequest.currency = address(NATIVE_TOKEN); + _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(recipient); + vm.expectRevert("Invalid msg value"); + loyaltyCard.mintWithSignature{ value: 0 }(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_ZeroQty() public { + vm.warp(1000); + + _mintrequest.quantity = 0; + _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("LoyaltyCard: only 1 NFT can be minted at a time."); + loyaltyCard.mintWithSignature(_mintrequest, _signature); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `setTokenURI` + //////////////////////////////////////////////////////////////*/ + + function test_setTokenURI_state() public { + string memory uri = "uri_string"; + + vm.prank(signer); + loyaltyCard.setTokenURI(0, uri); + + string memory _tokenURI = loyaltyCard.tokenURI(0); + + assertEq(_tokenURI, uri); + } + + function test_setTokenURI_revert_NotAuthorized() public { + string memory uri = "uri_string"; + + vm.expectRevert(abi.encodeWithSelector(NFTMetadata.NFTMetadataUnauthorized.selector)); + vm.prank(address(0x1)); + loyaltyCard.setTokenURI(0, uri); + } + + function test_setTokenURI_revert_Frozen() public { + string memory uri = "uri_string"; + + vm.startPrank(signer); + loyaltyCard.freezeMetadata(); + + vm.expectRevert(abi.encodeWithSelector(NFTMetadata.NFTMetadataFrozen.selector, 0)); + loyaltyCard.setTokenURI(0, uri); + } + + /*/////////////////////////////////////////////////////////////// + Audit fixes tests + //////////////////////////////////////////////////////////////*/ + + function test_audit_quantity_not_1() public { + vm.warp(1000); + _mintrequest.pricePerToken = 1; + _mintrequest.quantity = 5; + _mintrequest.currency = address(erc20); + _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(recipient); + erc20.approve(address(loyaltyCard), 5); + + vm.prank(recipient); + vm.expectRevert("LoyaltyCard: only 1 NFT can be minted at a time."); + loyaltyCard.mintWithSignature(_mintrequest, _signature); + } +} diff --git a/src/test/LoyaltyPoints.t.sol b/src/test/LoyaltyPoints.t.sol new file mode 100644 index 000000000..c557091bf --- /dev/null +++ b/src/test/LoyaltyPoints.t.sol @@ -0,0 +1,265 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import "./utils/BaseTest.sol"; +import "contracts/infra/TWProxy.sol"; +import { Strings } from "contracts/lib/Strings.sol"; +import { LoyaltyPoints } from "contracts/prebuilts/unaudited/loyalty/LoyaltyPoints.sol"; + +contract LoyaltyPointsTest is BaseTest { + LoyaltyPoints internal loyaltyPoints; + using Strings for uint256; + + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + LoyaltyPoints.MintRequest _mintrequest; + bytes _signature; + + address recipient; + + function setUp() public override { + super.setUp(); + + address loyaltyPointsImpl = address(new LoyaltyPoints()); + + vm.prank(signer); + loyaltyPoints = LoyaltyPoints( + address( + new TWProxy( + loyaltyPointsImpl, + abi.encodeCall( + LoyaltyPoints.initialize, + ( + signer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ); + + recipient = address(0x123); + erc20.mint(recipient, 1_000); + vm.deal(recipient, 1_000); + + typehashMintRequest = keccak256( + "MintRequest(address to,address primarySaleRecipient,uint256 quantity,uint256 price,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + nameHash = keccak256(bytes(NAME)); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256( + abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(loyaltyPoints)) + ); + + _mintrequest.to = recipient; + _mintrequest.primarySaleRecipient = saleRecipient; + _mintrequest.quantity = 1 ether; + _mintrequest.price = 0; + _mintrequest.currency = address(0); + _mintrequest.validityStartTimestamp = 1000; + _mintrequest.validityEndTimestamp = 2000; + _mintrequest.uid = bytes32(0); + + _signature = signMintRequest(_mintrequest, privateKey); + } + + function signMintRequest( + LoyaltyPoints.MintRequest memory _request, + uint256 _privateKey + ) internal view returns (bytes memory) { + bytes memory encodedRequest = abi.encode( + typehashMintRequest, + _request.to, + _request.primarySaleRecipient, + _request.quantity, + _request.price, + _request.currency, + _request.validityStartTimestamp, + _request.validityEndTimestamp, + _request.uid + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, typedDataHash); + bytes memory sig = abi.encodePacked(r, s, v); + + return sig; + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `mintTo` + //////////////////////////////////////////////////////////////*/ + + function test_state_mintTo() public { + uint256 amount = 1 ether; + + uint256 currentTotalSupply = loyaltyPoints.totalSupply(); + uint256 currentBalanceOfRecipient = loyaltyPoints.balanceOf(recipient); + + vm.prank(signer); + loyaltyPoints.mintTo(recipient, amount); + + assertEq(loyaltyPoints.totalSupply(), currentTotalSupply + amount); + assertEq(loyaltyPoints.balanceOf(recipient), currentBalanceOfRecipient + amount); + + assertEq(loyaltyPoints.getTotalMintedInLifetime(recipient), amount); + + vm.prank(signer); + loyaltyPoints.mintTo(recipient, amount); + assertEq(loyaltyPoints.getTotalMintedInLifetime(recipient), amount * 2); + + vm.prank(recipient); + loyaltyPoints.cancel(recipient, amount); + assertEq(loyaltyPoints.getTotalMintedInLifetime(recipient), amount * 2); + + vm.prank(signer); + loyaltyPoints.revoke(recipient, amount); + assertEq(loyaltyPoints.getTotalMintedInLifetime(recipient), amount * 2); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: cancel / revoke loyalty + //////////////////////////////////////////////////////////////*/ + + function test_state_cancelLoyalty() public { + uint256 amount = 10 ether; + + vm.prank(signer); + loyaltyPoints.mintTo(recipient, amount); + + assertEq(loyaltyPoints.balanceOf(recipient), amount); + + uint256 amountToCancel = 1 ether; + + vm.prank(recipient); + loyaltyPoints.approve(signer, amountToCancel); + + vm.prank(signer); + loyaltyPoints.cancel(recipient, amountToCancel); + assertEq(loyaltyPoints.balanceOf(recipient), amount - amountToCancel); + } + + function test_state_revokeLoyalty() public { + uint256 amount = 10 ether; + + vm.prank(signer); + loyaltyPoints.mintTo(recipient, amount); + + assertEq(loyaltyPoints.balanceOf(recipient), amount); + + address burner = address(0x123456); + vm.prank(signer); + loyaltyPoints.grantRole(keccak256("REVOKE_ROLE"), burner); + + vm.prank(signer); + loyaltyPoints.renounceRole(keccak256("REVOKE_ROLE"), signer); + + uint256 amountToRevoke = 1 ether; + + vm.expectRevert(); + vm.prank(signer); + loyaltyPoints.revoke(recipient, amountToRevoke); + + vm.prank(burner); + loyaltyPoints.revoke(recipient, amountToRevoke); + assertEq(loyaltyPoints.balanceOf(recipient), amount - amountToRevoke); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `mintWithSignature` + //////////////////////////////////////////////////////////////*/ + + function test_state_mintWithSignature_ZeroPrice() public { + vm.warp(1000); + + uint256 currentTotalSupply = loyaltyPoints.totalSupply(); + uint256 currentBalanceOfRecipient = loyaltyPoints.balanceOf(recipient); + + loyaltyPoints.mintWithSignature(_mintrequest, _signature); + + assertEq(loyaltyPoints.totalSupply(), currentTotalSupply + _mintrequest.quantity); + assertEq(loyaltyPoints.balanceOf(recipient), currentBalanceOfRecipient + _mintrequest.quantity); + } + + function test_state_mintWithSignature_NonZeroPrice_ERC20() public { + vm.warp(1000); + + _mintrequest.price = 1; + _mintrequest.currency = address(erc20); + _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(recipient); + erc20.approve(address(loyaltyPoints), 1); + + uint256 currentTotalSupply = loyaltyPoints.totalSupply(); + uint256 currentBalanceOfRecipient = loyaltyPoints.balanceOf(recipient); + uint256 currentCurrencyBalOfRecipient = erc20.balanceOf(recipient); + + vm.prank(recipient); + loyaltyPoints.mintWithSignature(_mintrequest, _signature); + + assertEq(loyaltyPoints.totalSupply(), currentTotalSupply + _mintrequest.quantity); + assertEq(loyaltyPoints.balanceOf(recipient), currentBalanceOfRecipient + _mintrequest.quantity); + assertEq(erc20.balanceOf(recipient), currentCurrencyBalOfRecipient - _mintrequest.price); + } + + function test_state_mintWithSignature_NonZeroPrice_NativeToken() public { + vm.warp(1000); + + _mintrequest.price = 1; + _mintrequest.currency = address(NATIVE_TOKEN); + _signature = signMintRequest(_mintrequest, privateKey); + + uint256 currentTotalSupply = loyaltyPoints.totalSupply(); + uint256 currentBalanceOfRecipient = loyaltyPoints.balanceOf(recipient); + + vm.deal(recipient, 1); + uint256 currentCurrencyBalOfRecipient = recipient.balance; + + vm.prank(recipient); + loyaltyPoints.mintWithSignature{ value: 1 }(_mintrequest, _signature); + + assertEq(loyaltyPoints.totalSupply(), currentTotalSupply + _mintrequest.quantity); + assertEq(loyaltyPoints.balanceOf(recipient), currentBalanceOfRecipient + _mintrequest.quantity); + assertEq(recipient.balance, currentCurrencyBalOfRecipient - _mintrequest.price); + } + + function test_revert_mintWithSignature_InvalidMsgValue() public { + vm.warp(1000); + + _mintrequest.price = 1; + _mintrequest.currency = address(NATIVE_TOKEN); + _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(recipient); + vm.expectRevert("Invalid msg value"); + loyaltyPoints.mintWithSignature{ value: 0 }(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_ZeroQty() public { + vm.warp(1000); + + _mintrequest.quantity = 0; + _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("Minting zero qty"); + loyaltyPoints.mintWithSignature(_mintrequest, _signature); + } +} diff --git a/src/test/Marketplace.t.sol b/src/test/Marketplace.t.sol deleted file mode 100644 index f14c6c181..000000000 --- a/src/test/Marketplace.t.sol +++ /dev/null @@ -1,469 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -import { Marketplace, IMarketplace } from "contracts/marketplace/Marketplace.sol"; - -// Test imports -import "./utils/BaseTest.sol"; - -contract MarketplaceTest is BaseTest { - Marketplace public marketplace; - - Marketplace.ListingParameters public directListing; - Marketplace.ListingParameters public auctionListing; - - function setUp() public override { - super.setUp(); - marketplace = Marketplace(payable(getContract("Marketplace"))); - } - - function createERC721Listing( - address to, - address currency, - uint256 price, - IMarketplace.ListingType listingType - ) public returns (uint256 listingId, Marketplace.ListingParameters memory listing) { - uint256 tokenId = erc721.nextTokenIdToMint(); - erc721.mint(to, 1); - vm.prank(to); - erc721.setApprovalForAll(address(marketplace), true); - - listing.assetContract = address(erc721); - listing.tokenId = tokenId; - listing.startTime = 0; - listing.secondsUntilEndTime = 1 * 24 * 60 * 60; // 1 day - listing.quantityToList = 1; - listing.currencyToAccept = currency; - listing.reservePricePerToken = 0; - listing.buyoutPricePerToken = price; - listing.listingType = listingType; - - listingId = marketplace.totalListings(); - vm.prank(to); - marketplace.createListing(listing); - } - - function getListing(uint256 _listingId) public view returns (Marketplace.Listing memory listing) { - ( - uint256 listingId, - address tokenOwner, - address assetContract, - uint256 tokenId, - uint256 startTime, - uint256 endTime, - uint256 quantity, - address currency, - uint256 reservePricePerToken, - uint256 buyoutPricePerToken, - IMarketplace.TokenType tokenType, - IMarketplace.ListingType listingType - ) = marketplace.listings(_listingId); - listing.listingId = listingId; - listing.tokenOwner = tokenOwner; - listing.assetContract = assetContract; - listing.tokenId = tokenId; - listing.startTime = startTime; - listing.endTime = endTime; - listing.quantity = quantity; - listing.currency = currency; - listing.reservePricePerToken = reservePricePerToken; - listing.buyoutPricePerToken = buyoutPricePerToken; - listing.tokenType = tokenType; - listing.listingType = listingType; - } - - function getWinningBid(uint256 _listingId) public view returns (Marketplace.Offer memory winningBid) { - ( - uint256 listingId, - address offeror, - uint256 quantityWanted, - address currency, - uint256 pricePerToken, - - ) = marketplace.winningBid(_listingId); - winningBid.listingId = listingId; - winningBid.offeror = offeror; - winningBid.quantityWanted = quantityWanted; - winningBid.currency = currency; - winningBid.pricePerToken = pricePerToken; - } - - function test_createListing_auctionListing() public { - vm.warp(0); - (uint256 createdListingId, Marketplace.ListingParameters memory createdListing) = createERC721Listing( - getActor(0), - NATIVE_TOKEN, - 1 ether, - IMarketplace.ListingType.Auction - ); - - Marketplace.Listing memory listing = getListing(createdListingId); - assertEq(createdListingId, listing.listingId); - assertEq(createdListing.assetContract, listing.assetContract); - assertEq(createdListing.tokenId, listing.tokenId); - assertEq(createdListing.startTime, listing.startTime); - assertEq(createdListing.startTime + createdListing.secondsUntilEndTime, listing.endTime); - assertEq(createdListing.quantityToList, listing.quantity); - assertEq(createdListing.currencyToAccept, listing.currency); - assertEq(createdListing.reservePricePerToken, listing.reservePricePerToken); - assertEq(createdListing.buyoutPricePerToken, listing.buyoutPricePerToken); - assertEq(uint8(IMarketplace.TokenType.ERC721), uint8(listing.tokenType)); - assertEq(uint8(IMarketplace.ListingType.Auction), uint8(listing.listingType)); - } - - function test_offer_bidAuctionNativeToken() public { - vm.deal(getActor(0), 100 ether); - - Marketplace.Offer memory winningBid; - vm.warp(0); - (uint256 listingId, ) = createERC721Listing( - getActor(0), - NATIVE_TOKEN, - 123456 ether, - IMarketplace.ListingType.Auction - ); - - assertEq(getActor(0).balance, 100 ether); - - vm.prank(getActor(0)); - vm.warp(1); - marketplace.offer{ value: 1 ether }(listingId, 1, NATIVE_TOKEN, 1 ether, type(uint256).max); - winningBid = getWinningBid(listingId); - assertEq(getActor(0).balance, 99 ether); - assertEq(winningBid.listingId, listingId); - assertEq(winningBid.offeror, getActor(0)); - assertEq(winningBid.quantityWanted, 1); - assertEq(winningBid.currency, NATIVE_TOKEN); - assertEq(winningBid.pricePerToken, 1 ether); - - vm.prank(getActor(0)); - vm.warp(2); - marketplace.offer{ value: 2 ether }(listingId, 1, NATIVE_TOKEN, 2 ether, type(uint256).max); - winningBid = getWinningBid(listingId); - assertEq(getActor(0).balance, 98 ether); - assertEq(winningBid.listingId, listingId); - assertEq(winningBid.offeror, getActor(0)); - assertEq(winningBid.quantityWanted, 1); - assertEq(winningBid.currency, NATIVE_TOKEN); - assertEq(winningBid.pricePerToken, 2 ether); - } - - function test_closeAuctionForCreator_afterBuyout() public { - vm.deal(getActor(0), 100 ether); - vm.deal(getActor(1), 100 ether); - - // Actor-0 creates an auction listing. - vm.prank(getActor(0)); - vm.warp(0); - (uint256 listingId, ) = createERC721Listing( - getActor(0), - NATIVE_TOKEN, - 5 ether, - IMarketplace.ListingType.Auction - ); - - Marketplace.Listing memory listing = getListing(listingId); - assertEq(erc721.ownerOf(listing.tokenId), address(marketplace)); - assertEq(weth.balanceOf(address(marketplace)), 0); - - /** - * Actor-1 bids with buyout price. Outcome: - * - Actor-1 receives auctioned items escrowed in Marketplace. - * - Winning bid amount is escrowed in the contract. - */ - vm.prank(getActor(1)); - vm.warp(1); - marketplace.offer{ value: 5 ether }(listingId, 1, NATIVE_TOKEN, 5 ether, type(uint256).max); - - assertEq(erc721.ownerOf(listing.tokenId), getActor(1)); - assertEq(weth.balanceOf(address(marketplace)), 5 ether); - - /** - * Auction is closed for the auction creator i.e. Actor-0. Outcome: - * - Actor-0 receives the escrowed buyout amount. - */ - - uint256 listerBalBefore = getActor(0).balance; - - vm.warp(2); - vm.prank(getActor(2)); - marketplace.closeAuction(listingId, getActor(0)); - - uint256 listerBalAfter = getActor(0).balance; - uint256 winningBidPostFee = (5 ether * (MAX_BPS - platformFeeBps)) / MAX_BPS; - - assertEq(listerBalAfter - listerBalBefore, winningBidPostFee); - assertEq(weth.balanceOf(address(marketplace)), 0); - } - - function test_acceptOffer_whenListingAcceptsNativeToken() public { - vm.deal(getActor(0), 100 ether); - vm.deal(getActor(1), 100 ether); - - // Actor-0 creates a direct listing with NATIVE_TOKEN as accepted currency. - vm.prank(getActor(0)); - vm.warp(0); - (uint256 listingId, ) = createERC721Listing( - getActor(0), - NATIVE_TOKEN, - 5 ether, - IMarketplace.ListingType.Direct - ); - - vm.startPrank(getActor(1)); - - // Actor-1 mints 4 ether worth of WETH - assertEq(weth.balanceOf(getActor(1)), 0); - weth.deposit{ value: 4 ether }(); - assertEq(weth.balanceOf(getActor(1)), 4 ether); - - // Actor-1 makes an offer to the direct listing for 4 WETH. - weth.approve(address(marketplace), 4 ether); - - vm.warp(1); - marketplace.offer(listingId, 1, NATIVE_TOKEN, 4 ether, type(uint256).max); - - vm.stopPrank(); - - // Actor-0 successfully accepts the offer. - Marketplace.Listing memory listing = getListing(listingId); - assertEq(erc721.ownerOf(listing.tokenId), getActor(0)); - assertEq(weth.balanceOf(getActor(0)), 0); - assertEq(weth.balanceOf(getActor(1)), 4 ether); - - uint256 offerValuePostFee = (4 ether * (MAX_BPS - platformFeeBps)) / MAX_BPS; - - vm.prank(getActor(0)); - vm.warp(2); - marketplace.acceptOffer(listingId, getActor(1), address(weth), 4 ether); - assertEq(erc721.ownerOf(listing.tokenId), getActor(1)); - assertEq(weth.balanceOf(getActor(0)), offerValuePostFee); - assertEq(weth.balanceOf(getActor(1)), 0); - } - - function test_acceptOffer_expiration() public { - vm.deal(getActor(0), 100 ether); - vm.deal(getActor(1), 100 ether); - - // Actor-0 creates a direct listing with NATIVE_TOKEN as accepted currency. - vm.prank(getActor(0)); - (uint256 listingId, ) = createERC721Listing( - getActor(0), - NATIVE_TOKEN, - 5 ether, - IMarketplace.ListingType.Direct - ); - - vm.startPrank(getActor(1)); - - // Actor-1 mints 4 ether worth of WETH - assertEq(weth.balanceOf(getActor(1)), 0); - weth.deposit{ value: 4 ether }(); - assertEq(weth.balanceOf(getActor(1)), 4 ether); - - // Actor-1 makes an offer to the direct listing for 4 WETH. - weth.approve(address(marketplace), 4 ether); - - vm.warp(2); - marketplace.offer(listingId, 1, NATIVE_TOKEN, 4 ether, 0); - - vm.stopPrank(); - - // Actor-0 successfully accepts the offer. - Marketplace.Listing memory listing = getListing(listingId); - assertEq(erc721.ownerOf(listing.tokenId), getActor(0)); - assertEq(weth.balanceOf(getActor(0)), 0); - assertEq(weth.balanceOf(getActor(1)), 4 ether); - - vm.prank(getActor(0)); - vm.expectRevert(bytes("EXPIRED")); - marketplace.acceptOffer(listingId, getActor(1), address(weth), 4 ether); - - vm.prank(getActor(1)); - vm.warp(3); - marketplace.offer(listingId, 1, NATIVE_TOKEN, 4 ether, 5); - - vm.prank(getActor(0)); - vm.warp(4); - marketplace.acceptOffer(listingId, getActor(1), address(weth), 4 ether); - } - - function test_createListing_startTime_past() public { - address to = getActor(0); - uint256 tokenId = erc721.nextTokenIdToMint(); - vm.startPrank(to); - erc721.mint(to, 1); - erc721.setApprovalForAll(address(marketplace), true); - - // initial block.timestamp - vm.warp(100 days); - - Marketplace.ListingParameters memory listing; - listing.assetContract = address(erc721); - listing.tokenId = tokenId; - listing.startTime = 0; - listing.secondsUntilEndTime = 1 * 24 * 60 * 60; // 1 day - listing.quantityToList = 1; - listing.currencyToAccept = NATIVE_TOKEN; - listing.reservePricePerToken = 0; - listing.buyoutPricePerToken = 1 ether; - listing.listingType = IMarketplace.ListingType.Direct; - - vm.expectRevert(bytes("ST")); - marketplace.createListing(listing); - } - - function test_createListing_startTime_pastWithBuffer() public { - address to = getActor(0); - uint256 tokenId = erc721.nextTokenIdToMint(); - vm.startPrank(to); - erc721.mint(to, 1); - erc721.setApprovalForAll(address(marketplace), true); - - // initial block.timestamp - vm.warp(100 days); - - Marketplace.ListingParameters memory listing; - listing.assetContract = address(erc721); - listing.tokenId = tokenId; - listing.startTime = block.timestamp - 30 minutes; - listing.secondsUntilEndTime = 1 * 24 * 60 * 60; // 1 day - listing.quantityToList = 1; - listing.currencyToAccept = NATIVE_TOKEN; - listing.reservePricePerToken = 0; - listing.buyoutPricePerToken = 1 ether; - listing.listingType = IMarketplace.ListingType.Direct; - - marketplace.createListing(listing); - } - - function test_createListing_startTime_now() public { - address to = getActor(0); - uint256 tokenId = erc721.nextTokenIdToMint(); - vm.startPrank(to); - erc721.mint(to, 1); - erc721.setApprovalForAll(address(marketplace), true); - - // initial block.timestamp - vm.warp(100 days); - - Marketplace.ListingParameters memory listing; - listing.assetContract = address(erc721); - listing.tokenId = tokenId; - listing.startTime = block.timestamp; - listing.secondsUntilEndTime = 1 * 24 * 60 * 60; // 1 day - listing.quantityToList = 1; - listing.currencyToAccept = NATIVE_TOKEN; - listing.reservePricePerToken = 0; - listing.buyoutPricePerToken = 1 ether; - listing.listingType = IMarketplace.ListingType.Direct; - - marketplace.createListing(listing); - } - - function test_createListing_startTime_future() public { - address to = getActor(0); - uint256 tokenId = erc721.nextTokenIdToMint(); - vm.startPrank(to); - erc721.mint(to, 1); - erc721.setApprovalForAll(address(marketplace), true); - - // initial block.timestamp - vm.warp(100 days); - - Marketplace.ListingParameters memory listing; - listing.assetContract = address(erc721); - listing.tokenId = tokenId; - listing.startTime = 200 days; - listing.secondsUntilEndTime = 1 * 24 * 60 * 60; // 1 day - listing.quantityToList = 1; - listing.currencyToAccept = NATIVE_TOKEN; - listing.reservePricePerToken = 0; - listing.buyoutPricePerToken = 1 ether; - listing.listingType = IMarketplace.ListingType.Direct; - - marketplace.createListing(listing); - } - - function test_updateListing_startTime_past() public { - address to = getActor(0); - uint256 tokenId = erc721.nextTokenIdToMint(); - vm.startPrank(to); - erc721.mint(to, 1); - erc721.setApprovalForAll(address(marketplace), true); - - vm.warp(100 days); - - // future listing - Marketplace.ListingParameters memory listing; - listing.assetContract = address(erc721); - listing.tokenId = tokenId; - listing.startTime = 200 days; - listing.secondsUntilEndTime = 1 * 24 * 60 * 60; // 1 day - listing.quantityToList = 1; - listing.currencyToAccept = NATIVE_TOKEN; - listing.reservePricePerToken = 0; - listing.buyoutPricePerToken = 1 ether; - listing.listingType = IMarketplace.ListingType.Direct; - marketplace.createListing(listing); - - // update into the past - vm.expectRevert(bytes("ST")); - marketplace.updateListing(0, 1, 0, 1 ether, NATIVE_TOKEN, 99 days, 0); - } - - function test_updateListing_startTime_future() public { - address to = getActor(0); - uint256 tokenId = erc721.nextTokenIdToMint(); - vm.startPrank(to); - erc721.mint(to, 1); - erc721.setApprovalForAll(address(marketplace), true); - - vm.warp(100 days); - - // future listing - Marketplace.ListingParameters memory listing; - listing.assetContract = address(erc721); - listing.tokenId = tokenId; - listing.startTime = 200 days; - listing.secondsUntilEndTime = 1 * 24 * 60 * 60; // 1 day - listing.quantityToList = 1; - listing.currencyToAccept = NATIVE_TOKEN; - listing.reservePricePerToken = 0; - listing.buyoutPricePerToken = 1 ether; - listing.listingType = IMarketplace.ListingType.Direct; - marketplace.createListing(listing); - - // future time - marketplace.updateListing(0, 1, 0, 1 ether, NATIVE_TOKEN, 205 days, 0); - } - - function test_updateListing_startTimeAndEndTime() public { - address to = getActor(0); - uint256 tokenId = erc721.nextTokenIdToMint(); - vm.startPrank(to); - erc721.mint(to, 1); - erc721.setApprovalForAll(address(marketplace), true); - - vm.warp(100 days); - - // future listing - Marketplace.ListingParameters memory listing; - listing.assetContract = address(erc721); - listing.tokenId = tokenId; - listing.startTime = 200 days; - listing.secondsUntilEndTime = 1 * 24 * 60 * 60; // 1 day - listing.quantityToList = 1; - listing.currencyToAccept = NATIVE_TOKEN; - listing.reservePricePerToken = 0; - listing.buyoutPricePerToken = 1 ether; - listing.listingType = IMarketplace.ListingType.Direct; - marketplace.createListing(listing); - - // future time - marketplace.updateListing(0, 1, 0, 1 ether, NATIVE_TOKEN, 205 days, 1 days); - Marketplace.Listing memory updatedListing = getListing(0); - assertEq(205 days, updatedListing.startTime); - assertEq(205 days + 1 days, updatedListing.endTime); - } -} diff --git a/src/test/Multicall.t.sol b/src/test/Multicall.t.sol new file mode 100644 index 000000000..3b9bbfb93 --- /dev/null +++ b/src/test/Multicall.t.sol @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "@std/Test.sol"; + +import { Multicall } from "contracts/extension/Multicall.sol"; +import { Forwarder } from "contracts/infra/forwarder/Forwarder.sol"; +import { ERC2771Context } from "contracts/extension/upgradeable/ERC2771Context.sol"; +import { TokenERC721 } from "contracts/prebuilts/token/TokenERC721.sol"; +import { Strings } from "contracts/lib/Strings.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MockMulticallForwarderConsumer is Multicall, ERC2771Context { + event Increment(address caller); + mapping(address => uint256) public counter; + + constructor(address[] memory trustedForwarders) ERC2771Context(trustedForwarders) {} + + function increment() external { + counter[_msgSender()]++; + emit Increment(_msgSender()); + } + + function _msgSender() internal view override(Multicall, ERC2771Context) returns (address sender) { + return ERC2771Context._msgSender(); + } +} + +contract MulticallTest is Test { + // Target (mock) contract + address internal consumer; + TokenERC721 internal token; + + address internal user1; + uint256 internal user1Pkey = 100; + + address internal user2; + uint256 internal user2Pkey = 200; + + // Forwarder details + Forwarder internal forwarder; + + bytes32 internal typehashForwardRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + function setUp() public { + user1 = vm.addr(user1Pkey); + user2 = vm.addr(user2Pkey); + + // Deploy forwarder + forwarder = new Forwarder(); + + // Deploy consumer + address[] memory forwarders = new address[](1); + forwarders[0] = address(forwarder); + consumer = address(new MockMulticallForwarderConsumer(forwarders)); + + // Deploy `TokenERC721` + address impl = address(new TokenERC721()); + token = TokenERC721( + address( + new TWProxy( + impl, + abi.encodeWithSelector( + TokenERC721.initialize.selector, + user1, + "name", + "SYMBOL", + "ipfs://", + forwarders, + user1, + user1, + 0, + 0, + user1 + ) + ) + ) + ); + + // Setup forwarder details + typehashForwardRequest = keccak256( + "ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data)" + ); + nameHash = keccak256(bytes("GSNv2 Forwarder")); + versionHash = keccak256(bytes("0.0.1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256( + abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(forwarder)) + ); + + vm.label(user1, "USER_1"); + vm.label(user2, "USER_2"); + vm.label(address(forwarder), "FORWARDER"); + vm.label(address(consumer), "CONSUMER"); + } + + function _signForwarderRequest( + Forwarder.ForwardRequest memory forwardRequest, + uint256 privateKey + ) internal view returns (bytes memory) { + bytes memory encodedRequest = abi.encode( + typehashForwardRequest, + forwardRequest.from, + forwardRequest.to, + forwardRequest.value, + forwardRequest.gas, + forwardRequest.nonce, + keccak256(forwardRequest.data) + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, typedDataHash); + bytes memory signature = abi.encodePacked(r, s, v); + + return signature; + } + + function test_multicall_viaDirectCall() public { + // Make 3 calls to `increment` within a multicall + bytes[] memory calls = new bytes[](3); + calls[0] = abi.encodeWithSelector(MockMulticallForwarderConsumer.increment.selector); + calls[1] = abi.encodeWithSelector(MockMulticallForwarderConsumer.increment.selector); + calls[2] = abi.encodeWithSelector(MockMulticallForwarderConsumer.increment.selector); + + // CASE 1: multicall without using forwarder. Should increment counter for the caller i.e. `msg.sender`. + + assertEq(MockMulticallForwarderConsumer(consumer).counter(user1), 0); + + vm.prank(user1); + Multicall(consumer).multicall(calls); + + assertEq(MockMulticallForwarderConsumer(consumer).counter(user1), 3); // counter incremented! + } + + function test_multicall_viaForwarder() public { + // Make 3 calls to `increment` within a multicall + bytes[] memory calls = new bytes[](3); + calls[0] = abi.encodeWithSelector(MockMulticallForwarderConsumer.increment.selector); + calls[1] = abi.encodeWithSelector(MockMulticallForwarderConsumer.increment.selector); + calls[2] = abi.encodeWithSelector(MockMulticallForwarderConsumer.increment.selector); + + // CASE 2: multicall with using forwarder. Should increment counter for the signer of the forwarder request. + + bytes memory multicallData = abi.encodeWithSelector(Multicall.multicall.selector, calls); + + Forwarder.ForwardRequest memory forwardRequest; + + forwardRequest.from = user1; + forwardRequest.to = address(consumer); + forwardRequest.value = 0; + forwardRequest.gas = 100_000; + forwardRequest.nonce = Forwarder(forwarder).getNonce(user1); + forwardRequest.data = multicallData; + + bytes memory signature = _signForwarderRequest(forwardRequest, user1Pkey); + + Forwarder(forwarder).execute(forwardRequest, signature); + + assertEq(MockMulticallForwarderConsumer(consumer).counter(user1), 3); // counter incremented! + } + + function test_multicall_viaForwarder_attemptSpoof() public { + // Make 3 calls to `increment` within a multicall + bytes[] memory callsSpoof = new bytes[](3); + callsSpoof[0] = abi.encodePacked( + abi.encodeWithSelector(MockMulticallForwarderConsumer.increment.selector), + user1 + ); + callsSpoof[1] = abi.encodePacked( + abi.encodeWithSelector(MockMulticallForwarderConsumer.increment.selector), + user1 + ); + callsSpoof[2] = abi.encodePacked( + abi.encodeWithSelector(MockMulticallForwarderConsumer.increment.selector), + user1 + ); + + // CASE 3: attempting to spoof address by manually appending address to multicall data arg. + // + // This attempt fails because `multicall` enforces original forwarder request signer + // as the `_msgSender()`. + + bytes memory multicallDataSpoof = abi.encodeWithSelector(Multicall.multicall.selector, callsSpoof); + + // user2 spoofing as user1 + Forwarder.ForwardRequest memory forwardRequestSpoof; + + forwardRequestSpoof.from = user2; + forwardRequestSpoof.to = address(consumer); + forwardRequestSpoof.value = 0; + forwardRequestSpoof.gas = 100_000; + forwardRequestSpoof.nonce = Forwarder(forwarder).getNonce(user2); + forwardRequestSpoof.data = multicallDataSpoof; + + bytes memory signatureSpoof = _signForwarderRequest(forwardRequestSpoof, user2Pkey); + + // vm.expectRevert(); + Forwarder(forwarder).execute(forwardRequestSpoof, signatureSpoof); + + assertEq(MockMulticallForwarderConsumer(consumer).counter(user1), 0); // counter unchanged! + assertEq(MockMulticallForwarderConsumer(consumer).counter(user2), 3); // counter incremented for forwarder request signer! + } + + function test_multicall_tokenerc721_viaForwarder_attemptSpoof() public { + // User1 is admin on `token` + assertTrue(token.hasRole(keccak256("MINTER_ROLE"), user1)); + + // token ID `0` has no owner + vm.expectRevert("ERC721: invalid token ID"); + token.ownerOf(0); + + // Make call to `mintTo` within a multicall + bytes[] memory callsSpoof = new bytes[](1); + callsSpoof[0] = abi.encodePacked( + abi.encodeWithSelector(TokenERC721.mintTo.selector, user2, "metadataURI"), + user1 + ); + // CASE: attempting to spoof address by manually appending address to multicall data arg. + // + // This attempt fails because `multicall` enforces original forwarder request signer + // as the `_msgSender()`. + + bytes memory multicallDataSpoof = abi.encodeWithSelector(Multicall.multicall.selector, callsSpoof); + + // user2 spoofing as user1 + Forwarder.ForwardRequest memory forwardRequestSpoof; + + forwardRequestSpoof.from = user2; + forwardRequestSpoof.to = address(token); + forwardRequestSpoof.value = 0; + forwardRequestSpoof.gas = 100_000; + forwardRequestSpoof.nonce = Forwarder(forwarder).getNonce(user2); + forwardRequestSpoof.data = multicallDataSpoof; + + bytes memory signatureSpoof = _signForwarderRequest(forwardRequestSpoof, user2Pkey); + + // Minter role check occurs on user2 i.e. signer of the forwarder request, and not user1 i.e. the address user2 attempts to spoof. + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(user2), 20), + " is missing role ", + Strings.toHexString(uint256(keccak256("MINTER_ROLE")), 32) + ) + ); + Forwarder(forwarder).execute(forwardRequestSpoof, signatureSpoof); + + // token ID `0` still has no owner + vm.expectRevert("ERC721: invalid token ID"); + token.ownerOf(0); + } +} diff --git a/src/test/Multiwrap.t.sol b/src/test/Multiwrap.t.sol index 7377cd040..de0dd7bf8 100644 --- a/src/test/Multiwrap.t.sol +++ b/src/test/Multiwrap.t.sol @@ -1,12 +1,13 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; -import { Multiwrap } from "contracts/multiwrap/Multiwrap.sol"; +import { Multiwrap } from "contracts/prebuilts/multiwrap/Multiwrap.sol"; import { ITokenBundle } from "contracts/extension/interface/ITokenBundle.sol"; +import { CurrencyTransferLib } from "contracts/lib/CurrencyTransferLib.sol"; // Test imports -import "contracts/lib/TWStrings.sol"; import { MockERC20 } from "./mocks/MockERC20.sol"; +import { Strings } from "contracts/lib/Strings.sol"; import { Wallet } from "./utils/Wallet.sol"; import "./utils/BaseTest.sol"; @@ -18,11 +19,7 @@ contract MultiwrapReentrant is MockERC20, ITokenBundle { multiwrap = Multiwrap(_multiwrap); } - function transferFrom( - address from, - address to, - uint256 amount - ) public override returns (bool) { + function transferFrom(address from, address to, uint256 amount) public override returns (bool) { multiwrap.unwrap(0, address(this)); return super.transferFrom(from, to, amount); } @@ -116,14 +113,7 @@ contract MultiwrapTest is BaseTest { bytes32 role = keccak256("MINTER_ROLE"); vm.prank(caller); - vm.expectRevert( - abi.encodePacked( - "Permissions: account ", - TWStrings.toHexString(uint160(caller), 20), - " is missing role ", - TWStrings.toHexString(uint256(role), 32) - ) - ); + vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsUnauthorizedAccount.selector, caller, role)); multiwrap.renounceRole(role, caller); } @@ -136,14 +126,7 @@ contract MultiwrapTest is BaseTest { bytes32 role = keccak256("MINTER_ROLE"); vm.prank(deployer); - vm.expectRevert( - abi.encodePacked( - "Permissions: account ", - TWStrings.toHexString(uint160(target), 20), - " is missing role ", - TWStrings.toHexString(uint256(role), 32) - ) - ); + vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsUnauthorizedAccount.selector, target, role)); multiwrap.revokeRole(role, target); } @@ -338,17 +321,14 @@ contract MultiwrapTest is BaseTest { address recipient = address(0x123); - string memory errorMsg = string( - abi.encodePacked( - "Permissions: account ", - Strings.toHexString(uint160(wrappedContent[0].assetContract), 20), - " is missing role ", - Strings.toHexString(uint256(keccak256("ASSET_ROLE")), 32) + vm.prank(address(tokenOwner)); + vm.expectRevert( + abi.encodeWithSelector( + Permissions.PermissionsUnauthorizedAccount.selector, + address(erc20), + keccak256("ASSET_ROLE") ) ); - - vm.prank(address(tokenOwner)); - vm.expectRevert(bytes(errorMsg)); multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); } @@ -361,17 +341,14 @@ contract MultiwrapTest is BaseTest { address recipient = address(0x123); - string memory errorMsg = string( - abi.encodePacked( - "Permissions: account ", - Strings.toHexString(uint160(address(tokenOwner)), 20), - " is missing role ", - Strings.toHexString(uint256(keccak256("MINTER_ROLE")), 32) + vm.prank(address(tokenOwner)); + vm.expectRevert( + abi.encodeWithSelector( + Permissions.PermissionsUnauthorizedAccount.selector, + address(tokenOwner), + keccak256("MINTER_ROLE") ) ); - - vm.prank(address(tokenOwner)); - vm.expectRevert(bytes(errorMsg)); multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); } @@ -392,7 +369,9 @@ contract MultiwrapTest is BaseTest { }); vm.prank(address(tokenOwner)); - vm.expectRevert("msg.value != amount"); + vm.expectRevert( + abi.encodeWithSelector(CurrencyTransferLib.CurrencyTransferLibMismatchedValue.selector, 0, 10 ether) + ); multiwrap.wrap(nativeTokenContentToWrap, uriForWrappedToken, recipient); } @@ -446,7 +425,7 @@ contract MultiwrapTest is BaseTest { address recipient = address(0x123); vm.prank(address(tokenOwner)); - vm.expectRevert("ERC721: transfer caller is not owner nor approved"); + vm.expectRevert("ERC721: caller is not token owner or approved"); multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); } @@ -485,7 +464,7 @@ contract MultiwrapTest is BaseTest { address recipient = address(0x123); vm.prank(address(tokenOwner)); - vm.expectRevert("ERC721: transfer caller is not owner nor approved"); + vm.expectRevert("ERC721: caller is not token owner or approved"); multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); } @@ -498,7 +477,7 @@ contract MultiwrapTest is BaseTest { address recipient = address(0x123); vm.prank(address(tokenOwner)); - vm.expectRevert("ERC1155: caller is not owner nor approved"); + vm.expectRevert("ERC1155: caller is not token owner or approved"); multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); } @@ -508,7 +487,7 @@ contract MultiwrapTest is BaseTest { address recipient = address(0x123); vm.prank(address(tokenOwner)); - vm.expectRevert("TokenBundle: no tokens to bind."); + vm.expectRevert("!Tokens"); multiwrap.wrap(emptyContent, uriForWrappedToken, recipient); } @@ -533,7 +512,9 @@ contract MultiwrapTest is BaseTest { }); vm.prank(address(tokenOwner)); - vm.expectRevert("msg.value != amount"); + vm.expectRevert( + abi.encodeWithSelector(CurrencyTransferLib.CurrencyTransferLibMismatchedValue.selector, 10 ether, 20 ether) + ); multiwrap.wrap{ value: 10 ether }(nativeTokenContentToWrap, uriForWrappedToken, recipient); assertEq(address(multiwrap).balance, 10 ether); @@ -559,7 +540,7 @@ contract MultiwrapTest is BaseTest { vm.prank(recipient); multiwrap.unwrap(expectedIdForWrappedToken, recipient); - vm.expectRevert("ERC721: owner query for nonexistent token"); + vm.expectRevert("ERC721: invalid token ID"); multiwrap.ownerOf(expectedIdForWrappedToken); assertEq(uriForWrappedToken, multiwrap.tokenURI(expectedIdForWrappedToken)); @@ -622,7 +603,7 @@ contract MultiwrapTest is BaseTest { vm.prank(approvedCaller); multiwrap.unwrap(expectedIdForWrappedToken, recipient); - vm.expectRevert("ERC721: owner query for nonexistent token"); + vm.expectRevert("ERC721: invalid token ID"); multiwrap.ownerOf(expectedIdForWrappedToken); assertEq(uriForWrappedToken, multiwrap.tokenURI(expectedIdForWrappedToken)); @@ -747,17 +728,14 @@ contract MultiwrapTest is BaseTest { vm.prank(deployer); multiwrap.revokeRole(keccak256("UNWRAP_ROLE"), address(0)); - string memory errorMsg = string( - abi.encodePacked( - "Permissions: account ", - Strings.toHexString(uint160(recipient), 20), - " is missing role ", - Strings.toHexString(uint256(keccak256("UNWRAP_ROLE")), 32) + vm.prank(recipient); + vm.expectRevert( + abi.encodeWithSelector( + Permissions.PermissionsUnauthorizedAccount.selector, + recipient, + keccak256("UNWRAP_ROLE") ) ); - - vm.prank(recipient); - vm.expectRevert(bytes(errorMsg)); multiwrap.unwrap(expectedIdForWrappedToken, recipient); } @@ -854,7 +832,7 @@ contract MultiwrapTest is BaseTest { vm.prank(recipient); multiwrap.unwrap(expectedIdForWrappedToken, recipient); - vm.expectRevert("ERC721: owner query for nonexistent token"); + vm.expectRevert("ERC721: invalid token ID"); multiwrap.ownerOf(expectedIdForWrappedToken); assertEq(uriForWrappedToken, multiwrap.tokenURI(expectedIdForWrappedToken)); diff --git a/src/test/OpenEditionERC721.t.sol b/src/test/OpenEditionERC721.t.sol new file mode 100644 index 000000000..2b9a63db7 --- /dev/null +++ b/src/test/OpenEditionERC721.t.sol @@ -0,0 +1,790 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { ERC721AUpgradeable, OpenEditionERC721, ISharedMetadata } from "contracts/prebuilts/open-edition/OpenEditionERC721.sol"; +import { Drop } from "contracts/extension/Drop.sol"; +import { LazyMint } from "contracts/extension/LazyMint.sol"; +import { OpenEditionERC721 } from "contracts/prebuilts/open-edition/OpenEditionERC721.sol"; +import { NFTMetadataRenderer } from "contracts/lib/NFTMetadataRenderer.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "erc721a-upgradeable/contracts/IERC721AUpgradeable.sol"; +import "./utils/BaseTest.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; + +contract OpenEditionERC721Test is BaseTest { + using Strings for uint256; + using Strings for address; + + event SharedMetadataUpdated(string name, string description, string imageURI, string animationURI); + + OpenEditionERC721 public openEdition; + ISharedMetadata.SharedMetadataInfo public sharedMetadata; + + bytes private emptyEncodedBytes = abi.encode("", ""); + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + address openEditionImpl = address(new OpenEditionERC721()); + + vm.prank(deployer); + openEdition = OpenEditionERC721( + address( + new TWProxy( + openEditionImpl, + abi.encodeCall( + OpenEditionERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps + ) + ) + ) + ) + ); + + sharedMetadata = ISharedMetadata.SharedMetadataInfo({ + name: "Test", + description: "Test", + imageURI: "https://test.com", + animationURI: "https://test.com" + }); + + erc20.mint(deployer, 1_000 ether); + vm.deal(deployer, 1_000 ether); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: misc. + //////////////////////////////////////////////////////////////*/ + + /** + * note: Tests whether contract reverts when a non-holder renounces a role. + */ + function test_revert_nonHolder_renounceRole() public { + address caller = address(0x123); + bytes32 role = keccak256("MINTER_ROLE"); + + vm.prank(caller); + vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsUnauthorizedAccount.selector, caller, role)); + + openEdition.renounceRole(role, caller); + } + + /** + * note: Tests whether contract reverts when a role admin revokes a role for a non-holder. + */ + function test_revert_revokeRoleForNonHolder() public { + address target = address(0x123); + bytes32 role = keccak256("MINTER_ROLE"); + + vm.prank(deployer); + vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsUnauthorizedAccount.selector, target, role)); + + openEdition.revokeRole(role, target); + } + + /** + * @dev Tests whether contract reverts when a role is granted to an existent role holder. + */ + function test_revert_grant_role_to_account_with_role() public { + bytes32 role = keccak256("ABC_ROLE"); + address receiver = getActor(0); + + vm.startPrank(deployer); + + openEdition.grantRole(role, receiver); + + vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsAlreadyGranted.selector, receiver, role)); + openEdition.grantRole(role, receiver); + + vm.stopPrank(); + } + + /** + * @dev Tests contract state for Transfer role. + */ + function test_state_grant_transferRole() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + + // check if admin and address(0) have transfer role in the beginning + bool checkAddressZero = openEdition.hasRole(role, address(0)); + bool checkAdmin = openEdition.hasRole(role, deployer); + assertTrue(checkAddressZero); + assertTrue(checkAdmin); + + // check if transfer role can be granted to a non-holder + address receiver = getActor(0); + vm.startPrank(deployer); + openEdition.grantRole(role, receiver); + + // expect revert when granting to a holder + vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsAlreadyGranted.selector, receiver, role)); + openEdition.grantRole(role, receiver); + + // check if receiver has transfer role + bool checkReceiver = openEdition.hasRole(role, receiver); + assertTrue(checkReceiver); + + // check if role is correctly revoked + openEdition.revokeRole(role, receiver); + checkReceiver = openEdition.hasRole(role, receiver); + assertFalse(checkReceiver); + openEdition.revokeRole(role, address(0)); + checkAddressZero = openEdition.hasRole(role, address(0)); + assertFalse(checkAddressZero); + + vm.stopPrank(); + } + + /** + * note: Testing transfer of tokens when transfer-role is restricted + */ + function test_claim_transferRole() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + OpenEditionERC721.AllowlistProof memory alp; + alp.proof = proofs; + + OpenEditionERC721.ClaimCondition[] memory conditions = new OpenEditionERC721.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + openEdition.claim(receiver, 1, address(0), 0, alp, ""); + + // revoke transfer role from address(0) + vm.prank(deployer); + openEdition.revokeRole(keccak256("TRANSFER_ROLE"), address(0)); + vm.startPrank(receiver); + vm.expectRevert(bytes("!T")); + openEdition.transferFrom(receiver, address(123), 1); + } + + function test_claimCondition_with_startTimestamp() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + OpenEditionERC721.AllowlistProof memory alp; + alp.proof = proofs; + + OpenEditionERC721.ClaimCondition[] memory conditions = new OpenEditionERC721.ClaimCondition[](1); + conditions[0].startTimestamp = 100; + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + vm.warp(99); + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(abi.encodeWithSelector(Drop.DropNoActiveCondition.selector)); + openEdition.claim(receiver, 1, address(0), 0, alp, ""); + + vm.warp(100); + vm.prank(getActor(4), getActor(4)); + openEdition.claim(receiver, 1, address(0), 0, alp, ""); + } + + /*/////////////////////////////////////////////////////////////// + Set Shared Metadata Tests + //////////////////////////////////////////////////////////////*/ + + /* + * note: Testing state changes; set shared metadata for tokens. + */ + function test_state_sharedMetadata() public { + // SET METADATA + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + + // CLAIM 1 TOKEN + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "0"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + OpenEditionERC721.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 0; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + OpenEditionERC721.ClaimCondition[] memory conditions = new OpenEditionERC721.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + openEdition.claim(receiver, 100, address(erc20), 0, alp, ""); + + string memory uri = openEdition.tokenURI(1); + assertEq( + uri, + NFTMetadataRenderer.createMetadataEdition({ + name: sharedMetadata.name, + description: sharedMetadata.description, + imageURI: sharedMetadata.imageURI, + animationURI: sharedMetadata.animationURI, + tokenOfEdition: 1 + }) + ); + } + + /** + * note: Testing revert condition; an address without MINTER_ROLE calls setSharedMetadata function. + */ + function test_revert_setSharedMetadata_MINTER_ROLE() public { + vm.expectRevert(); + openEdition.setSharedMetadata(sharedMetadata); + } + + /** + * note: Testing event emission; shared metadata set. + */ + function test_event_setSharedMetadata_SharedMetadataUpdated() public { + vm.startPrank(deployer); + + vm.expectEmit(true, false, false, true); + emit SharedMetadataUpdated( + sharedMetadata.name, + sharedMetadata.description, + sharedMetadata.imageURI, + sharedMetadata.animationURI + ); + openEdition.setSharedMetadata(sharedMetadata); + + vm.stopPrank(); + } + + /*/////////////////////////////////////////////////////////////// + Claim Tests + //////////////////////////////////////////////////////////////*/ + + /** + * note: Testing revert condition; exceed max claimable supply. + */ + function test_revert_claimCondition_exceedMaxClaimableSupply() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + OpenEditionERC721.AllowlistProof memory alp; + alp.proof = proofs; + + OpenEditionERC721.ClaimCondition[] memory conditions = new OpenEditionERC721.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 200; + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + openEdition.claim(receiver, 100, address(0), 0, alp, ""); + + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedMaxSupply.selector, conditions[0].maxClaimableSupply, 101) + ); + vm.prank(getActor(6), getActor(6)); + openEdition.claim(receiver, 1, address(0), 0, alp, ""); + } + + /** + * note: Testing quantity limit restriction when no allowlist present. + */ + function test_fuzz_claim_noAllowlist(uint256 x) public { + vm.assume(x != 0); + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + OpenEditionERC721.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = x; + + OpenEditionERC721.ClaimCondition[] memory conditions = new OpenEditionERC721.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, conditions[0].quantityLimitPerWallet, 0) + ); + openEdition.claim(receiver, 0, address(0), 0, alp, ""); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, conditions[0].quantityLimitPerWallet, 101) + ); + openEdition.claim(receiver, 101, address(0), 0, alp, ""); + + vm.prank(deployer); + openEdition.setClaimConditions(conditions, true); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, conditions[0].quantityLimitPerWallet, 101) + ); + openEdition.claim(receiver, 101, address(0), 0, alp, ""); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price set to 0 + */ + function test_state_claim_allowlisted_SetQuantityZeroPrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "0"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + OpenEditionERC721.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 0; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + OpenEditionERC721.ClaimCondition[] memory conditions = new OpenEditionERC721.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + openEdition.claim(receiver, 100, address(erc20), 0, alp, ""); // claims for free, because allowlist price is 0 + assertEq(openEdition.getSupplyClaimedByWallet(openEdition.getActiveClaimConditionId(), receiver), 100); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price set to non-zero value + */ + function test_state_claim_allowlisted_SetQuantityPrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "5"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + OpenEditionERC721.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 5; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + OpenEditionERC721.ClaimCondition[] memory conditions = new OpenEditionERC721.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + vm.prank(receiver, receiver); + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimInvalidTokenPrice.selector, address(erc20), 0, address(erc20), 5) + ); + openEdition.claim(receiver, 100, address(erc20), 0, alp, ""); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(openEdition), 10000); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + openEdition.claim(receiver, 100, address(erc20), 5, alp, ""); + assertEq(openEdition.getSupplyClaimedByWallet(openEdition.getActiveClaimConditionId(), receiver), 100); + assertEq(erc20.balanceOf(receiver), 10000 - 500); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price not set; should default to general price and currency + */ + function test_state_claim_allowlisted_SetQuantityDefaultPrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = Strings.toString(type(uint256).max); // this implies that general price is applicable + inputs[4] = "0x0000000000000000000000000000000000000000"; + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + OpenEditionERC721.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = type(uint256).max; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + OpenEditionERC721.ClaimCondition[] memory conditions = new OpenEditionERC721.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(openEdition), 10000); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + openEdition.claim(receiver, 100, address(erc20), 10, alp, ""); + assertEq(openEdition.getSupplyClaimedByWallet(openEdition.getActiveClaimConditionId(), receiver), 100); + assertEq(erc20.balanceOf(receiver), 10000 - 1000); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to 0 => should default to general limit + * - allowlist price set to some value different than general price + */ + function test_state_claim_allowlisted_DefaultQuantitySomePrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "0"; // this implies that general limit is applicable + inputs[3] = "5"; + inputs[4] = "0x0000000000000000000000000000000000000000"; // general currency will be applicable + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + OpenEditionERC721.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 0; + alp.pricePerToken = 5; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + OpenEditionERC721.ClaimCondition[] memory conditions = new OpenEditionERC721.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(openEdition), 10000); + + vm.prank(receiver, receiver); + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, conditions[0].quantityLimitPerWallet, 100) + ); + openEdition.claim(receiver, 100, address(erc20), 5, alp, ""); // trying to claim more than general limit + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + openEdition.claim(receiver, 10, address(erc20), 5, alp, ""); + assertEq(openEdition.getSupplyClaimedByWallet(openEdition.getActiveClaimConditionId(), receiver), 10); + assertEq(erc20.balanceOf(receiver), 10000 - 50); + } + + function test_fuzz_claim_merkleProof(uint256 x) public { + vm.assume(x > 10 && x < 500); + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = Strings.toString(x); + inputs[3] = "0"; + inputs[4] = "0x0000000000000000000000000000000000000000"; + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + OpenEditionERC721.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = x; + alp.pricePerToken = 0; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + + // bytes32[] memory proofs = new bytes32[](0); + + OpenEditionERC721.ClaimCondition[] memory conditions = new OpenEditionERC721.ClaimCondition[](1); + conditions[0].maxClaimableSupply = x; + conditions[0].quantityLimitPerWallet = 1; + conditions[0].merkleRoot = root; + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + openEdition.claim(receiver, x - 5, address(0), 0, alp, ""); + assertEq(openEdition.getSupplyClaimedByWallet(openEdition.getActiveClaimConditionId(), receiver), x - 5); + + vm.prank(receiver, receiver); + vm.expectRevert(abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, x, x + 1)); + openEdition.claim(receiver, 6, address(0), 0, alp, ""); + + vm.prank(receiver, receiver); + openEdition.claim(receiver, 5, address(0), 0, alp, ""); + assertEq(openEdition.getSupplyClaimedByWallet(openEdition.getActiveClaimConditionId(), receiver), x); + + vm.prank(receiver, receiver); + vm.expectRevert(abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, x, x + 5)); + openEdition.claim(receiver, 5, address(0), 0, alp, ""); // quantity limit already claimed + } + + /** + * note: Testing state changes; reset eligibility of claim conditions and claiming again for same condition id. + */ + function test_state_claimCondition_resetEligibility() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + OpenEditionERC721.AllowlistProof memory alp; + alp.proof = proofs; + + OpenEditionERC721.ClaimCondition[] memory conditions = new OpenEditionERC721.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + openEdition.claim(receiver, 100, address(0), 0, alp, ""); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, conditions[0].quantityLimitPerWallet, 200) + ); + openEdition.claim(receiver, 100, address(0), 0, alp, ""); + + vm.prank(deployer); + openEdition.setClaimConditions(conditions, true); + + vm.prank(getActor(5), getActor(5)); + openEdition.claim(receiver, 100, address(0), 0, alp, ""); + } + + /*/////////////////////////////////////////////////////////////// + setClaimConditions + //////////////////////////////////////////////////////////////*/ + + function test_claimCondition_startIdAndCount() public { + vm.startPrank(deployer); + + uint256 currentStartId = 0; + uint256 count = 0; + + OpenEditionERC721.ClaimCondition[] memory conditions = new OpenEditionERC721.ClaimCondition[](2); + conditions[0].startTimestamp = 0; + conditions[0].maxClaimableSupply = 10; + conditions[1].startTimestamp = 1; + conditions[1].maxClaimableSupply = 10; + + openEdition.setClaimConditions(conditions, false); + (currentStartId, count) = openEdition.claimCondition(); + assertEq(currentStartId, 0); + assertEq(count, 2); + + openEdition.setClaimConditions(conditions, false); + (currentStartId, count) = openEdition.claimCondition(); + assertEq(currentStartId, 0); + assertEq(count, 2); + + openEdition.setClaimConditions(conditions, true); + (currentStartId, count) = openEdition.claimCondition(); + assertEq(currentStartId, 2); + assertEq(count, 2); + + openEdition.setClaimConditions(conditions, true); + (currentStartId, count) = openEdition.claimCondition(); + assertEq(currentStartId, 4); + assertEq(count, 2); + } + + function test_claimCondition_startPhase() public { + vm.startPrank(deployer); + + uint256 activeConditionId = 0; + + OpenEditionERC721.ClaimCondition[] memory conditions = new OpenEditionERC721.ClaimCondition[](3); + conditions[0].startTimestamp = 10; + conditions[0].maxClaimableSupply = 11; + conditions[0].quantityLimitPerWallet = 12; + conditions[1].startTimestamp = 20; + conditions[1].maxClaimableSupply = 21; + conditions[1].quantityLimitPerWallet = 22; + conditions[2].startTimestamp = 30; + conditions[2].maxClaimableSupply = 31; + conditions[2].quantityLimitPerWallet = 32; + openEdition.setClaimConditions(conditions, false); + + vm.expectRevert(abi.encodeWithSelector(Drop.DropNoActiveCondition.selector)); + openEdition.getActiveClaimConditionId(); + + vm.warp(10); + activeConditionId = openEdition.getActiveClaimConditionId(); + assertEq(activeConditionId, 0); + assertEq(openEdition.getClaimConditionById(activeConditionId).startTimestamp, 10); + assertEq(openEdition.getClaimConditionById(activeConditionId).maxClaimableSupply, 11); + assertEq(openEdition.getClaimConditionById(activeConditionId).quantityLimitPerWallet, 12); + + vm.warp(20); + activeConditionId = openEdition.getActiveClaimConditionId(); + assertEq(activeConditionId, 1); + assertEq(openEdition.getClaimConditionById(activeConditionId).startTimestamp, 20); + assertEq(openEdition.getClaimConditionById(activeConditionId).maxClaimableSupply, 21); + assertEq(openEdition.getClaimConditionById(activeConditionId).quantityLimitPerWallet, 22); + + vm.warp(30); + activeConditionId = openEdition.getActiveClaimConditionId(); + assertEq(activeConditionId, 2); + assertEq(openEdition.getClaimConditionById(activeConditionId).startTimestamp, 30); + assertEq(openEdition.getClaimConditionById(activeConditionId).maxClaimableSupply, 31); + assertEq(openEdition.getClaimConditionById(activeConditionId).quantityLimitPerWallet, 32); + + vm.warp(40); + assertEq(openEdition.getActiveClaimConditionId(), 2); + } +} diff --git a/src/test/OpenEditionERC721FlatFee.t.sol b/src/test/OpenEditionERC721FlatFee.t.sol new file mode 100644 index 000000000..38de7326c --- /dev/null +++ b/src/test/OpenEditionERC721FlatFee.t.sol @@ -0,0 +1,792 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { ERC721AUpgradeable, OpenEditionERC721FlatFee, ISharedMetadata } from "contracts/prebuilts/open-edition/OpenEditionERC721FlatFee.sol"; +import { Drop } from "contracts/extension/Drop.sol"; +import { LazyMint } from "contracts/extension/LazyMint.sol"; +import { OpenEditionERC721FlatFee } from "contracts/prebuilts/open-edition/OpenEditionERC721FlatFee.sol"; +import { NFTMetadataRenderer } from "contracts/lib/NFTMetadataRenderer.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "erc721a-upgradeable/contracts/IERC721AUpgradeable.sol"; +import "./utils/BaseTest.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; + +contract OpenEditionERC721FlatFeeTest is BaseTest { + using Strings for uint256; + using Strings for address; + + event SharedMetadataUpdated(string name, string description, string imageURI, string animationURI); + + OpenEditionERC721FlatFee public openEdition; + ISharedMetadata.SharedMetadataInfo public sharedMetadata; + + bytes private emptyEncodedBytes = abi.encode("", ""); + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + address openEditionImpl = address(new OpenEditionERC721FlatFee()); + + vm.prank(deployer); + openEdition = OpenEditionERC721FlatFee( + address( + new TWProxy( + openEditionImpl, + abi.encodeCall( + OpenEditionERC721FlatFee.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ); + + sharedMetadata = ISharedMetadata.SharedMetadataInfo({ + name: "Test", + description: "Test", + imageURI: "https://test.com", + animationURI: "https://test.com" + }); + + erc20.mint(deployer, 1_000 ether); + vm.deal(deployer, 1_000 ether); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: misc. + //////////////////////////////////////////////////////////////*/ + + /** + * note: Tests whether contract reverts when a non-holder renounces a role. + */ + function test_revert_nonHolder_renounceRole() public { + address caller = address(0x123); + bytes32 role = keccak256("MINTER_ROLE"); + + vm.prank(caller); + vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsUnauthorizedAccount.selector, caller, role)); + + openEdition.renounceRole(role, caller); + } + + /** + * note: Tests whether contract reverts when a role admin revokes a role for a non-holder. + */ + function test_revert_revokeRoleForNonHolder() public { + address target = address(0x123); + bytes32 role = keccak256("MINTER_ROLE"); + + vm.prank(deployer); + vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsUnauthorizedAccount.selector, target, role)); + + openEdition.revokeRole(role, target); + } + + /** + * @dev Tests whether contract reverts when a role is granted to an existent role holder. + */ + function test_revert_grant_role_to_account_with_role() public { + bytes32 role = keccak256("ABC_ROLE"); + address receiver = getActor(0); + + vm.startPrank(deployer); + + openEdition.grantRole(role, receiver); + + vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsAlreadyGranted.selector, receiver, role)); + openEdition.grantRole(role, receiver); + + vm.stopPrank(); + } + + /** + * @dev Tests contract state for Transfer role. + */ + function test_state_grant_transferRole() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + + // check if admin and address(0) have transfer role in the beginning + bool checkAddressZero = openEdition.hasRole(role, address(0)); + bool checkAdmin = openEdition.hasRole(role, deployer); + assertTrue(checkAddressZero); + assertTrue(checkAdmin); + + // check if transfer role can be granted to a non-holder + address receiver = getActor(0); + vm.startPrank(deployer); + openEdition.grantRole(role, receiver); + + // expect revert when granting to a holder + vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsAlreadyGranted.selector, receiver, role)); + openEdition.grantRole(role, receiver); + + // check if receiver has transfer role + bool checkReceiver = openEdition.hasRole(role, receiver); + assertTrue(checkReceiver); + + // check if role is correctly revoked + openEdition.revokeRole(role, receiver); + checkReceiver = openEdition.hasRole(role, receiver); + assertFalse(checkReceiver); + openEdition.revokeRole(role, address(0)); + checkAddressZero = openEdition.hasRole(role, address(0)); + assertFalse(checkAddressZero); + + vm.stopPrank(); + } + + /** + * note: Testing transfer of tokens when transfer-role is restricted + */ + function test_claim_transferRole() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + OpenEditionERC721FlatFee.AllowlistProof memory alp; + alp.proof = proofs; + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + openEdition.claim(receiver, 1, address(0), 0, alp, ""); + + // revoke transfer role from address(0) + vm.prank(deployer); + openEdition.revokeRole(keccak256("TRANSFER_ROLE"), address(0)); + vm.startPrank(receiver); + vm.expectRevert(bytes("!T")); + openEdition.transferFrom(receiver, address(123), 1); + } + + function test_claimCondition_with_startTimestamp() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + OpenEditionERC721FlatFee.AllowlistProof memory alp; + alp.proof = proofs; + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](1); + conditions[0].startTimestamp = 100; + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + vm.warp(99); + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(abi.encodeWithSelector(Drop.DropNoActiveCondition.selector)); + openEdition.claim(receiver, 1, address(0), 0, alp, ""); + + vm.warp(100); + vm.prank(getActor(4), getActor(4)); + openEdition.claim(receiver, 1, address(0), 0, alp, ""); + } + + /*/////////////////////////////////////////////////////////////// + Set Shared Metadata Tests + //////////////////////////////////////////////////////////////*/ + + /* + * note: Testing state changes; set shared metadata for tokens. + */ + function test_state_sharedMetadata() public { + // SET METADATA + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + + // CLAIM 1 TOKEN + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "0"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + OpenEditionERC721FlatFee.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 0; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + openEdition.claim(receiver, 100, address(erc20), 0, alp, ""); + + string memory uri = openEdition.tokenURI(1); + assertEq( + uri, + NFTMetadataRenderer.createMetadataEdition({ + name: sharedMetadata.name, + description: sharedMetadata.description, + imageURI: sharedMetadata.imageURI, + animationURI: sharedMetadata.animationURI, + tokenOfEdition: 1 + }) + ); + } + + /** + * note: Testing revert condition; an address without MINTER_ROLE calls setSharedMetadata function. + */ + function test_revert_setSharedMetadata_MINTER_ROLE() public { + vm.expectRevert(); + openEdition.setSharedMetadata(sharedMetadata); + } + + /** + * note: Testing event emission; shared metadata set. + */ + function test_event_setSharedMetadata_SharedMetadataUpdated() public { + vm.startPrank(deployer); + + vm.expectEmit(true, false, false, true); + emit SharedMetadataUpdated( + sharedMetadata.name, + sharedMetadata.description, + sharedMetadata.imageURI, + sharedMetadata.animationURI + ); + openEdition.setSharedMetadata(sharedMetadata); + + vm.stopPrank(); + } + + /*/////////////////////////////////////////////////////////////// + Claim Tests + //////////////////////////////////////////////////////////////*/ + + /** + * note: Testing revert condition; exceed max claimable supply. + */ + function test_revert_claimCondition_exceedMaxClaimableSupply() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + OpenEditionERC721FlatFee.AllowlistProof memory alp; + alp.proof = proofs; + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 200; + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + openEdition.claim(receiver, 100, address(0), 0, alp, ""); + + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedMaxSupply.selector, conditions[0].maxClaimableSupply, 101) + ); + vm.prank(getActor(6), getActor(6)); + openEdition.claim(receiver, 1, address(0), 0, alp, ""); + } + + /** + * note: Testing quantity limit restriction when no allowlist present. + */ + function test_fuzz_claim_noAllowlist(uint256 x) public { + vm.assume(x != 0); + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + OpenEditionERC721FlatFee.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = x; + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, conditions[0].quantityLimitPerWallet, 0) + ); + openEdition.claim(receiver, 0, address(0), 0, alp, ""); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, conditions[0].quantityLimitPerWallet, 101) + ); + openEdition.claim(receiver, 101, address(0), 0, alp, ""); + + vm.prank(deployer); + openEdition.setClaimConditions(conditions, true); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, conditions[0].quantityLimitPerWallet, 101) + ); + openEdition.claim(receiver, 101, address(0), 0, alp, ""); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price set to 0 + */ + function test_state_claim_allowlisted_SetQuantityZeroPrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "0"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + OpenEditionERC721FlatFee.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 0; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + openEdition.claim(receiver, 100, address(erc20), 0, alp, ""); // claims for free, because allowlist price is 0 + assertEq(openEdition.getSupplyClaimedByWallet(openEdition.getActiveClaimConditionId(), receiver), 100); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price set to non-zero value + */ + function test_state_claim_allowlisted_SetQuantityPrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "5"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + OpenEditionERC721FlatFee.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 5; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + vm.prank(receiver, receiver); + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimInvalidTokenPrice.selector, address(erc20), 0, address(erc20), 5) + ); + openEdition.claim(receiver, 100, address(erc20), 0, alp, ""); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(openEdition), 10000); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + openEdition.claim(receiver, 100, address(erc20), 5, alp, ""); + assertEq(openEdition.getSupplyClaimedByWallet(openEdition.getActiveClaimConditionId(), receiver), 100); + assertEq(erc20.balanceOf(receiver), 10000 - 500); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price not set; should default to general price and currency + */ + function test_state_claim_allowlisted_SetQuantityDefaultPrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = Strings.toString(type(uint256).max); // this implies that general price is applicable + inputs[4] = "0x0000000000000000000000000000000000000000"; + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + OpenEditionERC721FlatFee.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = type(uint256).max; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(openEdition), 10000); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + openEdition.claim(receiver, 100, address(erc20), 10, alp, ""); + assertEq(openEdition.getSupplyClaimedByWallet(openEdition.getActiveClaimConditionId(), receiver), 100); + assertEq(erc20.balanceOf(receiver), 10000 - 1000); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to 0 => should default to general limit + * - allowlist price set to some value different than general price + */ + function test_state_claim_allowlisted_DefaultQuantitySomePrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "0"; // this implies that general limit is applicable + inputs[3] = "5"; + inputs[4] = "0x0000000000000000000000000000000000000000"; // general currency will be applicable + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + OpenEditionERC721FlatFee.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 0; + alp.pricePerToken = 5; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(openEdition), 10000); + + vm.prank(receiver, receiver); + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, conditions[0].quantityLimitPerWallet, 100) + ); + openEdition.claim(receiver, 100, address(erc20), 5, alp, ""); // trying to claim more than general limit + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + openEdition.claim(receiver, 10, address(erc20), 5, alp, ""); + assertEq(openEdition.getSupplyClaimedByWallet(openEdition.getActiveClaimConditionId(), receiver), 10); + assertEq(erc20.balanceOf(receiver), 10000 - 50); + } + + function test_fuzz_claim_merkleProof(uint256 x) public { + vm.assume(x > 10 && x < 500); + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = Strings.toString(x); + inputs[3] = "0"; + inputs[4] = "0x0000000000000000000000000000000000000000"; + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + OpenEditionERC721FlatFee.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = x; + alp.pricePerToken = 0; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + + // bytes32[] memory proofs = new bytes32[](0); + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](1); + conditions[0].maxClaimableSupply = x; + conditions[0].quantityLimitPerWallet = 1; + conditions[0].merkleRoot = root; + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + openEdition.claim(receiver, x - 5, address(0), 0, alp, ""); + assertEq(openEdition.getSupplyClaimedByWallet(openEdition.getActiveClaimConditionId(), receiver), x - 5); + + vm.prank(receiver, receiver); + vm.expectRevert(abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, x, x + 1)); + openEdition.claim(receiver, 6, address(0), 0, alp, ""); + + vm.prank(receiver, receiver); + openEdition.claim(receiver, 5, address(0), 0, alp, ""); + assertEq(openEdition.getSupplyClaimedByWallet(openEdition.getActiveClaimConditionId(), receiver), x); + + vm.prank(receiver, receiver); + vm.expectRevert(abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, x, x + 5)); + openEdition.claim(receiver, 5, address(0), 0, alp, ""); // quantity limit already claimed + } + + /** + * note: Testing state changes; reset eligibility of claim conditions and claiming again for same condition id. + */ + function test_state_claimCondition_resetEligibility() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + OpenEditionERC721FlatFee.AllowlistProof memory alp; + alp.proof = proofs; + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + openEdition.claim(receiver, 100, address(0), 0, alp, ""); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, conditions[0].quantityLimitPerWallet, 200) + ); + openEdition.claim(receiver, 100, address(0), 0, alp, ""); + + vm.prank(deployer); + openEdition.setClaimConditions(conditions, true); + + vm.prank(getActor(5), getActor(5)); + openEdition.claim(receiver, 100, address(0), 0, alp, ""); + } + + /*/////////////////////////////////////////////////////////////// + setClaimConditions + //////////////////////////////////////////////////////////////*/ + + function test_claimCondition_startIdAndCount() public { + vm.startPrank(deployer); + + uint256 currentStartId = 0; + uint256 count = 0; + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](2); + conditions[0].startTimestamp = 0; + conditions[0].maxClaimableSupply = 10; + conditions[1].startTimestamp = 1; + conditions[1].maxClaimableSupply = 10; + + openEdition.setClaimConditions(conditions, false); + (currentStartId, count) = openEdition.claimCondition(); + assertEq(currentStartId, 0); + assertEq(count, 2); + + openEdition.setClaimConditions(conditions, false); + (currentStartId, count) = openEdition.claimCondition(); + assertEq(currentStartId, 0); + assertEq(count, 2); + + openEdition.setClaimConditions(conditions, true); + (currentStartId, count) = openEdition.claimCondition(); + assertEq(currentStartId, 2); + assertEq(count, 2); + + openEdition.setClaimConditions(conditions, true); + (currentStartId, count) = openEdition.claimCondition(); + assertEq(currentStartId, 4); + assertEq(count, 2); + } + + function test_claimCondition_startPhase() public { + vm.startPrank(deployer); + + uint256 activeConditionId = 0; + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](3); + conditions[0].startTimestamp = 10; + conditions[0].maxClaimableSupply = 11; + conditions[0].quantityLimitPerWallet = 12; + conditions[1].startTimestamp = 20; + conditions[1].maxClaimableSupply = 21; + conditions[1].quantityLimitPerWallet = 22; + conditions[2].startTimestamp = 30; + conditions[2].maxClaimableSupply = 31; + conditions[2].quantityLimitPerWallet = 32; + openEdition.setClaimConditions(conditions, false); + + vm.expectRevert(abi.encodeWithSelector(Drop.DropNoActiveCondition.selector)); + openEdition.getActiveClaimConditionId(); + + vm.warp(10); + activeConditionId = openEdition.getActiveClaimConditionId(); + assertEq(activeConditionId, 0); + assertEq(openEdition.getClaimConditionById(activeConditionId).startTimestamp, 10); + assertEq(openEdition.getClaimConditionById(activeConditionId).maxClaimableSupply, 11); + assertEq(openEdition.getClaimConditionById(activeConditionId).quantityLimitPerWallet, 12); + + vm.warp(20); + activeConditionId = openEdition.getActiveClaimConditionId(); + assertEq(activeConditionId, 1); + assertEq(openEdition.getClaimConditionById(activeConditionId).startTimestamp, 20); + assertEq(openEdition.getClaimConditionById(activeConditionId).maxClaimableSupply, 21); + assertEq(openEdition.getClaimConditionById(activeConditionId).quantityLimitPerWallet, 22); + + vm.warp(30); + activeConditionId = openEdition.getActiveClaimConditionId(); + assertEq(activeConditionId, 2); + assertEq(openEdition.getClaimConditionById(activeConditionId).startTimestamp, 30); + assertEq(openEdition.getClaimConditionById(activeConditionId).maxClaimableSupply, 31); + assertEq(openEdition.getClaimConditionById(activeConditionId).quantityLimitPerWallet, 32); + + vm.warp(40); + assertEq(openEdition.getActiveClaimConditionId(), 2); + } +} diff --git a/src/test/Pack.t.sol b/src/test/Pack.t.sol deleted file mode 100644 index 171e588b4..000000000 --- a/src/test/Pack.t.sol +++ /dev/null @@ -1,879 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -import { Pack } from "contracts/pack/Pack.sol"; -import { IPack } from "contracts/interfaces/IPack.sol"; -import { ITokenBundle } from "contracts/extension/interface/ITokenBundle.sol"; - -// Test imports -import { MockERC20 } from "./mocks/MockERC20.sol"; -import { Wallet } from "./utils/Wallet.sol"; -import "./utils/BaseTest.sol"; - -contract PackTest is BaseTest { - /// @notice Emitted when a set of packs is created. - event PackCreated( - uint256 indexed packId, - address indexed packCreator, - address recipient, - uint256 totalPacksCreated - ); - - /// @notice Emitted when a pack is opened. - event PackOpened( - uint256 indexed packId, - address indexed opener, - uint256 numOfPacksOpened, - ITokenBundle.Token[] rewardUnitsDistributed - ); - - Pack internal pack; - - Wallet internal tokenOwner; - string internal packUri; - ITokenBundle.Token[] internal packContents; - uint256[] internal numOfRewardUnits; - - function setUp() public override { - super.setUp(); - - pack = Pack(payable(getContract("Pack"))); - - tokenOwner = getWallet(); - packUri = "ipfs://"; - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 0, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc1155), - tokenType: ITokenBundle.TokenType.ERC1155, - tokenId: 0, - totalAmount: 100 - }) - ); - numOfRewardUnits.push(20); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 1000 ether - }) - ); - numOfRewardUnits.push(50); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 1, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 2, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 1000 ether - }) - ); - numOfRewardUnits.push(100); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 3, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 4, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 5, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc1155), - tokenType: ITokenBundle.TokenType.ERC1155, - tokenId: 1, - totalAmount: 500 - }) - ); - numOfRewardUnits.push(50); - - erc20.mint(address(tokenOwner), 2000 ether); - erc721.mint(address(tokenOwner), 6); - erc1155.mint(address(tokenOwner), 0, 100); - erc1155.mint(address(tokenOwner), 1, 500); - - tokenOwner.setAllowanceERC20(address(erc20), address(pack), type(uint256).max); - tokenOwner.setApprovalForAllERC721(address(erc721), address(pack), true); - tokenOwner.setApprovalForAllERC1155(address(erc1155), address(pack), true); - - vm.prank(deployer); - pack.grantRole(keccak256("MINTER_ROLE"), address(tokenOwner)); - } - - /*/////////////////////////////////////////////////////////////// - Unit tests: `createPack` - //////////////////////////////////////////////////////////////*/ - - /** - * note: Testing state changes; token owner calls `createPack` to pack owned tokens. - */ - function test_state_createPack() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(1); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - assertEq(packId + 1, pack.nextTokenIdToMint()); - - (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); - assertEq(packed.length, packContents.length); - for (uint256 i = 0; i < packed.length; i += 1) { - assertEq(packed[i].assetContract, packContents[i].assetContract); - assertEq(uint256(packed[i].tokenType), uint256(packContents[i].tokenType)); - assertEq(packed[i].tokenId, packContents[i].tokenId); - assertEq(packed[i].totalAmount, packContents[i].totalAmount); - } - - assertEq(packUri, pack.uri(packId)); - } - - /* - * note: Testing state changes; token owner calls `createPack` to pack native tokens. - */ - function test_state_createPack_nativeTokens() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(0x123); - - vm.deal(address(tokenOwner), 100 ether); - packContents.push( - ITokenBundle.Token({ - assetContract: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 20 ether - }) - ); - numOfRewardUnits.push(20); - - vm.prank(address(tokenOwner)); - pack.createPack{ value: 20 ether }(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - assertEq(packId + 1, pack.nextTokenIdToMint()); - - (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); - assertEq(packed.length, packContents.length); - for (uint256 i = 0; i < packed.length; i += 1) { - assertEq(packed[i].assetContract, packContents[i].assetContract); - assertEq(uint256(packed[i].tokenType), uint256(packContents[i].tokenType)); - assertEq(packed[i].tokenId, packContents[i].tokenId); - assertEq(packed[i].totalAmount, packContents[i].totalAmount); - } - - assertEq(packUri, pack.uri(packId)); - } - - /** - * note: Testing state changes; token owner calls `createPack` to pack owned tokens. - * Only assets with ASSET_ROLE can be packed. - */ - function test_state_createPack_withAssetRoleRestriction() public { - vm.startPrank(deployer); - pack.revokeRole(keccak256("ASSET_ROLE"), address(0)); - for (uint256 i = 0; i < packContents.length; i += 1) { - if (!pack.hasRole(keccak256("ASSET_ROLE"), packContents[i].assetContract)) { - pack.grantRole(keccak256("ASSET_ROLE"), packContents[i].assetContract); - } - } - vm.stopPrank(); - - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(0x123); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - assertEq(packId + 1, pack.nextTokenIdToMint()); - - (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); - assertEq(packed.length, packContents.length); - for (uint256 i = 0; i < packed.length; i += 1) { - assertEq(packed[i].assetContract, packContents[i].assetContract); - assertEq(uint256(packed[i].tokenType), uint256(packContents[i].tokenType)); - assertEq(packed[i].tokenId, packContents[i].tokenId); - assertEq(packed[i].totalAmount, packContents[i].totalAmount); - } - - assertEq(packUri, pack.uri(packId)); - } - - /** - * note: Testing event emission; token owner calls `createPack` to pack owned tokens. - */ - function test_event_createPack_PackCreated() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectEmit(true, true, true, true); - emit PackCreated(packId, address(tokenOwner), recipient, 226); - - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - vm.stopPrank(); - } - - /** - * note: Testing token balances; token owner calls `createPack` to pack owned tokens. - */ - function test_balances_createPack() public { - // ERC20 balance - assertEq(erc20.balanceOf(address(tokenOwner)), 2000 ether); - assertEq(erc20.balanceOf(address(pack)), 0); - - // ERC721 balance - assertEq(erc721.ownerOf(0), address(tokenOwner)); - assertEq(erc721.ownerOf(1), address(tokenOwner)); - assertEq(erc721.ownerOf(2), address(tokenOwner)); - assertEq(erc721.ownerOf(3), address(tokenOwner)); - assertEq(erc721.ownerOf(4), address(tokenOwner)); - assertEq(erc721.ownerOf(5), address(tokenOwner)); - - // ERC1155 balance - assertEq(erc1155.balanceOf(address(tokenOwner), 0), 100); - assertEq(erc1155.balanceOf(address(pack), 0), 0); - - assertEq(erc1155.balanceOf(address(tokenOwner), 1), 500); - assertEq(erc1155.balanceOf(address(pack), 1), 0); - - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(1); - - vm.prank(address(tokenOwner)); - (, uint256 totalSupply) = pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - // ERC20 balance - assertEq(erc20.balanceOf(address(tokenOwner)), 0); - assertEq(erc20.balanceOf(address(pack)), 2000 ether); - - // ERC721 balance - assertEq(erc721.ownerOf(0), address(pack)); - assertEq(erc721.ownerOf(1), address(pack)); - assertEq(erc721.ownerOf(2), address(pack)); - assertEq(erc721.ownerOf(3), address(pack)); - assertEq(erc721.ownerOf(4), address(pack)); - assertEq(erc721.ownerOf(5), address(pack)); - - // ERC1155 balance - assertEq(erc1155.balanceOf(address(tokenOwner), 0), 0); - assertEq(erc1155.balanceOf(address(pack), 0), 100); - - assertEq(erc1155.balanceOf(address(tokenOwner), 1), 0); - assertEq(erc1155.balanceOf(address(pack), 1), 500); - - // Pack wrapped token balance - assertEq(pack.balanceOf(address(recipient), packId), totalSupply); - } - - /** - * note: Testing revert condition; token owner calls `createPack` to pack owned tokens. - * Only assets with ASSET_ROLE can be packed, but assets being packed don't have that role. - */ - function test_revert_createPack_access_ASSET_ROLE() public { - vm.prank(deployer); - pack.revokeRole(keccak256("ASSET_ROLE"), address(0)); - - address recipient = address(0x123); - - string memory errorMsg = string( - abi.encodePacked( - "Permissions: account ", - Strings.toHexString(uint160(packContents[0].assetContract), 20), - " is missing role ", - Strings.toHexString(uint256(keccak256("ASSET_ROLE")), 32) - ) - ); - - vm.prank(address(tokenOwner)); - vm.expectRevert(bytes(errorMsg)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` to pack owned tokens, without MINTER_ROLE. - */ - function test_revert_createPack_access_MINTER_ROLE() public { - vm.prank(address(tokenOwner)); - pack.renounceRole(keccak256("MINTER_ROLE"), address(tokenOwner)); - - address recipient = address(0x123); - - string memory errorMsg = string( - abi.encodePacked( - "Permissions: account ", - Strings.toHexString(uint160(address(tokenOwner)), 20), - " is missing role ", - Strings.toHexString(uint256(keccak256("MINTER_ROLE")), 32) - ) - ); - - vm.prank(address(tokenOwner)); - vm.expectRevert(bytes(errorMsg)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` with insufficient value when packing native tokens. - */ - function test_revert_createPack_nativeTokens_insufficientValue() public { - address recipient = address(0x123); - - vm.deal(address(tokenOwner), 100 ether); - - packContents.push( - ITokenBundle.Token({ - assetContract: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 20 ether - }) - ); - numOfRewardUnits.push(1 ether); - - vm.prank(address(tokenOwner)); - vm.expectRevert("msg.value != amount"); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` to pack un-owned ERC20 tokens. - */ - function test_revert_createPack_notOwner_ERC20() public { - tokenOwner.transferERC20(address(erc20), address(0x12), 1000 ether); - - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("ERC20: transfer amount exceeds balance"); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` to pack un-owned ERC721 tokens. - */ - function test_revert_createPack_notOwner_ERC721() public { - tokenOwner.transferERC721(address(erc721), address(0x12), 0); - - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("ERC721: transfer caller is not owner nor approved"); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` to pack un-owned ERC1155 tokens. - */ - function test_revert_createPack_notOwner_ERC1155() public { - tokenOwner.transferERC1155(address(erc1155), address(0x12), 0, 100, ""); - - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("ERC1155: insufficient balance for transfer"); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` to pack un-approved ERC20 tokens. - */ - function test_revert_createPack_notApprovedTransfer_ERC20() public { - tokenOwner.setAllowanceERC20(address(erc20), address(pack), 0); - - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("ERC20: insufficient allowance"); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` to pack un-approved ERC721 tokens. - */ - function test_revert_createPack_notApprovedTransfer_ERC721() public { - tokenOwner.setApprovalForAllERC721(address(erc721), address(pack), false); - - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("ERC721: transfer caller is not owner nor approved"); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` to pack un-approved ERC1155 tokens. - */ - function test_revert_createPack_notApprovedTransfer_ERC1155() public { - tokenOwner.setApprovalForAllERC1155(address(erc1155), address(pack), false); - - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("ERC1155: caller is not owner nor approved"); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` with no tokens to pack. - */ - function test_revert_createPack_noTokensToPack() public { - ITokenBundle.Token[] memory emptyContent; - uint256[] memory rewardUnits; - - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("nothing to pack"); - pack.createPack(emptyContent, rewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` with unequal length of contents and rewardUnits. - */ - function test_revert_createPack_invalidRewardUnits() public { - uint256[] memory rewardUnits; - - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("invalid reward units"); - pack.createPack(packContents, rewardUnits, packUri, 0, 1, recipient); - } - - /*/////////////////////////////////////////////////////////////// - Unit tests: `openPack` - //////////////////////////////////////////////////////////////*/ - - /** - * note: Testing state changes; pack owner calls `openPack` to redeem underlying rewards. - */ - function test_state_openPack() public { - vm.warp(1000); - uint256 packId = pack.nextTokenIdToMint(); - uint256 packsToOpen = 3; - address recipient = address(1); - - vm.prank(address(tokenOwner)); - (, uint256 totalSupply) = pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); - - vm.prank(recipient, recipient); - ITokenBundle.Token[] memory rewardUnits = pack.openPack(packId, packsToOpen); - console2.log("total reward units: ", rewardUnits.length); - - for (uint256 i = 0; i < rewardUnits.length; i++) { - console2.log("----- reward unit number: ", i, "------"); - console2.log("asset contract: ", rewardUnits[i].assetContract); - console2.log("token type: ", uint256(rewardUnits[i].tokenType)); - console2.log("tokenId: ", rewardUnits[i].tokenId); - if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC20) { - console2.log("total amount: ", rewardUnits[i].totalAmount / 1 ether, "ether"); - } else { - console2.log("total amount: ", rewardUnits[i].totalAmount); - } - console2.log(""); - } - - assertEq(packUri, pack.uri(packId)); - assertEq(pack.totalSupply(packId), totalSupply - packsToOpen); - - (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); - assertEq(packed.length, packContents.length); - } - - /** - * note: Testing event emission; pack owner calls `openPack` to open owned packs. - */ - function test_event_openPack_PackOpened() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(0x123); - - ITokenBundle.Token[] memory emptyRewardUnitsForTestingEvent; - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - vm.expectEmit(true, true, false, false); - emit PackOpened(packId, recipient, 1, emptyRewardUnitsForTestingEvent); - - vm.prank(recipient, recipient); - pack.openPack(packId, 1); - } - - function test_balances_openPack() public { - uint256 packId = pack.nextTokenIdToMint(); - uint256 packsToOpen = 3; - address recipient = address(1); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); - - // ERC20 balance - assertEq(erc20.balanceOf(address(recipient)), 0); - assertEq(erc20.balanceOf(address(pack)), 2000 ether); - - // ERC721 balance - assertEq(erc721.ownerOf(0), address(pack)); - assertEq(erc721.ownerOf(1), address(pack)); - assertEq(erc721.ownerOf(2), address(pack)); - assertEq(erc721.ownerOf(3), address(pack)); - assertEq(erc721.ownerOf(4), address(pack)); - assertEq(erc721.ownerOf(5), address(pack)); - - // ERC1155 balance - assertEq(erc1155.balanceOf(address(recipient), 0), 0); - assertEq(erc1155.balanceOf(address(pack), 0), 100); - - assertEq(erc1155.balanceOf(address(recipient), 1), 0); - assertEq(erc1155.balanceOf(address(pack), 1), 500); - - vm.prank(recipient, recipient); - ITokenBundle.Token[] memory rewardUnits = pack.openPack(packId, packsToOpen); - console2.log("total reward units: ", rewardUnits.length); - - uint256 erc20Amount; - uint256[] memory erc1155Amounts = new uint256[](2); - uint256 erc721Amount; - - for (uint256 i = 0; i < rewardUnits.length; i++) { - console2.log("----- reward unit number: ", i, "------"); - console2.log("asset contract: ", rewardUnits[i].assetContract); - console2.log("token type: ", uint256(rewardUnits[i].tokenType)); - console2.log("tokenId: ", rewardUnits[i].tokenId); - if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC20) { - console2.log("total amount: ", rewardUnits[i].totalAmount / 1 ether, "ether"); - console.log("balance of recipient: ", erc20.balanceOf(address(recipient)) / 1 ether, "ether"); - erc20Amount += rewardUnits[i].totalAmount; - } else if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC1155) { - console2.log("total amount: ", rewardUnits[i].totalAmount); - console.log("balance of recipient: ", erc1155.balanceOf(address(recipient), rewardUnits[i].tokenId)); - erc1155Amounts[rewardUnits[i].tokenId] += rewardUnits[i].totalAmount; - } else if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC721) { - console2.log("total amount: ", rewardUnits[i].totalAmount); - console.log("balance of recipient: ", erc721.balanceOf(address(recipient))); - erc721Amount += rewardUnits[i].totalAmount; - } - console2.log(""); - } - - assertEq(erc20.balanceOf(address(recipient)), erc20Amount); - assertEq(erc721.balanceOf(address(recipient)), erc721Amount); - - for (uint256 i = 0; i < erc1155Amounts.length; i += 1) { - assertEq(erc1155.balanceOf(address(recipient), i), erc1155Amounts[i]); - } - } - - /** - * note: Testing revert condition; caller of `openPack` is not EOA. - */ - function test_revert_openPack_notEOA() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(0x123); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - vm.startPrank(recipient, address(27)); - vm.expectRevert("opener must be eoa"); - pack.openPack(packId, 1); - } - - /** - * note: Testing revert condition; pack owner calls `openPack` to open more than owned packs. - */ - function test_revert_openPack_openMoreThanOwned() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(0x123); - - vm.prank(address(tokenOwner)); - (, uint256 totalSupply) = pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - vm.startPrank(recipient, recipient); - vm.expectRevert("opening more than owned"); - pack.openPack(packId, totalSupply + 1); - } - - /** - * note: Testing revert condition; pack owner calls `openPack` before start timestamp. - */ - function test_revert_openPack_openBeforeStart() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(0x123); - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 1000, 1, recipient); - - vm.startPrank(recipient, recipient); - vm.expectRevert("cannot open yet"); - pack.openPack(packId, 1); - } - - /** - * note: Testing revert condition; pack owner calls `openPack` with pack-id non-existent or not owned. - */ - function test_revert_openPack_invalidPackId() public { - address recipient = address(0x123); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - vm.startPrank(recipient, recipient); - vm.expectRevert("opening more than owned"); - pack.openPack(2, 1); - } - - /*/////////////////////////////////////////////////////////////// - Fuzz testing - //////////////////////////////////////////////////////////////*/ - - uint256 internal constant MAX_TOKENS = 2000; - - function getTokensToPack(uint256 len) - internal - returns (ITokenBundle.Token[] memory tokensToPack, uint256[] memory rewardUnits) - { - vm.assume(len < MAX_TOKENS); - tokensToPack = new ITokenBundle.Token[](len); - rewardUnits = new uint256[](len); - - for (uint256 i = 0; i < len; i += 1) { - uint256 random = uint256(keccak256(abi.encodePacked(len + i))) % MAX_TOKENS; - uint256 selector = random % 4; - - if (selector == 0) { - tokensToPack[i] = ITokenBundle.Token({ - assetContract: address(erc20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: (random + 1) * 10 ether - }); - rewardUnits[i] = random + 1; - - erc20.mint(address(tokenOwner), tokensToPack[i].totalAmount); - } else if (selector == 1) { - uint256 tokenId = erc721.nextTokenIdToMint(); - - tokensToPack[i] = ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: tokenId, - totalAmount: 1 - }); - rewardUnits[i] = 1; - - erc721.mint(address(tokenOwner), 1); - } else if (selector == 2) { - tokensToPack[i] = ITokenBundle.Token({ - assetContract: address(erc1155), - tokenType: ITokenBundle.TokenType.ERC1155, - tokenId: random, - totalAmount: (random + 1) * 10 - }); - rewardUnits[i] = random + 1; - - erc1155.mint(address(tokenOwner), tokensToPack[i].tokenId, tokensToPack[i].totalAmount); - } else if (selector == 3) { - tokensToPack[i] = ITokenBundle.Token({ - assetContract: address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 5 ether - }); - rewardUnits[i] = 5; - } - } - } - - function checkBalances(ITokenBundle.Token[] memory rewardUnits, address recipient) - internal - view - returns ( - uint256 nativeTokenAmount, - uint256 erc20Amount, - uint256[] memory erc1155Amounts, - uint256 erc721Amount - ) - { - erc1155Amounts = new uint256[](MAX_TOKENS); - - for (uint256 i = 0; i < rewardUnits.length; i++) { - console2.log("----- reward unit number: ", i, "------"); - console2.log("asset contract: ", rewardUnits[i].assetContract); - console2.log("token type: ", uint256(rewardUnits[i].tokenType)); - console2.log("tokenId: ", rewardUnits[i].tokenId); - if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC20) { - if (rewardUnits[i].assetContract == address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) { - console2.log("total amount: ", rewardUnits[i].totalAmount / 1 ether, "ether"); - console.log("balance of recipient: ", address(recipient).balance); - nativeTokenAmount += rewardUnits[i].totalAmount; - } else { - console2.log("total amount: ", rewardUnits[i].totalAmount / 1 ether, "ether"); - console.log("balance of recipient: ", erc20.balanceOf(address(recipient)) / 1 ether, "ether"); - erc20Amount += rewardUnits[i].totalAmount; - } - } else if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC1155) { - console2.log("total amount: ", rewardUnits[i].totalAmount); - console.log("balance of recipient: ", erc1155.balanceOf(address(recipient), rewardUnits[i].tokenId)); - erc1155Amounts[rewardUnits[i].tokenId] += rewardUnits[i].totalAmount; - } else if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC721) { - console2.log("total amount: ", rewardUnits[i].totalAmount); - console.log("balance of recipient: ", erc721.balanceOf(address(recipient))); - erc721Amount += rewardUnits[i].totalAmount; - } - console2.log(""); - } - } - - function test_fuzz_state_createPack(uint256 x, uint128 y) public { - (ITokenBundle.Token[] memory tokensToPack, uint256[] memory rewardUnits) = getTokensToPack(x); - if (tokensToPack.length == 0) { - return; - } - - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(0x123); - uint256 totalRewardUnits; - uint256 nativeTokenPacked; - - for (uint256 i = 0; i < tokensToPack.length; i += 1) { - totalRewardUnits += rewardUnits[i]; - if (tokensToPack[i].assetContract == address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) { - nativeTokenPacked += tokensToPack[i].totalAmount; - } - } - vm.deal(address(tokenOwner), nativeTokenPacked); - vm.assume(y > 0 && totalRewardUnits % y == 0); - - vm.prank(address(tokenOwner)); - (, uint256 totalSupply) = pack.createPack{ value: nativeTokenPacked }( - tokensToPack, - rewardUnits, - packUri, - 0, - y, - recipient - ); - console2.log("total supply: ", totalSupply); - console2.log("total reward units: ", totalRewardUnits); - - assertEq(packId + 1, pack.nextTokenIdToMint()); - - (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); - assertEq(packed.length, tokensToPack.length); - for (uint256 i = 0; i < packed.length; i += 1) { - assertEq(packed[i].assetContract, tokensToPack[i].assetContract); - assertEq(uint256(packed[i].tokenType), uint256(tokensToPack[i].tokenType)); - assertEq(packed[i].tokenId, tokensToPack[i].tokenId); - assertEq(packed[i].totalAmount, tokensToPack[i].totalAmount); - } - - assertEq(packUri, pack.uri(packId)); - } - - /*/////////////////////////////////////////////////////////////// - Scenario/Exploit tests - //////////////////////////////////////////////////////////////*/ - /** - * note: Testing revert condition; token owner calls `createPack` to pack owned tokens. - */ - function test_revert_createPack_reentrancy() public { - MaliciousERC20 malERC20 = new MaliciousERC20(payable(address(pack))); - ITokenBundle.Token[] memory content = new ITokenBundle.Token[](1); - uint256[] memory rewards = new uint256[](1); - - malERC20.mint(address(tokenOwner), 10 ether); - content[0] = ITokenBundle.Token({ - assetContract: address(malERC20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 10 ether - }); - rewards[0] = 10; - - tokenOwner.setAllowanceERC20(address(malERC20), address(pack), 10 ether); - - address recipient = address(0x123); - - vm.prank(address(deployer)); - pack.grantRole(keccak256("MINTER_ROLE"), address(malERC20)); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("ReentrancyGuard: reentrant call"); - pack.createPack(content, rewards, packUri, 0, 1, recipient); - } -} - -contract MaliciousERC20 is MockERC20, ITokenBundle { - Pack public pack; - - constructor(address payable _pack) { - pack = Pack(_pack); - } - - function transferFrom( - address from, - address to, - uint256 amount - ) public override returns (bool) { - ITokenBundle.Token[] memory content = new ITokenBundle.Token[](1); - uint256[] memory rewards = new uint256[](1); - - address recipient = address(0x123); - pack.createPack(content, rewards, "", 0, 1, recipient); - return super.transferFrom(from, to, amount); - } -} diff --git a/src/test/PackBenchmark.t.sol b/src/test/PackBenchmark.t.sol deleted file mode 100644 index f1a9be149..000000000 --- a/src/test/PackBenchmark.t.sol +++ /dev/null @@ -1,258 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -import { Pack } from "contracts/pack/Pack.sol"; -import { IPack } from "contracts/interfaces/IPack.sol"; -import { ITokenBundle } from "contracts/extension/interface/ITokenBundle.sol"; - -// Test imports -import { MockERC20 } from "./mocks/MockERC20.sol"; -import { Wallet } from "./utils/Wallet.sol"; -import "./utils/BaseTest.sol"; - -contract CreatePackBenchmarkTest is BaseTest { - Pack internal pack; - - Wallet internal tokenOwner; - string internal packUri; - ITokenBundle.Token[] internal packContents; - uint256[] internal numOfRewardUnits; - - function setUp() public override { - super.setUp(); - - pack = Pack(payable(getContract("Pack"))); - - tokenOwner = getWallet(); - packUri = "ipfs://"; - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 0, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc1155), - tokenType: ITokenBundle.TokenType.ERC1155, - tokenId: 0, - totalAmount: 100 - }) - ); - numOfRewardUnits.push(20); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 1000 ether - }) - ); - numOfRewardUnits.push(50); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 1, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 1000 ether - }) - ); - numOfRewardUnits.push(100); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 2, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 3, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 4, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - erc20.mint(address(tokenOwner), 2000 ether); - erc721.mint(address(tokenOwner), 5); - erc1155.mint(address(tokenOwner), 0, 100); - - tokenOwner.setAllowanceERC20(address(erc20), address(pack), type(uint256).max); - tokenOwner.setApprovalForAllERC721(address(erc721), address(pack), true); - tokenOwner.setApprovalForAllERC1155(address(erc1155), address(pack), true); - - vm.prank(deployer); - pack.grantRole(keccak256("MINTER_ROLE"), address(tokenOwner)); - - vm.startPrank(address(tokenOwner)); - } - - /*/////////////////////////////////////////////////////////////// - Unit tests: `createPack` - //////////////////////////////////////////////////////////////*/ - - /** - * note: Testing state changes; token owner calls `createPack` to pack owned tokens. - */ - function test_benchmark_createPack() public { - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, address(0x123)); - } -} - -contract OpenPackBenchmarkTest is BaseTest { - Pack internal pack; - - Wallet internal tokenOwner; - string internal packUri; - ITokenBundle.Token[] internal packContents; - uint256[] internal numOfRewardUnits; - - function setUp() public override { - super.setUp(); - - pack = Pack(payable(getContract("Pack"))); - - tokenOwner = getWallet(); - packUri = "ipfs://"; - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 0, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc1155), - tokenType: ITokenBundle.TokenType.ERC1155, - tokenId: 0, - totalAmount: 100 - }) - ); - numOfRewardUnits.push(20); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 1000 ether - }) - ); - numOfRewardUnits.push(50); - - // packContents.push( - // ITokenBundle.Token({ - // assetContract: address(erc721), - // tokenType: ITokenBundle.TokenType.ERC721, - // tokenId: 1, - // totalAmount: 1 - // }) - // ); - // amountsPerUnit.push(1); - - // packContents.push( - // ITokenBundle.Token({ - // assetContract: address(erc20), - // tokenType: ITokenBundle.TokenType.ERC20, - // tokenId: 0, - // totalAmount: 1000 ether - // }) - // ); - // amountsPerUnit.push(10 ether); - - // packContents.push( - // ITokenBundle.Token({ - // assetContract: address(erc721), - // tokenType: ITokenBundle.TokenType.ERC721, - // tokenId: 2, - // totalAmount: 1 - // }) - // ); - // amountsPerUnit.push(1); - - // packContents.push( - // ITokenBundle.Token({ - // assetContract: address(erc721), - // tokenType: ITokenBundle.TokenType.ERC721, - // tokenId: 3, - // totalAmount: 1 - // }) - // ); - // amountsPerUnit.push(1); - - // packContents.push( - // ITokenBundle.Token({ - // assetContract: address(erc721), - // tokenType: ITokenBundle.TokenType.ERC721, - // tokenId: 4, - // totalAmount: 1 - // }) - // ); - // amountsPerUnit.push(1); - - erc20.mint(address(tokenOwner), 2000 ether); - erc721.mint(address(tokenOwner), 5); - erc1155.mint(address(tokenOwner), 0, 100); - - tokenOwner.setAllowanceERC20(address(erc20), address(pack), type(uint256).max); - tokenOwner.setApprovalForAllERC721(address(erc721), address(pack), true); - tokenOwner.setApprovalForAllERC1155(address(erc1155), address(pack), true); - - vm.prank(deployer); - pack.grantRole(keccak256("MINTER_ROLE"), address(tokenOwner)); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, address(0x123)); - - vm.startPrank(address(0x123), address(0x123)); - } - - /*/////////////////////////////////////////////////////////////// - Unit tests: `openPack` - //////////////////////////////////////////////////////////////*/ - - /** - * note: Testing state changes; pack owner calls `openPack` to redeem underlying rewards. - */ - function test_benchmark_openPack() public { - pack.openPack(0, 1); - } -} diff --git a/src/test/ProxyBenchmark.t.sol b/src/test/ProxyBenchmark.t.sol deleted file mode 100644 index 9a425eca7..000000000 --- a/src/test/ProxyBenchmark.t.sol +++ /dev/null @@ -1,103 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -// Test imports -import "./utils/BaseTest.sol"; -import "contracts/TWFactory.sol"; -import "contracts/TWRegistry.sol"; - -// Helpers -import "contracts/TWProxy.sol"; -import "@openzeppelin/contracts/utils/Create2.sol"; -// import "./utils/Console.sol"; -import "./mocks/MockThirdwebContract.sol"; - -contract TWProxyBenchmark is BaseTest { - function setUp() public override { - super.setUp(); - } - - function testBenchmark_deployDrop721() public { - deployContractProxy( - "DropERC721", - abi.encodeCall( - DropERC721.initialize, - ( - deployer, - NAME, - SYMBOL, - CONTRACT_URI, - forwarders(), - saleRecipient, - royaltyRecipient, - royaltyBps, - platformFeeBps, - platformFeeRecipient - ) - ) - ); - } - - function testBenchmark_deployDrop1155() public { - deployContractProxy( - "DropERC1155", - abi.encodeCall( - DropERC1155.initialize, - ( - deployer, - NAME, - SYMBOL, - CONTRACT_URI, - forwarders(), - saleRecipient, - royaltyRecipient, - royaltyBps, - platformFeeBps, - platformFeeRecipient - ) - ) - ); - } - - function testBenchmark_deployToken721() public { - deployContractProxy( - "TokenERC721", - abi.encodeCall( - TokenERC721.initialize, - ( - deployer, - NAME, - SYMBOL, - CONTRACT_URI, - forwarders(), - saleRecipient, - royaltyRecipient, - royaltyBps, - platformFeeBps, - platformFeeRecipient - ) - ) - ); - } - - function testBenchmark_deployToken1155() public { - deployContractProxy( - "TokenERC1155", - abi.encodeCall( - TokenERC1155.initialize, - ( - deployer, - NAME, - SYMBOL, - CONTRACT_URI, - forwarders(), - saleRecipient, - royaltyRecipient, - royaltyBps, - platformFeeBps, - platformFeeRecipient - ) - ) - ); - } -} diff --git a/src/test/SignatureDrop.t.sol b/src/test/SignatureDrop.t.sol index 32c2663ed..f26f82022 100644 --- a/src/test/SignatureDrop.t.sol +++ b/src/test/SignatureDrop.t.sol @@ -1,123 +1,17 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; -import { SignatureDrop, IDropSinglePhase, IDelayedReveal, ISignatureMintERC721, ERC721AUpgradeable, IPermissions, ILazyMint } from "contracts/signature-drop/SignatureDrop.sol"; +import { SignatureDrop, DropSinglePhase, Permissions, LazyMint, BatchMintMetadata, DelayedReveal, IDropSinglePhase, IDelayedReveal, ISignatureMintERC721, ERC721AUpgradeable, IPermissions, ILazyMint } from "contracts/prebuilts/signature-drop/SignatureDrop.sol"; +import { SignatureMintERC721 } from "contracts/extension/SignatureMintERC721.sol"; // Test imports import "erc721a-upgradeable/contracts/IERC721AUpgradeable.sol"; -import "contracts/lib/TWStrings.sol"; import "./utils/BaseTest.sol"; import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; -import "@openzeppelin/contracts/utils/Strings.sol"; - -contract SignatureDropBenchmarkTest is BaseTest { - using StringsUpgradeable for uint256; - - SignatureDrop public sigdrop; - address internal deployerSigner; - bytes32 internal typehashMintRequest; - bytes32 internal nameHash; - bytes32 internal versionHash; - bytes32 internal typehashEip712; - bytes32 internal domainSeparator; - - SignatureDrop.AllowlistProof alp; - SignatureDrop.MintRequest _mintrequest; - bytes _signature; - - using stdStorage for StdStorage; - - function setUp() public override { - super.setUp(); - deployerSigner = signer; - sigdrop = SignatureDrop(getContract("SignatureDrop")); - - erc20.mint(deployerSigner, 1_000_000); - vm.deal(deployerSigner, 1_000); - - typehashMintRequest = keccak256( - "MintRequest(address to,address royaltyRecipient,uint256 royaltyBps,address primarySaleRecipient,string uri,uint256 quantity,uint256 pricePerToken,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" - ); - nameHash = keccak256(bytes("SignatureMintERC721")); - versionHash = keccak256(bytes("1")); - typehashEip712 = keccak256( - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" - ); - domainSeparator = keccak256(abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(sigdrop))); - - // ========================== - - bytes32[] memory proofs = new bytes32[](0); - alp.proof = proofs; - - SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); - conditions[0].maxClaimableSupply = 100; - conditions[0].quantityLimitPerTransaction = 100; - conditions[0].waitTimeInSecondsBetweenClaims = type(uint256).max; - - vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", ""); - vm.prank(deployerSigner); - sigdrop.setClaimConditions(conditions[0], false); - uint256 id = 0; - - _mintrequest.to = address(0); - _mintrequest.royaltyRecipient = address(2); - _mintrequest.royaltyBps = 0; - _mintrequest.primarySaleRecipient = address(deployer); - _mintrequest.uri = "ipfs://"; - _mintrequest.quantity = 1; - _mintrequest.pricePerToken = 1; - _mintrequest.currency = address(erc20); - _mintrequest.validityStartTimestamp = 1000; - _mintrequest.validityEndTimestamp = 2000; - _mintrequest.uid = bytes32(id); - - _signature = signMintRequest(_mintrequest, privateKey); - vm.startPrank(deployerSigner, deployerSigner); - - vm.warp(1000); - erc20.approve(address(sigdrop), 1); - } - - function signMintRequest(SignatureDrop.MintRequest memory _request, uint256 privateKey) - internal - returns (bytes memory) - { - bytes memory encodedRequest = abi.encode( - typehashMintRequest, - _request.to, - _request.royaltyRecipient, - _request.royaltyBps, - _request.primarySaleRecipient, - keccak256(bytes(_request.uri)), - _request.quantity, - _request.pricePerToken, - _request.currency, - _request.validityStartTimestamp, - _request.validityEndTimestamp, - _request.uid - ); - bytes32 structHash = keccak256(encodedRequest); - bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, typedDataHash); - bytes memory sig = abi.encodePacked(r, s, v); - - return sig; - } - - function test_benchmark_mintWithSignature() public { - sigdrop.mintWithSignature(_mintrequest, _signature); - } - - function test_benchmark_claim() public { - sigdrop.claim(address(25), 1, address(0), 0, alp, ""); - } -} contract SignatureDropTest is BaseTest { - using StringsUpgradeable for uint256; + using Strings for uint256; + using Strings for address; event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI); event TokenURIRevealed(uint256 indexed index, string revealedURI); @@ -136,6 +30,8 @@ contract SignatureDropTest is BaseTest { bytes32 internal typehashEip712; bytes32 internal domainSeparator; + bytes private emptyEncodedBytes = abi.encode("", ""); + using stdStorage for StdStorage; function setUp() public override { @@ -143,8 +39,8 @@ contract SignatureDropTest is BaseTest { deployerSigner = signer; sigdrop = SignatureDrop(getContract("SignatureDrop")); - erc20.mint(deployerSigner, 1_000_000); - vm.deal(deployerSigner, 1_000); + erc20.mint(deployerSigner, 1_000 ether); + vm.deal(deployerSigner, 1_000 ether); typehashMintRequest = keccak256( "MintRequest(address to,address royaltyRecipient,uint256 royaltyBps,address primarySaleRecipient,string uri,uint256 quantity,uint256 pricePerToken,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" @@ -169,14 +65,7 @@ contract SignatureDropTest is BaseTest { bytes32 role = keccak256("MINTER_ROLE"); vm.prank(caller); - vm.expectRevert( - abi.encodePacked( - "Permissions: account ", - TWStrings.toHexString(uint160(caller), 20), - " is missing role ", - TWStrings.toHexString(uint256(role), 32) - ) - ); + vm.expectRevert(); sigdrop.renounceRole(role, caller); } @@ -189,14 +78,7 @@ contract SignatureDropTest is BaseTest { bytes32 role = keccak256("MINTER_ROLE"); vm.prank(deployerSigner); - vm.expectRevert( - abi.encodePacked( - "Permissions: account ", - TWStrings.toHexString(uint160(target), 20), - " is missing role ", - TWStrings.toHexString(uint256(role), 32) - ) - ); + vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsUnauthorizedAccount.selector, target, role)); sigdrop.revokeRole(role, target); } @@ -212,7 +94,7 @@ contract SignatureDropTest is BaseTest { sigdrop.grantRole(role, receiver); - vm.expectRevert("Can only grant to non holders"); + vm.expectRevert(); sigdrop.grantRole(role, receiver); vm.stopPrank(); @@ -236,7 +118,7 @@ contract SignatureDropTest is BaseTest { sigdrop.grantRole(role, receiver); // expect revert when granting to a holder - vm.expectRevert("Can only grant to non holders"); + vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsAlreadyGranted.selector, receiver, role)); sigdrop.grantRole(role, receiver); // check if receiver has transfer role @@ -333,10 +215,10 @@ contract SignatureDropTest is BaseTest { SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); conditions[0].maxClaimableSupply = 100; - conditions[0].quantityLimitPerTransaction = 100; + conditions[0].quantityLimitPerWallet = 100; vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", ""); + sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); vm.prank(deployerSigner); sigdrop.setClaimConditions(conditions[0], false); @@ -382,11 +264,10 @@ contract SignatureDropTest is BaseTest { SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); conditions[0].startTimestamp = 100; conditions[0].maxClaimableSupply = 100; - conditions[0].quantityLimitPerTransaction = 100; - conditions[0].waitTimeInSecondsBetweenClaims = type(uint256).max; + conditions[0].quantityLimitPerWallet = 100; vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", ""); + sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); vm.prank(deployerSigner); sigdrop.setClaimConditions(conditions[0], false); @@ -397,7 +278,13 @@ contract SignatureDropTest is BaseTest { vm.warp(99); vm.prank(getActor(5), getActor(5)); - vm.expectRevert("cant claim yet"); + vm.expectRevert( + abi.encodeWithSelector( + DropSinglePhase.DropClaimNotStarted.selector, + conditions[0].startTimestamp, + block.timestamp + ) + ); sigdrop.claim(receiver, 1, address(0), 0, alp, ""); } @@ -411,12 +298,11 @@ contract SignatureDropTest is BaseTest { function test_state_lazyMint_noEncryptedURI() public { uint256 amountToLazyMint = 100; string memory baseURI = "ipfs://"; - bytes memory encryptedBaseURI = ""; uint256 nextTokenIdToMintBefore = sigdrop.nextTokenIdToMint(); vm.startPrank(deployerSigner); - uint256 batchId = sigdrop.lazyMint(amountToLazyMint, baseURI, encryptedBaseURI); + uint256 batchId = sigdrop.lazyMint(amountToLazyMint, baseURI, emptyEncodedBytes); assertEq(nextTokenIdToMintBefore + amountToLazyMint, sigdrop.nextTokenIdToMint()); assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); @@ -437,11 +323,12 @@ contract SignatureDropTest is BaseTest { uint256 amountToLazyMint = 100; string memory baseURI = "ipfs://"; bytes memory encryptedBaseURI = "encryptedBaseURI://"; + bytes32 provenanceHash = bytes32("whatever"); uint256 nextTokenIdToMintBefore = sigdrop.nextTokenIdToMint(); vm.startPrank(deployerSigner); - uint256 batchId = sigdrop.lazyMint(amountToLazyMint, baseURI, encryptedBaseURI); + uint256 batchId = sigdrop.lazyMint(amountToLazyMint, baseURI, abi.encode(encryptedBaseURI, provenanceHash)); assertEq(nextTokenIdToMintBefore + amountToLazyMint, sigdrop.nextTokenIdToMint()); assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); @@ -458,15 +345,17 @@ contract SignatureDropTest is BaseTest { * note: Testing revert condition; an address without MINTER_ROLE calls lazyMint function. */ function test_revert_lazyMint_MINTER_ROLE() public { - bytes memory errorMessage = abi.encodePacked( - "Permissions: account ", - Strings.toHexString(uint160(address(this)), 20), - " is missing role ", - Strings.toHexString(uint256(keccak256("MINTER_ROLE")), 32) - ); + bytes32 _minterRole = keccak256("MINTER_ROLE"); - vm.expectRevert(errorMessage); - sigdrop.lazyMint(100, "ipfs://", ""); + vm.prank(deployerSigner); + sigdrop.grantRole(_minterRole, address(0x345)); + + vm.prank(address(0x345)); + sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + vm.prank(address(0x567)); + vm.expectRevert(abi.encodeWithSelector(LazyMint.LazyMintUnauthorized.selector)); + sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); } /* @@ -475,9 +364,9 @@ contract SignatureDropTest is BaseTest { function test_revert_lazyMint_URIForNonLazyMintedToken() public { vm.startPrank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", ""); + sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - vm.expectRevert("Invalid tokenId"); + vm.expectRevert(abi.encodeWithSelector(BatchMintMetadata.BatchMintInvalidTokenId.selector, 100)); sigdrop.tokenURI(100); vm.stopPrank(); @@ -490,8 +379,8 @@ contract SignatureDropTest is BaseTest { vm.startPrank(deployerSigner); vm.expectEmit(true, false, false, true); - emit TokensLazyMinted(0, 99, "ipfs://", ""); - sigdrop.lazyMint(100, "ipfs://", ""); + emit TokensLazyMinted(0, 99, "ipfs://", emptyEncodedBytes); + sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); vm.stopPrank(); } @@ -504,12 +393,11 @@ contract SignatureDropTest is BaseTest { uint256 amountToLazyMint = x; string memory baseURI = "ipfs://"; - bytes memory encryptedBaseURI = ""; uint256 nextTokenIdToMintBefore = sigdrop.nextTokenIdToMint(); vm.startPrank(deployerSigner); - uint256 batchId = sigdrop.lazyMint(amountToLazyMint, baseURI, encryptedBaseURI); + uint256 batchId = sigdrop.lazyMint(amountToLazyMint, baseURI, emptyEncodedBytes); assertEq(nextTokenIdToMintBefore + amountToLazyMint, sigdrop.nextTokenIdToMint()); assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); @@ -541,11 +429,12 @@ contract SignatureDropTest is BaseTest { uint256 amountToLazyMint = x; string memory baseURI = "ipfs://"; bytes memory encryptedBaseURI = "encryptedBaseURI://"; + bytes32 provenanceHash = bytes32("whatever"); uint256 nextTokenIdToMintBefore = sigdrop.nextTokenIdToMint(); vm.startPrank(deployerSigner); - uint256 batchId = sigdrop.lazyMint(amountToLazyMint, baseURI, encryptedBaseURI); + uint256 batchId = sigdrop.lazyMint(amountToLazyMint, baseURI, abi.encode(encryptedBaseURI, provenanceHash)); assertEq(nextTokenIdToMintBefore + amountToLazyMint, sigdrop.nextTokenIdToMint()); assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); @@ -577,7 +466,7 @@ contract SignatureDropTest is BaseTest { if (x == 0) { vm.expectRevert("Zero amount"); } - sigdrop.lazyMint(x, "ipfs://", ""); + sigdrop.lazyMint(x, "ipfs://", emptyEncodedBytes); uint256 slot = stdstore.target(address(sigdrop)).sig("nextTokenIdToMint()").find(); bytes32 loc = bytes32(slot); @@ -602,7 +491,9 @@ contract SignatureDropTest is BaseTest { bytes memory secretURI = "ipfs://"; string memory placeholderURI = "ipfs://"; bytes memory encryptedURI = sigdrop.encryptDecrypt(secretURI, key); - sigdrop.lazyMint(amountToLazyMint, placeholderURI, encryptedURI); + bytes32 provenanceHash = keccak256(abi.encodePacked(secretURI, key, block.chainid)); + + sigdrop.lazyMint(amountToLazyMint, placeholderURI, abi.encode(encryptedURI, provenanceHash)); for (uint256 i = 0; i < amountToLazyMint; i += 1) { string memory uri = sigdrop.tokenURI(i); @@ -624,21 +515,22 @@ contract SignatureDropTest is BaseTest { * note: Testing revert condition; an address without MINTER_ROLE calls reveal function. */ function test_revert_reveal_MINTER_ROLE() public { - bytes memory encryptedURI = sigdrop.encryptDecrypt("ipfs://", "key"); + bytes memory key = "key"; + bytes memory encryptedURI = sigdrop.encryptDecrypt("ipfs://", key); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); vm.prank(deployerSigner); - sigdrop.lazyMint(100, "", encryptedURI); + sigdrop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); vm.prank(deployerSigner); sigdrop.reveal(0, "key"); - bytes memory errorMessage = abi.encodePacked( - "Permissions: account ", - TWStrings.toHexString(uint160(address(this)), 20), - " is missing role ", - TWStrings.toHexString(uint256(keccak256("MINTER_ROLE")), 32) + vm.expectRevert( + abi.encodeWithSelector( + Permissions.PermissionsUnauthorizedAccount.selector, + address(this), + keccak256("MINTER_ROLE") + ) ); - - vm.expectRevert(errorMessage); sigdrop.reveal(0, "key"); } @@ -648,14 +540,16 @@ contract SignatureDropTest is BaseTest { function test_revert_reveal_revealingNonExistentBatch() public { vm.startPrank(deployerSigner); - bytes memory encryptedURI = sigdrop.encryptDecrypt("ipfs://", "key"); - sigdrop.lazyMint(100, "", encryptedURI); + bytes memory key = "key"; + bytes memory encryptedURI = sigdrop.encryptDecrypt("ipfs://", key); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); + sigdrop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); sigdrop.reveal(0, "key"); console.log(sigdrop.getBaseURICount()); - sigdrop.lazyMint(100, "", encryptedURI); - vm.expectRevert("Invalid index"); + sigdrop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + vm.expectRevert(abi.encodeWithSelector(BatchMintMetadata.BatchMintInvalidBatchId.selector, 2)); sigdrop.reveal(2, "key"); vm.stopPrank(); @@ -667,11 +561,13 @@ contract SignatureDropTest is BaseTest { function test_revert_delayedReveal_alreadyRevealed() public { vm.startPrank(deployerSigner); - bytes memory encryptedURI = sigdrop.encryptDecrypt("ipfs://", "key"); - sigdrop.lazyMint(100, "", encryptedURI); + bytes memory key = "key"; + bytes memory encryptedURI = sigdrop.encryptDecrypt("ipfs://", key); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); + sigdrop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); sigdrop.reveal(0, "key"); - vm.expectRevert("Nothing to reveal"); + vm.expectRevert(abi.encodeWithSelector(DelayedReveal.DelayedRevealNothingToReveal.selector)); sigdrop.reveal(0, "key"); vm.stopPrank(); @@ -680,14 +576,16 @@ contract SignatureDropTest is BaseTest { /* * note: Testing state changes; revealing URI with an incorrect key. */ - function testFail_reveal_incorrectKey() public { + function test_revert_reveal_incorrectKey() public { vm.startPrank(deployerSigner); - bytes memory encryptedURI = sigdrop.encryptDecrypt("ipfs://", "key"); - sigdrop.lazyMint(100, "", encryptedURI); + bytes memory key = "key"; + bytes memory encryptedURI = sigdrop.encryptDecrypt("ipfs://", key); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); + sigdrop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + vm.expectRevert(); string memory revealedURI = sigdrop.reveal(0, "keyy"); - assertEq(revealedURI, "ipfs://"); vm.stopPrank(); } @@ -698,8 +596,10 @@ contract SignatureDropTest is BaseTest { function test_event_reveal_TokenURIRevealed() public { vm.startPrank(deployerSigner); - bytes memory encryptedURI = sigdrop.encryptDecrypt("ipfs://", "key"); - sigdrop.lazyMint(100, "", encryptedURI); + bytes memory key = "key"; + bytes memory encryptedURI = sigdrop.encryptDecrypt("ipfs://", key); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); + sigdrop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); vm.expectEmit(true, false, false, true); emit TokenURIRevealed(0, "ipfs://"); @@ -712,10 +612,10 @@ contract SignatureDropTest is BaseTest { Signature Mint Tests //////////////////////////////////////////////////////////////*/ - function signMintRequest(SignatureDrop.MintRequest memory mintrequest, uint256 privateKey) - internal - returns (bytes memory) - { + function signMintRequest( + SignatureDrop.MintRequest memory mintrequest, + uint256 privateKey + ) internal view returns (bytes memory) { bytes memory encodedRequest = abi.encode( typehashMintRequest, mintrequest.to, @@ -744,11 +644,11 @@ contract SignatureDropTest is BaseTest { */ function test_state_mintWithSignature() public { vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", ""); + sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); uint256 id = 0; SignatureDrop.MintRequest memory mintrequest; - mintrequest.to = address(0); + mintrequest.to = address(0x567); mintrequest.royaltyRecipient = address(2); mintrequest.royaltyBps = 0; mintrequest.primarySaleRecipient = address(deployer); @@ -769,7 +669,7 @@ contract SignatureDropTest is BaseTest { vm.warp(1000); erc20.approve(address(sigdrop), 1); vm.expectEmit(true, true, true, false); - emit TokensMintedWithSignature(deployerSigner, deployerSigner, 0, mintrequest); + emit TokensMintedWithSignature(deployerSigner, address(0x567), 0, mintrequest); sigdrop.mintWithSignature(mintrequest, signature); vm.stopPrank(); @@ -794,16 +694,89 @@ contract SignatureDropTest is BaseTest { } } + /* + * note: Testing state changes; minting with signature, for a given price and currency. + */ + function test_state_mintWithSignature_UpdateRoyaltyAndSaleInfo() public { + vm.prank(deployerSigner); + sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); + uint256 id = 0; + SignatureDrop.MintRequest memory mintrequest; + + mintrequest.to = address(0x567); + mintrequest.royaltyRecipient = address(0x567); + mintrequest.royaltyBps = 100; + mintrequest.primarySaleRecipient = address(0x567); + mintrequest.uri = "ipfs://"; + mintrequest.quantity = 1; + mintrequest.pricePerToken = 1 ether; + mintrequest.currency = address(erc20); + mintrequest.validityStartTimestamp = 1000; + mintrequest.validityEndTimestamp = 2000; + mintrequest.uid = bytes32(id); + + // Test with ERC20 currency + { + erc20.mint(address(0x345), 1 ether); + uint256 totalSupplyBefore = sigdrop.totalSupply(); + + bytes memory signature = signMintRequest(mintrequest, privateKey); + vm.startPrank(address(0x345)); + vm.warp(1000); + erc20.approve(address(sigdrop), 1 ether); + vm.expectEmit(true, true, true, true); + emit TokensMintedWithSignature(deployerSigner, address(0x567), 0, mintrequest); + sigdrop.mintWithSignature(mintrequest, signature); + vm.stopPrank(); + + assertEq(totalSupplyBefore + mintrequest.quantity, sigdrop.totalSupply()); + + (address _royaltyRecipient, uint16 _royaltyBps) = sigdrop.getRoyaltyInfoForToken(0); + assertEq(_royaltyRecipient, address(0x567)); + assertEq(_royaltyBps, 100); + + uint256 totalPrice = 1 * 1 ether; + uint256 platformFees = (totalPrice * platformFeeBps) / MAX_BPS; + assertEq(erc20.balanceOf(address(0x567)), totalPrice - platformFees); + } + + // Test with native token currency + { + vm.deal(address(0x345), 1 ether); + uint256 totalSupplyBefore = sigdrop.totalSupply(); + + mintrequest.currency = address(NATIVE_TOKEN); + id = 1; + mintrequest.uid = bytes32(id); + + bytes memory signature = signMintRequest(mintrequest, privateKey); + vm.startPrank(address(0x345)); + vm.warp(1000); + sigdrop.mintWithSignature{ value: mintrequest.pricePerToken }(mintrequest, signature); + vm.stopPrank(); + + assertEq(totalSupplyBefore + mintrequest.quantity, sigdrop.totalSupply()); + + (address _royaltyRecipient, uint16 _royaltyBps) = sigdrop.getRoyaltyInfoForToken(0); + assertEq(_royaltyRecipient, address(0x567)); + assertEq(_royaltyBps, 100); + + uint256 totalPrice = 1 * 1 ether; + uint256 platformFees = (totalPrice * platformFeeBps) / MAX_BPS; + assertEq(address(0x567).balance, totalPrice - platformFees); + } + } + /** * note: Testing revert condition; invalid signature. */ function test_revert_mintWithSignature_unapprovedSigner() public { vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", ""); + sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); uint256 id = 0; SignatureDrop.MintRequest memory mintrequest; - mintrequest.to = address(0); + mintrequest.to = address(0x567); mintrequest.royaltyRecipient = address(2); mintrequest.royaltyBps = 0; mintrequest.primarySaleRecipient = address(deployer); @@ -821,7 +794,36 @@ contract SignatureDropTest is BaseTest { sigdrop.mintWithSignature(mintrequest, signature); signature = signMintRequest(mintrequest, 4321); - vm.expectRevert("Invalid req"); + vm.expectRevert(abi.encodeWithSelector(SignatureMintERC721.SignatureMintInvalidSigner.selector)); + sigdrop.mintWithSignature(mintrequest, signature); + } + + /** + * note: Testing revert condition; minting zero tokens. + */ + function test_revert_mintWithSignature_zeroQuantity() public { + vm.prank(deployerSigner); + sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); + uint256 id = 0; + + SignatureDrop.MintRequest memory mintrequest; + mintrequest.to = address(0x567); + mintrequest.royaltyRecipient = address(2); + mintrequest.royaltyBps = 0; + mintrequest.primarySaleRecipient = address(deployer); + mintrequest.uri = "ipfs://"; + mintrequest.quantity = 0; + mintrequest.pricePerToken = 0; + mintrequest.currency = address(3); + mintrequest.validityStartTimestamp = 1000; + mintrequest.validityEndTimestamp = 2000; + mintrequest.uid = bytes32(id); + + bytes memory signature = signMintRequest(mintrequest, privateKey); + vm.warp(1000); + + vm.prank(deployerSigner); + vm.expectRevert(abi.encodeWithSelector(SignatureMintERC721.SignatureMintInvalidQuantity.selector)); sigdrop.mintWithSignature(mintrequest, signature); } @@ -830,7 +832,7 @@ contract SignatureDropTest is BaseTest { */ function test_revert_mintWithSignature_notEnoughMintedTokens() public { vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", ""); + sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); uint256 id = 0; SignatureDrop.MintRequest memory mintrequest; @@ -848,7 +850,7 @@ contract SignatureDropTest is BaseTest { bytes memory signature = signMintRequest(mintrequest, privateKey); vm.warp(1000); - vm.expectRevert("Not enough tokens"); + vm.expectRevert("!Tokens"); sigdrop.mintWithSignature(mintrequest, signature); } @@ -857,11 +859,11 @@ contract SignatureDropTest is BaseTest { */ function test_revert_mintWithSignature_notSentAmountRequired() public { vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", ""); + sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); uint256 id = 0; SignatureDrop.MintRequest memory mintrequest; - mintrequest.to = address(0); + mintrequest.to = address(0x567); mintrequest.royaltyRecipient = address(2); mintrequest.royaltyBps = 0; mintrequest.primarySaleRecipient = address(deployer); @@ -877,7 +879,7 @@ contract SignatureDropTest is BaseTest { bytes memory signature = signMintRequest(mintrequest, privateKey); vm.startPrank(address(deployerSigner)); vm.warp(mintrequest.validityStartTimestamp); - vm.expectRevert("Must send total price"); + vm.expectRevert("!Price"); sigdrop.mintWithSignature{ value: 2 }(mintrequest, signature); vm.stopPrank(); } @@ -888,11 +890,11 @@ contract SignatureDropTest is BaseTest { */ function test_balances_mintWithSignature() public { vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", ""); + sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); uint256 id = 0; SignatureDrop.MintRequest memory mintrequest; - mintrequest.to = address(0); + mintrequest.to = address(0x567); mintrequest.royaltyRecipient = address(2); mintrequest.royaltyBps = 0; mintrequest.primarySaleRecipient = address(deployer); @@ -914,11 +916,11 @@ contract SignatureDropTest is BaseTest { sigdrop.mintWithSignature(mintrequest, signature); vm.stopPrank(); - uint256 balance = sigdrop.balanceOf(address(deployerSigner)); + uint256 balance = sigdrop.balanceOf(address(0x567)); assertEq(balance, 1); address owner = sigdrop.ownerOf(0); - assertEq(deployerSigner, owner); + assertEq(address(0x567), owner); assertEq( currencyBalBefore - mintrequest.pricePerToken * mintrequest.quantity, @@ -935,7 +937,7 @@ contract SignatureDropTest is BaseTest { */ function mintWithSignature(SignatureDrop.MintRequest memory mintrequest) internal { vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", ""); + sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); uint256 id = 0; { @@ -964,7 +966,7 @@ contract SignatureDropTest is BaseTest { uint256 id = 0; SignatureDrop.MintRequest memory mintrequest; - mintrequest.to = address(0); + mintrequest.to = address(0x567); mintrequest.royaltyRecipient = address(2); mintrequest.royaltyBps = 0; mintrequest.primarySaleRecipient = address(deployer); @@ -984,36 +986,6 @@ contract SignatureDropTest is BaseTest { Claim Tests //////////////////////////////////////////////////////////////*/ - /** - * note: Testing revert condition; not allowed to claim again before wait time is over. - */ - function test_revert_claimCondition_waitTimeInSecondsBetweenClaims() public { - vm.warp(1); - - address receiver = getActor(0); - bytes32[] memory proofs = new bytes32[](0); - - SignatureDrop.AllowlistProof memory alp; - alp.proof = proofs; - - SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); - conditions[0].maxClaimableSupply = 100; - conditions[0].quantityLimitPerTransaction = 100; - conditions[0].waitTimeInSecondsBetweenClaims = type(uint256).max; - - vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", ""); - vm.prank(deployerSigner); - sigdrop.setClaimConditions(conditions[0], false); - - vm.prank(getActor(5), getActor(5)); - sigdrop.claim(receiver, 1, address(0), 0, alp, ""); - - vm.expectRevert("cant claim yet"); - vm.prank(getActor(5), getActor(5)); - sigdrop.claim(receiver, 1, address(0), 0, alp, ""); - } - /** * note: Testing revert condition; not enough minted tokens. */ @@ -1028,15 +1000,14 @@ contract SignatureDropTest is BaseTest { SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); conditions[0].maxClaimableSupply = 100; - conditions[0].quantityLimitPerTransaction = 100; - conditions[0].waitTimeInSecondsBetweenClaims = type(uint256).max; + conditions[0].quantityLimitPerWallet = 100; vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", ""); + sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); vm.prank(deployerSigner); sigdrop.setClaimConditions(conditions[0], false); - vm.expectRevert("Not enough tokens"); + vm.expectRevert("!Tokens"); vm.prank(getActor(6), getActor(6)); sigdrop.claim(receiver, 101, address(0), 0, alp, ""); } @@ -1055,18 +1026,23 @@ contract SignatureDropTest is BaseTest { SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); conditions[0].maxClaimableSupply = 100; - conditions[0].quantityLimitPerTransaction = 100; - conditions[0].waitTimeInSecondsBetweenClaims = type(uint256).max; + conditions[0].quantityLimitPerWallet = 100; vm.prank(deployerSigner); - sigdrop.lazyMint(200, "ipfs://", ""); + sigdrop.lazyMint(200, "ipfs://", emptyEncodedBytes); vm.prank(deployerSigner); sigdrop.setClaimConditions(conditions[0], false); vm.prank(getActor(5), getActor(5)); sigdrop.claim(receiver, 100, address(0), 0, alp, ""); - vm.expectRevert("exceeds max supply"); + vm.expectRevert( + abi.encodeWithSelector( + DropSinglePhase.DropClaimExceedMaxSupply.selector, + conditions[0].maxClaimableSupply, + 101 + ) + ); vm.prank(getActor(6), getActor(6)); sigdrop.claim(receiver, 1, address(0), 0, alp, ""); } @@ -1083,80 +1059,104 @@ contract SignatureDropTest is BaseTest { SignatureDrop.AllowlistProof memory alp; alp.proof = proofs; - alp.maxQuantityInAllowlist = x; + alp.quantityLimitPerWallet = x; SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); conditions[0].maxClaimableSupply = 500; - conditions[0].quantityLimitPerTransaction = 100; - conditions[0].waitTimeInSecondsBetweenClaims = type(uint256).max; + conditions[0].quantityLimitPerWallet = 100; vm.prank(deployerSigner); - sigdrop.lazyMint(500, "ipfs://", bytes("")); + sigdrop.lazyMint(500, "ipfs://", emptyEncodedBytes); vm.prank(deployerSigner); sigdrop.setClaimConditions(conditions[0], false); vm.prank(getActor(5), getActor(5)); - vm.expectRevert("Invalid quantity"); + vm.expectRevert( + abi.encodeWithSelector( + DropSinglePhase.DropClaimExceedLimit.selector, + conditions[0].quantityLimitPerWallet, + 101 + ) + ); sigdrop.claim(receiver, 101, address(0), 0, alp, ""); vm.prank(deployerSigner); sigdrop.setClaimConditions(conditions[0], true); vm.prank(getActor(5), getActor(5)); - vm.expectRevert("Invalid quantity"); + vm.expectRevert( + abi.encodeWithSelector( + DropSinglePhase.DropClaimExceedLimit.selector, + conditions[0].quantityLimitPerWallet, + 101 + ) + ); sigdrop.claim(receiver, 101, address(0), 0, alp, ""); } - /** - * note: Testing revert condition; can't claim if not in whitelist. - */ - function test_revert_claimCondition_merkleProof() public { - string[] memory inputs = new string[](3); + function test_fuzz_claim_merkleProof(uint256 x) public { + vm.assume(x > 10 && x < 500); + string[] memory inputs = new string[](5); inputs[0] = "node"; inputs[1] = "src/test/scripts/generateRoot.ts"; - inputs[2] = "1"; + inputs[2] = Strings.toString(x); + inputs[3] = "0"; + inputs[4] = "0x0000000000000000000000000000000000000000"; bytes memory result = vm.ffi(inputs); + // revert(); bytes32 root = abi.decode(result, (bytes32)); inputs[1] = "src/test/scripts/getProof.ts"; result = vm.ffi(inputs); bytes32[] memory proofs = abi.decode(result, (bytes32[])); + SignatureDrop.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = x; + alp.pricePerToken = 0; + alp.currency = address(0); + vm.warp(1); - address receiver = address(0x92Bb439374a091c7507bE100183d8D1Ed2c9dAD3); + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); - SignatureDrop.AllowlistProof memory alp; - alp.proof = proofs; - alp.maxQuantityInAllowlist = 1; + // bytes32[] memory proofs = new bytes32[](0); SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); - conditions[0].maxClaimableSupply = 100; - conditions[0].quantityLimitPerTransaction = 100; - conditions[0].waitTimeInSecondsBetweenClaims = type(uint256).max; + conditions[0].maxClaimableSupply = x; + conditions[0].quantityLimitPerWallet = 1; conditions[0].merkleRoot = root; vm.prank(deployerSigner); - sigdrop.lazyMint(200, "ipfs://", ""); + sigdrop.lazyMint(2 * x, "ipfs://", emptyEncodedBytes); vm.prank(deployerSigner); sigdrop.setClaimConditions(conditions[0], false); // vm.prank(getActor(5), getActor(5)); vm.prank(receiver, receiver); - sigdrop.claim(receiver, 1, address(0), 0, alp, ""); + sigdrop.claim(receiver, x - 5, address(0), 0, alp, ""); + assertEq(sigdrop.getSupplyClaimedByWallet(receiver), x - 5); - vm.prank(address(4), address(4)); - vm.expectRevert("not in allowlist"); - sigdrop.claim(receiver, 1, address(0), 0, alp, ""); + vm.prank(receiver, receiver); + vm.expectRevert(abi.encodeWithSelector(DropSinglePhase.DropClaimExceedLimit.selector, x, x + 1)); + sigdrop.claim(receiver, 6, address(0), 0, alp, ""); + + vm.prank(receiver, receiver); + sigdrop.claim(receiver, 5, address(0), 0, alp, ""); + assertEq(sigdrop.getSupplyClaimedByWallet(receiver), x); + + vm.prank(receiver, receiver); + vm.expectRevert(abi.encodeWithSelector(DropSinglePhase.DropClaimExceedLimit.selector, x, x + 5)); + sigdrop.claim(receiver, 5, address(0), 0, alp, ""); } /** * note: Testing state changes; reset eligibility of claim conditions and claiming again for same condition id. */ - function test_state_claimCondition_resetEligibility_waitTimeInSecondsBetweenClaims() public { + function test_state_claimCondition_resetEligibility() public { vm.warp(1); address receiver = getActor(0); @@ -1167,11 +1167,10 @@ contract SignatureDropTest is BaseTest { SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); conditions[0].maxClaimableSupply = 100; - conditions[0].quantityLimitPerTransaction = 100; - conditions[0].waitTimeInSecondsBetweenClaims = type(uint256).max; + conditions[0].quantityLimitPerWallet = 100; vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", ""); + sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); vm.prank(deployerSigner); sigdrop.setClaimConditions(conditions[0], false); @@ -1189,34 +1188,21 @@ contract SignatureDropTest is BaseTest { /*/////////////////////////////////////////////////////////////// Miscellaneous //////////////////////////////////////////////////////////////*/ - function test_breaking_reveal() public { - address attacker = getActor(0); - bytes memory encryptedURI = sigdrop.encryptDecrypt("ipfs://", "key"); - - vm.prank(deployerSigner); - sigdrop.lazyMint(100, "", encryptedURI); - - uint256 batchId = sigdrop.getBatchIdAtIndex(0); - vm.prank(attacker); - sigdrop.getRevealURI(batchId, "wrong keyy"); - - vm.prank(deployerSigner); - sigdrop.reveal(0, "key"); - } function test_delayedReveal_withNewLazyMintedEmptyBatch() public { vm.startPrank(deployerSigner); bytes memory encryptedURI = sigdrop.encryptDecrypt("ipfs://", "key"); - sigdrop.lazyMint(100, "", encryptedURI); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", "key", block.chainid)); + sigdrop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); sigdrop.reveal(0, "key"); string memory uri = sigdrop.tokenURI(1); assertEq(uri, string(abi.encodePacked("ipfs://", "1"))); bytes memory newEncryptedURI = sigdrop.encryptDecrypt("ipfs://secret", "key"); - vm.expectRevert("Minting 0 tokens"); - sigdrop.lazyMint(0, "", newEncryptedURI); + vm.expectRevert(abi.encodeWithSelector(LazyMint.LazyMintInvalidAmount.selector)); + sigdrop.lazyMint(0, "", abi.encode(newEncryptedURI, provenanceHash)); vm.stopPrank(); } @@ -1225,9 +1211,9 @@ contract SignatureDropTest is BaseTest { Reentrancy related Tests //////////////////////////////////////////////////////////////*/ - function testFail_reentrancy_mintWithSignature() public { + function test_revert_reentrancy_mintWithSignature() public { vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", ""); + sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); uint256 id = 0; SignatureDrop.MintRequest memory mintrequest; @@ -1253,13 +1239,12 @@ contract SignatureDropTest is BaseTest { MaliciousReceiver mal = new MaliciousReceiver(address(sigdrop)); vm.deal(address(mal), 100 ether); vm.warp(1000); + vm.expectRevert(); mal.attackMintWithSignature(mintrequest, signature, false); - - assertEq(totalSupplyBefore + mintrequest.quantity, sigdrop.totalSupply()); } } - function testFail_reentrancy_claim() public { + function test_revert_reentrancy_claim() public { vm.warp(1); bytes32[] memory proofs = new bytes32[](0); @@ -1268,21 +1253,21 @@ contract SignatureDropTest is BaseTest { SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); conditions[0].maxClaimableSupply = 100; - conditions[0].quantityLimitPerTransaction = 100; - conditions[0].waitTimeInSecondsBetweenClaims = type(uint256).max; + conditions[0].quantityLimitPerWallet = 100; vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", ""); + sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); vm.prank(deployerSigner); sigdrop.setClaimConditions(conditions[0], false); MaliciousReceiver mal = new MaliciousReceiver(address(sigdrop)); vm.deal(address(mal), 100 ether); + vm.expectRevert(); mal.attackClaim(alp, false); } - function testFail_combination_signatureAndClaim() public { + function test_revert_combination_signatureAndClaim() public { vm.warp(1); bytes32[] memory proofs = new bytes32[](0); @@ -1291,11 +1276,10 @@ contract SignatureDropTest is BaseTest { SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); conditions[0].maxClaimableSupply = 100; - conditions[0].quantityLimitPerTransaction = 100; - conditions[0].waitTimeInSecondsBetweenClaims = type(uint256).max; + conditions[0].quantityLimitPerWallet = 100; vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", ""); + sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); vm.prank(deployerSigner); sigdrop.setClaimConditions(conditions[0], false); @@ -1325,10 +1309,9 @@ contract SignatureDropTest is BaseTest { vm.deal(address(mal), 100 ether); vm.warp(1000); mal.saveCombination(mintrequest, signature, alp); + vm.expectRevert(); mal.attackMintWithSignature(mintrequest, signature, true); // mal.attackClaim(alp, true); - - assertEq(totalSupplyBefore + mintrequest.quantity, sigdrop.totalSupply()); } } } @@ -1373,12 +1356,7 @@ contract MaliciousReceiver { alp = _alp; } - function onERC721Received( - address, - address, - uint256, - bytes calldata - ) external returns (bytes4) { + function onERC721Received(address, address, uint256, bytes calldata) external returns (bytes4) { if (claim && loop) { loop = false; claim = false; diff --git a/src/test/TWFactory.t.sol b/src/test/TWFactory.t.sol index bbd20d2b5..474286352 100644 --- a/src/test/TWFactory.t.sol +++ b/src/test/TWFactory.t.sol @@ -3,13 +3,13 @@ pragma solidity ^0.8.11; // Test imports import "./utils/BaseTest.sol"; -import "contracts/TWFactory.sol"; -import "contracts/TWRegistry.sol"; +import { TWFactory } from "contracts/infra/TWFactory.sol"; +import { TWRegistry } from "contracts/infra/TWRegistry.sol"; // Helpers import "@openzeppelin/contracts/utils/Create2.sol"; import "@openzeppelin/contracts/proxy/Clones.sol"; -import "contracts/TWProxy.sol"; +import "contracts/infra/TWProxy.sol"; // import "./utils/Console.sol"; import "./mocks/MockThirdwebContract.sol"; diff --git a/src/test/TWFee.t.sol b/src/test/TWFee.t.sol deleted file mode 100644 index b0aa40f42..000000000 --- a/src/test/TWFee.t.sol +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -// Test imports -import "./mocks/MockThirdwebContract.sol"; -import "./utils/BaseTest.sol"; -import "contracts/TWFee.sol"; - -// Helpers -import "@openzeppelin/contracts/utils/Create2.sol"; -import "contracts/TWRegistry.sol"; -import "contracts/TWFactory.sol"; -import "contracts/TWProxy.sol"; - -contract TWFeeTest is BaseTest { - // Target contract - TWFee internal twFee; - - // Helper contracts - TWRegistry internal twRegistry; - TWFactory internal twFactory; - MockThirdwebContract internal mockModule; - - // Actors - address internal mockModuleDeployer; - address internal moduleAdmin = address(0x1); - address internal feeAdmin = address(0x2); - address internal notFeeAdmin = address(0x3); - address internal payer = address(0x4); - - // Test params - address internal trustedForwarder = address(0x4); - address internal thirdwebTreasury = address(0x5); - - // ===== Set up ===== - - function setUp() public override {} -} diff --git a/src/test/TWMultichainRegistry.t.sol b/src/test/TWMultichainRegistry.t.sol new file mode 100644 index 000000000..374148b3c --- /dev/null +++ b/src/test/TWMultichainRegistry.t.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +// Test imports +import "./utils/BaseTest.sol"; +import "contracts/infra/interface/ITWMultichainRegistry.sol"; +import { TWMultichainRegistry } from "contracts/infra/TWMultichainRegistry.sol"; +import "./mocks/MockThirdwebContract.sol"; +import "contracts/extension/interface/plugin/IPluginMap.sol"; + +interface ITWMultichainRegistryData { + event Added(address indexed deployer, address indexed moduleAddress, uint256 indexed chainid, string metadataUri); + event Deleted(address indexed deployer, address indexed moduleAddress, uint256 indexed chainid); +} + +contract TWMultichainRegistryTest is ITWMultichainRegistryData, BaseTest { + // Target contract + TWMultichainRegistry internal _registry; + + // Test params + address internal factoryAdmin_; + address internal factory_; + + uint256[] internal chainIds; + address[] internal deploymentAddresses; + address internal deployer_; + + uint256 total = 1000; + + // ===== Set up ===== + + function setUp() public override { + super.setUp(); + + deployer_ = getActor(100); + factory_ = getActor(101); + factoryAdmin_ = getActor(102); + + for (uint256 i = 0; i < total; i += 1) { + chainIds.push(i); + vm.prank(deployer_); + address depl = address(new MockThirdwebContract()); + deploymentAddresses.push(depl); + } + + vm.startPrank(factoryAdmin_); + _registry = new TWMultichainRegistry(forwarders()); + + _registry.grantRole(keccak256("OPERATOR_ROLE"), factory_); + + vm.stopPrank(); + } + + function test_interfaceId() public pure { + console2.logBytes4(type(IPluginMap).interfaceId); + } + + // ===== Functionality tests ===== + + /// @dev Test `add` + + function test_addFromFactory() public { + vm.startPrank(factory_); + for (uint256 i = 0; i < total; i += 1) { + _registry.add(deployer_, deploymentAddresses[i], chainIds[i], ""); + } + vm.stopPrank(); + + ITWMultichainRegistry.Deployment[] memory modules = _registry.getAll(deployer_); + + assertEq(modules.length, total); + assertEq(_registry.count(deployer_), total); + + for (uint256 i = 0; i < total; i += 1) { + assertEq(modules[i].deploymentAddress, deploymentAddresses[i]); + assertEq(modules[i].chainId, chainIds[i]); + } + + vm.prank(factory_); + _registry.add(deployer_, address(0x43), 111, ""); + + modules = _registry.getAll(deployer_); + assertEq(modules.length, total + 1); + assertEq(_registry.count(deployer_), total + 1); + } + + function test_addFromSelf() public { + vm.startPrank(deployer_); + for (uint256 i = 0; i < total; i += 1) { + _registry.add(deployer_, deploymentAddresses[i], chainIds[i], ""); + } + vm.stopPrank(); + + ITWMultichainRegistry.Deployment[] memory modules = _registry.getAll(deployer_); + + assertEq(modules.length, total); + assertEq(_registry.count(deployer_), total); + + for (uint256 i = 0; i < total; i += 1) { + assertEq(modules[i].deploymentAddress, deploymentAddresses[i]); + assertEq(modules[i].chainId, chainIds[i]); + } + + vm.prank(factory_); + _registry.add(deployer_, address(0x43), 111, ""); + + modules = _registry.getAll(deployer_); + assertEq(modules.length, total + 1); + assertEq(_registry.count(deployer_), total + 1); + } + + function test_add_emit_Added() public { + vm.expectEmit(true, true, true, true); + emit Added(deployer_, deploymentAddresses[0], chainIds[0], "uri"); + + vm.prank(factory_); + _registry.add(deployer_, deploymentAddresses[0], chainIds[0], "uri"); + + string memory uri = _registry.getMetadataUri(chainIds[0], deploymentAddresses[0]); + assertEq(uri, "uri"); + } + + // Test `remove` + + function setUp_remove() public { + vm.startPrank(factory_); + for (uint256 i = 0; i < total; i += 1) { + _registry.add(deployer_, deploymentAddresses[i], chainIds[i], ""); + } + vm.stopPrank(); + } + + // ===== Functionality tests ===== + function test_removeFromFactory() public { + setUp_remove(); + vm.prank(factory_); + _registry.remove(deployer_, deploymentAddresses[0], chainIds[0]); + + ITWMultichainRegistry.Deployment[] memory modules = _registry.getAll(deployer_); + assertEq(modules.length, total - 1); + + for (uint256 i = 0; i < total - 1; i += 1) { + assertEq(modules[i].deploymentAddress, deploymentAddresses[i + 1]); + assertEq(modules[i].chainId, chainIds[i + 1]); + } + } + + function test_removeFromSelf() public { + setUp_remove(); + vm.prank(factory_); + _registry.remove(deployer_, deploymentAddresses[0], chainIds[0]); + + ITWMultichainRegistry.Deployment[] memory modules = _registry.getAll(deployer_); + assertEq(modules.length, total - 1); + } + + function test_remove_revert_invalidCaller() public { + setUp_remove(); + address invalidCaller = address(0x123); + assertTrue(invalidCaller != factory_ || invalidCaller != deployer_); + + vm.expectRevert("not operator or deployer."); + + vm.prank(invalidCaller); + _registry.remove(deployer_, deploymentAddresses[0], chainIds[0]); + } + + function test_remove_revert_noModulesToRemove() public { + setUp_remove(); + address actor = getActor(1); + ITWMultichainRegistry.Deployment[] memory modules = _registry.getAll(actor); + assertEq(modules.length, 0); + + vm.expectRevert("failed to remove"); + + vm.prank(actor); + _registry.remove(actor, deploymentAddresses[0], chainIds[0]); + } + + function test_remove_revert_incorrectChainId() public { + setUp_remove(); + + vm.expectRevert("failed to remove"); + + vm.prank(deployer_); + _registry.remove(deployer_, deploymentAddresses[0], 12345); + } + + function test_remove_emit_Deleted() public { + setUp_remove(); + vm.expectEmit(true, true, true, true); + emit Deleted(deployer_, deploymentAddresses[0], chainIds[0]); + + vm.prank(deployer_); + _registry.remove(deployer_, deploymentAddresses[0], chainIds[0]); + } +} diff --git a/src/test/TWRegistry.t.sol b/src/test/TWRegistry.t.sol index 0f4288522..8d9a8c79f 100644 --- a/src/test/TWRegistry.t.sol +++ b/src/test/TWRegistry.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.11; // Test imports import "./utils/BaseTest.sol"; -import "contracts/TWRegistry.sol"; +import { TWRegistry } from "contracts/infra/TWRegistry.sol"; interface ITWRegistryData { event Added(address indexed deployer, address indexed moduleAddress); diff --git a/src/test/TieredDrop.t.sol b/src/test/TieredDrop.t.sol new file mode 100644 index 000000000..151d12ecb --- /dev/null +++ b/src/test/TieredDrop.t.sol @@ -0,0 +1,1103 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "./utils/BaseTest.sol"; + +import { TieredDrop } from "contracts/prebuilts/tiered-drop/TieredDrop.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract TieredDropTest is BaseTest { + using Strings for uint256; + + TieredDrop public tieredDrop; + + address internal dropAdmin; + address internal claimer; + + // Signature params + address internal deployerSigner; + bytes32 internal typehashGenericRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + // Lazy mint variables + uint256 internal quantityTier1 = 10; + string internal tier1 = "tier1"; + string internal baseURITier1 = "baseURI1/"; + string internal placeholderURITier1 = "placeholderURI1/"; + bytes internal keyTier1 = "tier1_key"; + + uint256 internal quantityTier2 = 20; + string internal tier2 = "tier2"; + string internal baseURITier2 = "baseURI2/"; + string internal placeholderURITier2 = "placeholderURI2/"; + bytes internal keyTier2 = "tier2_key"; + + uint256 internal quantityTier3 = 30; + string internal tier3 = "tier3"; + string internal baseURITier3 = "baseURI3/"; + string internal placeholderURITier3 = "placeholderURI3/"; + bytes internal keyTier3 = "tier3_key"; + + function setUp() public virtual override { + super.setUp(); + + dropAdmin = getActor(1); + claimer = getActor(2); + + // Deploy implementation. + address tieredDropImpl = address(new TieredDrop()); + + // Deploy proxy pointing to implementaion. + vm.prank(dropAdmin); + tieredDrop = TieredDrop( + address( + new TWProxy( + tieredDropImpl, + abi.encodeCall( + TieredDrop.initialize, + (dropAdmin, "Tiered Drop", "TD", "ipfs://", new address[](0), dropAdmin, dropAdmin, 0) + ) + ) + ) + ); + + // ====== signature params + + deployerSigner = signer; + vm.prank(dropAdmin); + tieredDrop.grantRole(keccak256("MINTER_ROLE"), deployerSigner); + + typehashGenericRequest = keccak256( + "GenericRequest(uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid,bytes data)" + ); + nameHash = keccak256(bytes("SignatureAction")); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256( + abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(tieredDrop)) + ); + + // ====== + } + + TieredDrop.GenericRequest internal claimRequest; + bytes internal claimSignature; + + uint256 internal nonce; + + function _setupClaimSignature(string[] memory _orderedTiers, uint256 _totalQuantity) internal { + claimRequest.validityStartTimestamp = 1000; + claimRequest.validityEndTimestamp = 2000; + claimRequest.uid = keccak256(abi.encodePacked(nonce)); + nonce += 1; + claimRequest.data = abi.encode( + _orderedTiers, + claimer, + address(0), + 0, + dropAdmin, + _totalQuantity, + 0, + NATIVE_TOKEN + ); + + bytes memory encodedRequest = abi.encode( + typehashGenericRequest, + claimRequest.validityStartTimestamp, + claimRequest.validityEndTimestamp, + claimRequest.uid, + keccak256(bytes(claimRequest.data)) + ); + + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, typedDataHash); + claimSignature = abi.encodePacked(r, s, v); + } + + //////////////////////////////////////////////// + // // + // lazyMintWithTier tests // + // // + //////////////////////////////////////////////// + + // function test_state_lazyMintWithTier() public { + // // Lazy mint tokens: 3 different tiers + // vm.startPrank(dropAdmin); + + // // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. + // tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); + // // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. + // tieredDrop.lazyMint(quantityTier2, baseURITier2, tier2, ""); + // // Tier 3: tokenIds assigned 30 -> 60 non-inclusive. + // tieredDrop.lazyMint(quantityTier3, baseURITier3, tier3, ""); + + // vm.stopPrank(); + + // TieredDrop.TierMetadata[] memory metadataForAllTiers = tieredDrop.getMetadataForAllTiers(); + // (TieredDrop.TokenRange[] memory tokens_1, string[] memory baseURIs_1) = ( + // metadataForAllTiers[0].ranges, + // metadataForAllTiers[0].baseURIs + // ); + // (TieredDrop.TokenRange[] memory tokens_2, string[] memory baseURIs_2) = ( + // metadataForAllTiers[1].ranges, + // metadataForAllTiers[1].baseURIs + // ); + // (TieredDrop.TokenRange[] memory tokens_3, string[] memory baseURIs_3) = ( + // metadataForAllTiers[2].ranges, + // metadataForAllTiers[2].baseURIs + // ); + + // uint256 cumulativeStart = 0; + + // TieredDrop.TokenRange memory range = tokens_1[0]; + // string memory baseURI = baseURIs_1[0]; + + // assertEq(range.startIdInclusive, cumulativeStart); + // assertEq(range.endIdNonInclusive, cumulativeStart + quantityTier1); + // assertEq(baseURI, baseURITier1); + + // cumulativeStart += quantityTier1; + + // range = tokens_2[0]; + // baseURI = baseURIs_2[0]; + + // assertEq(range.startIdInclusive, cumulativeStart); + // assertEq(range.endIdNonInclusive, cumulativeStart + quantityTier2); + // assertEq(baseURI, baseURITier2); + + // cumulativeStart += quantityTier2; + + // range = tokens_3[0]; + // baseURI = baseURIs_3[0]; + + // assertEq(range.startIdInclusive, cumulativeStart); + // assertEq(range.endIdNonInclusive, cumulativeStart + quantityTier3); + // assertEq(baseURI, baseURITier3); + // } + + // function test_state_lazyMintWithTier_sameTier() public { + // // Lazy mint tokens: 3 different tiers + // vm.startPrank(dropAdmin); + + // // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. + // tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); + // // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. + // tieredDrop.lazyMint(quantityTier2, baseURITier2, tier2, ""); + // // Tier 1 Again: tokenIds assigned 30 -> 60 non-inclusive. + // tieredDrop.lazyMint(quantityTier3, baseURITier3, tier1, ""); + + // TieredDrop.TierMetadata[] memory metadataForAllTiers = tieredDrop.getMetadataForAllTiers(); + // (TieredDrop.TokenRange[] memory tokens_1, string[] memory baseURIs_1) = ( + // metadataForAllTiers[0].ranges, + // metadataForAllTiers[0].baseURIs + // ); + // (TieredDrop.TokenRange[] memory tokens_2, string[] memory baseURIs_2) = ( + // metadataForAllTiers[1].ranges, + // metadataForAllTiers[1].baseURIs + // ); + + // vm.stopPrank(); + + // uint256 cumulativeStart = 0; + + // TieredDrop.TokenRange memory range = tokens_1[0]; + // string memory baseURI = baseURIs_1[0]; + + // assertEq(range.startIdInclusive, cumulativeStart); + // assertEq(range.endIdNonInclusive, cumulativeStart + quantityTier1); + // assertEq(baseURI, baseURITier1); + + // cumulativeStart += quantityTier1; + + // range = tokens_2[0]; + // baseURI = baseURIs_2[0]; + + // assertEq(range.startIdInclusive, cumulativeStart); + // assertEq(range.endIdNonInclusive, cumulativeStart + quantityTier2); + // assertEq(baseURI, baseURITier2); + + // cumulativeStart += quantityTier2; + + // range = tokens_1[1]; + // baseURI = baseURIs_1[1]; + + // assertEq(range.startIdInclusive, cumulativeStart); + // assertEq(range.endIdNonInclusive, cumulativeStart + quantityTier3); + // assertEq(baseURI, baseURITier3); + // } + + function test_revert_lazyMintWithTier_notMinterRole() public { + vm.expectRevert("Not authorized"); + tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); + } + + function test_revert_lazyMintWithTier_mintingZeroAmount() public { + vm.prank(dropAdmin); + vm.expectRevert("0 amt"); + tieredDrop.lazyMint(0, baseURITier1, tier1, ""); + } + + //////////////////////////////////////////////// + // // + // claimWithSignature tests // + // // + //////////////////////////////////////////////// + + function test_state_claimWithSignature() public { + // Lazy mint tokens: 3 different tiers + vm.startPrank(dropAdmin); + + // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. + tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); + // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. + tieredDrop.lazyMint(quantityTier2, baseURITier2, tier2, ""); + // Tier 3: tokenIds assigned 30 -> 60 non-inclusive. + tieredDrop.lazyMint(quantityTier3, baseURITier3, tier3, ""); + + vm.stopPrank(); + + /** + * Claim tokens. + * - Order of priority: [tier2, tier1] + * - Total quantity: 25. [20 from tier2, 5 from tier1] + */ + + string[] memory tiers = new string[](2); + tiers[0] = tier2; + tiers[1] = tier1; + + uint256 claimQuantity = 25; + + _setupClaimSignature(tiers, claimQuantity); + + assertEq(tieredDrop.hasRole(keccak256("MINTER_ROLE"), deployerSigner), true); + + vm.warp(claimRequest.validityStartTimestamp); + vm.prank(claimer); + tieredDrop.claimWithSignature(claimRequest, claimSignature); + + /** + * Check token URIs for tokens of tiers: + * - Tier 2: token IDs 0 -> 19 mapped one-to-one to metadata IDs 10 -> 29 + * - Tier 1: token IDs 20 -> 24 mapped one-to-one to metadata IDs 0 -> 4 + */ + + uint256 tier2Id = 10; + uint256 tier1Id = 0; + + for (uint256 i = 0; i < claimQuantity; i += 1) { + if (i < 20) { + assertEq(tieredDrop.tokenURI(i), string(abi.encodePacked(baseURITier2, tier2Id.toString()))); + tier2Id += 1; + } else { + assertEq(tieredDrop.tokenURI(i), string(abi.encodePacked(baseURITier1, tier1Id.toString()))); + tier1Id += 1; + } + } + } + + function test_revert_claimWithSignature_invalidEncoding() public { + // Lazy mint tokens: 3 different tiers + vm.startPrank(dropAdmin); + + // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. + tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); + // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. + tieredDrop.lazyMint(quantityTier2, baseURITier2, tier2, ""); + // Tier 3: tokenIds assigned 30 -> 60 non-inclusive. + tieredDrop.lazyMint(quantityTier3, baseURITier3, tier3, ""); + + vm.stopPrank(); + + /** + * Claim tokens. + * - Order of priority: [tier2, tier1] + * - Total quantity: 25. [20 from tier2, 5 from tier1] + */ + + string[] memory tiers = new string[](2); + tiers[0] = tier2; + tiers[1] = tier1; + + uint256 claimQuantity = 25; + + // Create data with invalid encoding. + claimRequest.data = abi.encode(1, ""); + _setupClaimSignature(tiers, claimQuantity); + + claimRequest.data = abi.encode(1, ""); + + assertEq(tieredDrop.hasRole(keccak256("MINTER_ROLE"), deployerSigner), true); + + vm.warp(claimRequest.validityStartTimestamp); + vm.prank(claimer); + vm.expectRevert(); + tieredDrop.claimWithSignature(claimRequest, claimSignature); + } + + function test_revert_claimWithSignature_mintingZeroQuantity() public { + // Lazy mint tokens: 3 different tiers + vm.startPrank(dropAdmin); + + // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. + tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); + // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. + tieredDrop.lazyMint(quantityTier2, baseURITier2, tier2, ""); + // Tier 3: tokenIds assigned 30 -> 60 non-inclusive. + tieredDrop.lazyMint(quantityTier3, baseURITier3, tier3, ""); + + vm.stopPrank(); + + /** + * Claim tokens. + * - Order of priority: [tier2, tier1] + * - Total quantity: 25. [20 from tier2, 5 from tier1] + */ + + string[] memory tiers = new string[](2); + tiers[0] = tier2; + tiers[1] = tier1; + + uint256 claimQuantity = 0; + + _setupClaimSignature(tiers, claimQuantity); + + assertEq(tieredDrop.hasRole(keccak256("MINTER_ROLE"), deployerSigner), true); + + vm.warp(claimRequest.validityStartTimestamp); + vm.prank(claimer); + vm.expectRevert("0 qty"); + tieredDrop.claimWithSignature(claimRequest, claimSignature); + } + + function test_revert_claimWithSignature_notEnoughLazyMintedTokens() public { + // Lazy mint tokens: 3 different tiers + vm.startPrank(dropAdmin); + + // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. + tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); + // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. + tieredDrop.lazyMint(quantityTier2, baseURITier2, tier2, ""); + // Tier 3: tokenIds assigned 30 -> 60 non-inclusive. + tieredDrop.lazyMint(quantityTier3, baseURITier3, tier3, ""); + + vm.stopPrank(); + + /** + * Claim tokens. + * - Order of priority: [tier2, tier1] + * - Total quantity: 25. [20 from tier2, 5 from tier1] + */ + + string[] memory tiers = new string[](2); + tiers[0] = tier2; + tiers[1] = tier1; + + uint256 claimQuantity = quantityTier1 + quantityTier2 + quantityTier3 + 1; + + _setupClaimSignature(tiers, claimQuantity); + + assertEq(tieredDrop.hasRole(keccak256("MINTER_ROLE"), deployerSigner), true); + + vm.warp(claimRequest.validityStartTimestamp); + vm.prank(claimer); + vm.expectRevert("!Tokens"); + tieredDrop.claimWithSignature(claimRequest, claimSignature); + } + + function test_revert_claimWithSignature_insufficientTokensInTiers() public { + // Lazy mint tokens: 3 different tiers + vm.startPrank(dropAdmin); + + // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. + tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); + // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. + tieredDrop.lazyMint(quantityTier2, baseURITier2, tier2, ""); + // Tier 3: tokenIds assigned 30 -> 60 non-inclusive. + tieredDrop.lazyMint(quantityTier3, baseURITier3, tier3, ""); + + vm.stopPrank(); + + /** + * Claim tokens. + * - Order of priority: [tier2, tier1] + * - Total quantity: 25. [20 from tier2, 5 from tier1] + */ + + string[] memory tiers = new string[](2); + tiers[0] = "non-exsitent tier 1"; + tiers[1] = "non-exsitent tier 2"; + + uint256 claimQuantity = 25; + + _setupClaimSignature(tiers, claimQuantity); + + assertEq(tieredDrop.hasRole(keccak256("MINTER_ROLE"), deployerSigner), true); + + vm.warp(claimRequest.validityStartTimestamp); + vm.prank(claimer); + vm.expectRevert("Insufficient tokens in tiers."); + tieredDrop.claimWithSignature(claimRequest, claimSignature); + } + + //////////////////////////////////////////////// + // // + // reveal tests // + // // + //////////////////////////////////////////////// + + function _getProvenanceHash(string memory _revealURI, bytes memory _key) private view returns (bytes32) { + return keccak256(abi.encodePacked(_revealURI, _key, block.chainid)); + } + + function test_state_revealWithScrambleOffset() public { + // Lazy mint tokens: 3 different tiers: with delayed reveal + bytes memory encryptedURITier1 = tieredDrop.encryptDecrypt(bytes(baseURITier1), keyTier1); + bytes memory encryptedURITier2 = tieredDrop.encryptDecrypt(bytes(baseURITier2), keyTier2); + bytes memory encryptedURITier3 = tieredDrop.encryptDecrypt(bytes(baseURITier3), keyTier3); + + vm.startPrank(dropAdmin); + + // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. + tieredDrop.lazyMint( + quantityTier1, + placeholderURITier1, + tier1, + abi.encode(encryptedURITier1, _getProvenanceHash(baseURITier1, keyTier1)) + ); + // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. + tieredDrop.lazyMint( + quantityTier2, + placeholderURITier2, + tier2, + abi.encode(encryptedURITier2, _getProvenanceHash(baseURITier2, keyTier2)) + ); + // Tier 3: tokenIds assigned 30 -> 60 non-inclusive. + tieredDrop.lazyMint( + quantityTier3, + placeholderURITier3, + tier3, + abi.encode(encryptedURITier3, _getProvenanceHash(baseURITier3, keyTier3)) + ); + + vm.stopPrank(); + + /** + * Claim tokens. + * - Order of priority: [tier2, tier1] + * - Total quantity: 25. [20 from tier2, 5 from tier1] + */ + + string[] memory tiers = new string[](2); + tiers[0] = tier2; + tiers[1] = tier1; + + uint256 claimQuantity = 25; + + _setupClaimSignature(tiers, claimQuantity); + + assertEq(tieredDrop.hasRole(keccak256("MINTER_ROLE"), deployerSigner), true); + + vm.warp(claimRequest.validityStartTimestamp); + vm.prank(claimer); + tieredDrop.claimWithSignature(claimRequest, claimSignature); + + /** + * Check token URIs for tokens of tiers: + * - Tier 2: token IDs 0 -> 19 mapped one-to-one to metadata IDs 10 -> 29 + * - Tier 1: token IDs 20 -> 24 mapped one-to-one to metadata IDs 0 -> 4 + */ + + uint256 tier2Id = 10; + uint256 tier1Id = 0; + + for (uint256 i = 0; i < claimQuantity; i += 1) { + // console.log(i); + if (i < 20) { + assertEq(tieredDrop.tokenURI(i), string(abi.encodePacked(placeholderURITier2, uint256(0).toString()))); + tier2Id += 1; + } else { + assertEq(tieredDrop.tokenURI(i), string(abi.encodePacked(placeholderURITier1, uint256(0).toString()))); + tier1Id += 1; + } + } + + // Reveal tokens. + vm.startPrank(dropAdmin); + tieredDrop.reveal(0, keyTier1); + tieredDrop.reveal(1, keyTier2); + tieredDrop.reveal(2, keyTier3); + + uint256 tier2IdStart = 10; + uint256 tier2IdEnd = 30; + + uint256 tier1IdStart = 0; + uint256 tier1IdEnd = 10; + + for (uint256 i = 0; i < claimQuantity; i += 1) { + bytes32 tokenURIHash = keccak256(abi.encodePacked(tieredDrop.tokenURI(i))); + bool detected = false; + + if (i < 20) { + for (uint256 j = tier2IdStart; j < tier2IdEnd; j += 1) { + bytes32 expectedURIHash = keccak256(abi.encodePacked(baseURITier2, j.toString())); + + if (tokenURIHash == expectedURIHash) { + detected = true; + } + + if (detected) { + break; + } + } + } else { + for (uint256 k = tier1IdStart; k < tier1IdEnd; k += 1) { + bytes32 expectedURIHash = keccak256(abi.encodePacked(baseURITier1, k.toString())); + + if (tokenURIHash == expectedURIHash) { + detected = true; + } + + if (detected) { + break; + } + } + } + + assertEq(detected, true); + } + } + + event URIReveal(uint256 tokenId, string uri); + + //////////////////////////////////////////////// + // // + // getTokensInTierLen tests // + // // + //////////////////////////////////////////////// + + function test_state_getTokensInTierLen() public { + // Lazy mint tokens: 3 different tiers + vm.startPrank(dropAdmin); + + // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. + tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); + // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. + tieredDrop.lazyMint(quantityTier2, baseURITier2, tier2, ""); + // Tier 3: tokenIds assigned 30 -> 60 non-inclusive. + tieredDrop.lazyMint(quantityTier3, baseURITier3, tier3, ""); + + vm.stopPrank(); + + /** + * Claim tokens. + * - Order of priority: [tier2, tier1] + * - Total quantity: 25. [20 from tier2, 5 from tier1] + */ + + string[] memory tiers = new string[](2); + tiers[0] = tier2; + tiers[1] = tier1; + + uint256 claimQuantity = 25; + + _setupClaimSignature(tiers, claimQuantity); + + vm.warp(claimRequest.validityStartTimestamp); + + vm.prank(claimer); + tieredDrop.claimWithSignature(claimRequest, claimSignature); + + assertEq(tieredDrop.getTokensInTierLen(), 2); + + for (uint256 i = 0; i < 5; i += 1) { + _setupClaimSignature(tiers, 1); + + vm.warp(claimRequest.validityStartTimestamp); + + vm.prank(claimer); + tieredDrop.claimWithSignature(claimRequest, claimSignature); + } + + assertEq(tieredDrop.getTokensInTierLen(), 7); + } + + //////////////////////////////////////////////// + // // + // getTokensInTier tests // + // // + //////////////////////////////////////////////// + + function test_state_getTokensInTier() public { + // Lazy mint tokens: 3 different tiers + vm.startPrank(dropAdmin); + + // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. + tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); + // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. + tieredDrop.lazyMint(quantityTier2, baseURITier2, tier2, ""); + // Tier 3: tokenIds assigned 30 -> 60 non-inclusive. + tieredDrop.lazyMint(quantityTier3, baseURITier3, tier3, ""); + + vm.stopPrank(); + + /** + * Claim tokens. + * - Order of priority: [tier2, tier1] + * - Total quantity: 25. [20 from tier2, 5 from tier1] + */ + + string[] memory tiers = new string[](2); + tiers[0] = tier2; + tiers[1] = tier1; + + uint256 claimQuantity = 25; + + _setupClaimSignature(tiers, claimQuantity); + + vm.warp(claimRequest.validityStartTimestamp); + + vm.prank(claimer); + tieredDrop.claimWithSignature(claimRequest, claimSignature); + + TieredDrop.TokenRange[] memory rangesTier1 = tieredDrop.getTokensInTier(tier1, 0, 2); + assertEq(rangesTier1.length, 1); + + TieredDrop.TokenRange[] memory rangesTier2 = tieredDrop.getTokensInTier(tier2, 0, 2); + assertEq(rangesTier2.length, 1); + + assertEq(rangesTier1[0].startIdInclusive, 20); + assertEq(rangesTier1[0].endIdNonInclusive, 25); + assertEq(rangesTier2[0].startIdInclusive, 0); + assertEq(rangesTier2[0].endIdNonInclusive, 20); + } + + //////////////////////////////////////////////// + // // + // getTierForToken tests // + // // + //////////////////////////////////////////////// + + function test_state_getTierForToken() public { + // Lazy mint tokens: 3 different tiers + vm.startPrank(dropAdmin); + + // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. + tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); + // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. + tieredDrop.lazyMint(quantityTier2, baseURITier2, tier2, ""); + // Tier 3: tokenIds assigned 30 -> 60 non-inclusive. + tieredDrop.lazyMint(quantityTier3, baseURITier3, tier3, ""); + + vm.stopPrank(); + + /** + * Claim tokens. + * - Order of priority: [tier2, tier1] + * - Total quantity: 25. [20 from tier2, 5 from tier1] + */ + + string[] memory tiers = new string[](2); + tiers[0] = tier2; + tiers[1] = tier1; + + uint256 claimQuantity = 25; + + _setupClaimSignature(tiers, claimQuantity); + + vm.warp(claimRequest.validityStartTimestamp); + + vm.prank(claimer); + tieredDrop.claimWithSignature(claimRequest, claimSignature); + + /** + * Check token URIs for tokens of tiers: + * - Tier 2: token IDs 0 -> 19 mapped one-to-one to metadata IDs 10 -> 29 + * - Tier 1: token IDs 20 -> 24 mapped one-to-one to metadata IDs 0 -> 4 + */ + + uint256 tier2Id = 10; + uint256 tier1Id = 0; + + for (uint256 i = 0; i < claimQuantity; i += 1) { + if (i < 20) { + string memory tierForToken = tieredDrop.getTierForToken(i); + assertEq(tierForToken, tier2); + + tier2Id += 1; + } else { + string memory tierForToken = tieredDrop.getTierForToken(i); + assertEq(tierForToken, tier1); + + tier1Id += 1; + } + } + } + + //////////////////////////////////////////////// + // // + // getMetadataForAllTiers tests // + // // + //////////////////////////////////////////////// + + // function test_state_getMetadataForAllTiers() public { + // // Lazy mint tokens: 3 different tiers + // vm.startPrank(dropAdmin); + + // // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. + // tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); + // // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. + // tieredDrop.lazyMint(quantityTier2, baseURITier2, tier2, ""); + // // Tier 3: tokenIds assigned 30 -> 60 non-inclusive. + // tieredDrop.lazyMint(quantityTier3, baseURITier3, tier3, ""); + + // vm.stopPrank(); + + // TieredDrop.TierMetadata[] memory metadataForAllTiers = tieredDrop.getMetadataForAllTiers(); + + // // Tier 1 + // assertEq(metadataForAllTiers[0].tier, tier1); + + // TieredDrop.TokenRange[] memory ranges1 = metadataForAllTiers[0].ranges; + // assertEq(ranges1.length, 1); + // assertEq(ranges1[0].startIdInclusive, 0); + // assertEq(ranges1[0].endIdNonInclusive, 10); + + // string[] memory baseURIs1 = metadataForAllTiers[0].baseURIs; + // assertEq(baseURIs1.length, 1); + // assertEq(baseURIs1[0], baseURITier1); + + // // Tier 2 + // assertEq(metadataForAllTiers[1].tier, tier2); + + // TieredDrop.TokenRange[] memory ranges2 = metadataForAllTiers[1].ranges; + // assertEq(ranges2.length, 1); + // assertEq(ranges2[0].startIdInclusive, 10); + // assertEq(ranges2[0].endIdNonInclusive, 30); + + // string[] memory baseURIs2 = metadataForAllTiers[1].baseURIs; + // assertEq(baseURIs2.length, 1); + // assertEq(baseURIs2[0], baseURITier2); + + // // Tier 3 + // assertEq(metadataForAllTiers[2].tier, tier3); + + // TieredDrop.TokenRange[] memory ranges3 = metadataForAllTiers[2].ranges; + // assertEq(ranges3.length, 1); + // assertEq(ranges3[0].startIdInclusive, 30); + // assertEq(ranges3[0].endIdNonInclusive, 60); + + // string[] memory baseURIs3 = metadataForAllTiers[2].baseURIs; + // assertEq(baseURIs3.length, 1); + // assertEq(baseURIs3[0], baseURITier3); + // } + + //////////////////////////////////////////////// + // // + // audit tests // + // // + //////////////////////////////////////////////// + + function test_state_claimWithSignature_IssueH1() public { + // Lazy mint tokens: 3 different tiers + vm.startPrank(dropAdmin); + + // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. + tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); + // Tier 2: tokenIds assigned 10 -> 20 non-inclusive. + tieredDrop.lazyMint(10, baseURITier2, tier2, ""); + // Tier 3: tokenIds assigned 20 -> 50 non-inclusive. + tieredDrop.lazyMint(quantityTier3, baseURITier3, tier3, ""); + + // Tier 2: tokenIds assigned 50 -> 60 non-inclusive. + tieredDrop.lazyMint(quantityTier2 - 10, baseURITier2, tier2, ""); + + vm.stopPrank(); + + string[] memory tiers = new string[](2); + tiers[0] = tier2; + tiers[1] = tier1; + + uint256 claimQuantity = 25; + + _setupClaimSignature(tiers, claimQuantity); + + assertEq(tieredDrop.hasRole(keccak256("MINTER_ROLE"), deployerSigner), true); + + vm.warp(claimRequest.validityStartTimestamp); + vm.prank(claimer); + tieredDrop.claimWithSignature(claimRequest, claimSignature); + assertEq(tieredDrop.balanceOf(claimer), claimQuantity); + + for (uint256 i = 0; i < claimQuantity; i += 1) { + // Outputs: + // Checking 0 baseURI2/10 + // Checking 1 baseURI2/11 + // Checking 2 baseURI2/12 + // Checking 3 baseURI2/13 + // Checking 4 baseURI2/14 + // Checking 5 baseURI2/15 + // Checking 6 baseURI2/16 + // Checking 7 baseURI2/17 + // Checking 8 baseURI2/18 + // Checking 9 baseURI2/19 + // Checking 10 baseURI3/50 + // Checking 11 baseURI3/51 + // Checking 12 baseURI3/52 + // Checking 13 baseURI3/53 + // Checking 14 baseURI3/54 + // Checking 15 baseURI3/55 + // Checking 16 baseURI3/56 + // Checking 17 baseURI3/57 + // Checking 18 baseURI3/58 + // Checking 19 baseURI3/59 + // Checking 20 baseURI1/0 + // Checking 21 baseURI1/1 + // Checking 22 baseURI1/2 + // Checking 23 baseURI1/3 + // Checking 24 baseURI1/4 + console.log("Checking", i, tieredDrop.tokenURI(i)); + } + } + + function test_state_claimWithSignature_IssueH1_2() public { + // Lazy mint tokens: 3 different tiers + vm.startPrank(dropAdmin); + + // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. + tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); + // Tier 2: tokenIds assigned 10 -> 20 non-inclusive. + tieredDrop.lazyMint(1, baseURITier2, tier2, ""); // 10 -> 11 + tieredDrop.lazyMint(9, baseURITier2, tier2, ""); // 11 -> 20 + // Tier 3: tokenIds assigned 20 -> 50 non-inclusive. + tieredDrop.lazyMint(quantityTier3, baseURITier3, tier3, ""); + + // Tier 2: tokenIds assigned 50 -> 60 non-inclusive. + tieredDrop.lazyMint(quantityTier2 - 10, baseURITier2, tier2, ""); + + vm.stopPrank(); + + string[] memory tiers = new string[](2); + tiers[0] = tier2; + tiers[1] = tier1; + + uint256[3] memory claimQuantities = [uint256(1), uint256(3), uint256(21)]; + uint256 claimedCount = 0; + for (uint256 loop = 0; loop < 3; loop++) { + uint256 claimQuantity = claimQuantities[loop]; + uint256 offset = claimedCount; + + _setupClaimSignature(tiers, claimQuantity); + + assertEq(tieredDrop.hasRole(keccak256("MINTER_ROLE"), deployerSigner), true); + + vm.warp(claimRequest.validityStartTimestamp); + vm.prank(claimer); + tieredDrop.claimWithSignature(claimRequest, claimSignature); + + claimedCount += claimQuantity; + assertEq(tieredDrop.balanceOf(claimer), claimedCount); + + for (uint256 i = offset; i < claimQuantity + (offset); i += 1) { + // Outputs: + // Checking 0 baseURI2/10 + // Checking 1 baseURI2/11 + // Checking 2 baseURI2/12 + // Checking 3 baseURI2/13 + // Checking 4 baseURI2/14 + // Checking 5 baseURI2/15 + // Checking 6 baseURI2/16 + // Checking 7 baseURI2/17 + // Checking 8 baseURI2/18 + // Checking 9 baseURI2/19 + // Checking 10 baseURI3/50 + // Checking 11 baseURI3/51 + // Checking 12 baseURI3/52 + // Checking 13 baseURI3/53 + // Checking 14 baseURI3/54 + // Checking 15 baseURI3/55 + // Checking 16 baseURI3/56 + // Checking 17 baseURI3/57 + // Checking 18 baseURI3/58 + // Checking 19 baseURI3/59 + // Checking 20 baseURI1/0 + // Checking 21 baseURI1/1 + // Checking 22 baseURI1/2 + // Checking 23 baseURI1/3 + // Checking 24 baseURI1/4 + console.log("Checking", i, tieredDrop.tokenURI(i)); + } + } + } +} + +// contract TieredDropBechmarkTest is BaseTest { +// using Strings for uint256; + +// TieredDrop public tieredDrop; + +// address internal dropAdmin; +// address internal claimer; + +// // Signature params +// address internal deployerSigner; +// bytes32 internal typehashGenericRequest; +// bytes32 internal nameHash; +// bytes32 internal versionHash; +// bytes32 internal typehashEip712; +// bytes32 internal domainSeparator; + +// // Lazy mint variables +// uint256 internal quantityTier1 = 10; +// string internal tier1 = "tier1"; +// string internal baseURITier1 = "baseURI1/"; +// string internal placeholderURITier1 = "placeholderURI1/"; +// bytes internal keyTier1 = "tier1_key"; + +// uint256 internal quantityTier2 = 20; +// string internal tier2 = "tier2"; +// string internal baseURITier2 = "baseURI2/"; +// string internal placeholderURITier2 = "placeholderURI2/"; +// bytes internal keyTier2 = "tier2_key"; + +// uint256 internal quantityTier3 = 30; +// string internal tier3 = "tier3"; +// string internal baseURITier3 = "baseURI3/"; +// string internal placeholderURITier3 = "placeholderURI3/"; +// bytes internal keyTier3 = "tier3_key"; + +// function setUp() public virtual override { +// super.setUp(); + +// dropAdmin = getActor(1); +// claimer = getActor(2); + +// // Deploy implementation. +// address tieredDropImpl = address(new TieredDrop()); + +// // Deploy proxy pointing to implementaion. +// vm.prank(dropAdmin); +// tieredDrop = TieredDrop( +// address( +// new TWProxy( +// tieredDropImpl, +// abi.encodeCall( +// TieredDrop.initialize, +// (dropAdmin, "Tiered Drop", "TD", "ipfs://", new address[](0), dropAdmin, dropAdmin, 0) +// ) +// ) +// ) +// ); + +// // ====== signature params + +// deployerSigner = signer; +// vm.prank(dropAdmin); +// tieredDrop.grantRole(keccak256("MINTER_ROLE"), deployerSigner); + +// typehashGenericRequest = keccak256( +// "GenericRequest(uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid,bytes data)" +// ); +// nameHash = keccak256(bytes("SignatureAction")); +// versionHash = keccak256(bytes("1")); +// typehashEip712 = keccak256( +// "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" +// ); +// domainSeparator = keccak256( +// abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(tieredDrop)) +// ); + +// // ====== + +// // Lazy mint tokens: 3 different tiers +// vm.startPrank(dropAdmin); + +// // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. +// tieredDrop.lazyMint(totalQty, baseURITier1, tier1, ""); +// // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. +// tieredDrop.lazyMint(totalQty, baseURITier2, tier2, ""); + +// vm.stopPrank(); + +// /** +// * Claim tokens. +// * - Order of priority: [tier2, tier1] +// * - Total quantity: 25. [20 from tier2, 5 from tier1] +// */ + +// string[] memory tiers = new string[](2); +// tiers[0] = tier2; +// tiers[1] = tier1; + +// uint256 claimQuantity = totalQty; + +// for (uint256 i = 0; i < claimQuantity; i += 1) { +// _setupClaimSignature(tiers, 1); + +// vm.warp(claimRequest.validityStartTimestamp); + +// vm.prank(claimer); +// tieredDrop.claimWithSignature(claimRequest, claimSignature); +// } +// } + +// TieredDrop.GenericRequest internal claimRequest; +// bytes internal claimSignature; + +// uint256 internal nonce; + +// function _setupClaimSignature(string[] memory _orderedTiers, uint256 _totalQuantity) internal { +// claimRequest.validityStartTimestamp = 1000; +// claimRequest.validityEndTimestamp = 2000; +// claimRequest.uid = keccak256(abi.encodePacked(nonce)); +// nonce += 1; +// claimRequest.data = abi.encode( +// _orderedTiers, +// claimer, +// address(0), +// 0, +// dropAdmin, +// _totalQuantity, +// 0, +// NATIVE_TOKEN +// ); + +// bytes memory encodedRequest = abi.encode( +// typehashGenericRequest, +// claimRequest.validityStartTimestamp, +// claimRequest.validityEndTimestamp, +// claimRequest.uid, +// keccak256(bytes(claimRequest.data)) +// ); + +// bytes32 structHash = keccak256(encodedRequest); +// bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + +// (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, typedDataHash); +// claimSignature = abi.encodePacked(r, s, v); +// } + +// // What does it take to exhaust the 550mil RPC view fn gas limit ? + +// // 10_000: 67 mil gas (67,536,754) +// uint256 internal totalQty = 10_000; + +// function test_banchmark_getTokensInTier() public view { +// tieredDrop.getTokensInTier(tier1, 0, totalQty); +// } + +// function test_banchmark_getTokensInTier_ten() public view { +// tieredDrop.getTokensInTier(tier1, 0, 10); +// } + +// function test_banchmark_getTokensInTier_hundred() public view { +// tieredDrop.getTokensInTier(tier1, 0, 100); +// } +// } diff --git a/src/test/airdrop/Airdrop.t.sol b/src/test/airdrop/Airdrop.t.sol new file mode 100644 index 000000000..0d0ebbf60 --- /dev/null +++ b/src/test/airdrop/Airdrop.t.sol @@ -0,0 +1,1065 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { Airdrop, SafeTransferLib, ECDSA } from "contracts/prebuilts/airdrop/Airdrop.sol"; + +// Test imports +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import "../utils/BaseTest.sol"; + +contract MockSmartWallet { + using ECDSA for bytes32; + + bytes4 private constant EIP1271_MAGIC_VALUE = 0x1626ba7e; + address private admin; + + constructor(address _admin) { + admin = _admin; + } + + function isValidSignature(bytes32 _hash, bytes memory _signature) public view returns (bytes4) { + if (_hash.recover(_signature) == admin) { + return EIP1271_MAGIC_VALUE; + } + } + + function onERC721Received(address, address, uint256, bytes memory) external pure returns (bytes4) { + return this.onERC721Received.selector; + } + + function onERC1155Received(address, address, uint256, uint256, bytes memory) external pure returns (bytes4) { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address, + address, + uint256[] memory, + uint256[] memory, + bytes memory + ) external pure returns (bytes4) { + return this.onERC1155BatchReceived.selector; + } +} + +contract AirdropTest is BaseTest { + Airdrop internal airdrop; + MockSmartWallet internal mockSmartWallet; + + bytes32 private constant CONTENT_TYPEHASH_ERC20 = + keccak256("AirdropContentERC20(address recipient,uint256 amount)"); + bytes32 private constant REQUEST_TYPEHASH_ERC20 = + keccak256( + "AirdropRequestERC20(bytes32 uid,address tokenAddress,uint256 expirationTimestamp,AirdropContentERC20[] contents)AirdropContentERC20(address recipient,uint256 amount)" + ); + + bytes32 private constant CONTENT_TYPEHASH_ERC721 = + keccak256("AirdropContentERC721(address recipient,uint256 tokenId)"); + bytes32 private constant REQUEST_TYPEHASH_ERC721 = + keccak256( + "AirdropRequestERC721(bytes32 uid,address tokenAddress,uint256 expirationTimestamp,AirdropContentERC721[] contents)AirdropContentERC721(address recipient,uint256 tokenId)" + ); + + bytes32 private constant CONTENT_TYPEHASH_ERC1155 = + keccak256("AirdropContentERC1155(address recipient,uint256 tokenId,uint256 amount)"); + bytes32 private constant REQUEST_TYPEHASH_ERC1155 = + keccak256( + "AirdropRequestERC1155(bytes32 uid,address tokenAddress,uint256 expirationTimestamp,AirdropContentERC1155[] contents)AirdropContentERC1155(address recipient,uint256 tokenId,uint256 amount)" + ); + + bytes32 private constant NAME_HASH = keccak256(bytes("Airdrop")); + bytes32 private constant VERSION_HASH = keccak256(bytes("1")); + bytes32 private constant TYPE_HASH_EIP712 = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + bytes32 internal domainSeparator; + + function setUp() public override { + super.setUp(); + + address impl = address(new Airdrop()); + + airdrop = Airdrop(payable(address(new TWProxy(impl, abi.encodeCall(Airdrop.initialize, (signer, "")))))); + + domainSeparator = keccak256( + abi.encode(TYPE_HASH_EIP712, NAME_HASH, VERSION_HASH, block.chainid, address(airdrop)) + ); + + mockSmartWallet = new MockSmartWallet(signer); + } + + function _getContentsERC20(uint256 length) internal pure returns (Airdrop.AirdropContentERC20[] memory contents) { + contents = new Airdrop.AirdropContentERC20[](length); + for (uint256 i = 0; i < length; i++) { + contents[i].recipient = address(uint160(i + 10)); + contents[i].amount = i + 10; + } + } + + function _getContentsERC721(uint256 length) internal pure returns (Airdrop.AirdropContentERC721[] memory contents) { + contents = new Airdrop.AirdropContentERC721[](length); + for (uint256 i = 0; i < length; i++) { + contents[i].recipient = address(uint160(i + 10)); + contents[i].tokenId = i; + } + } + + function _getContentsERC1155( + uint256 length + ) internal pure returns (Airdrop.AirdropContentERC1155[] memory contents) { + contents = new Airdrop.AirdropContentERC1155[](length); + for (uint256 i = 0; i < length; i++) { + contents[i].recipient = address(uint160(i + 10)); + contents[i].tokenId = 0; + contents[i].amount = i + 10; + } + } + + function _signReqERC20( + Airdrop.AirdropRequestERC20 memory req, + uint256 privateKey + ) internal view returns (bytes memory signature) { + bytes32[] memory contentHashes = new bytes32[](req.contents.length); + for (uint i = 0; i < req.contents.length; i++) { + contentHashes[i] = keccak256( + abi.encode(CONTENT_TYPEHASH_ERC20, req.contents[i].recipient, req.contents[i].amount) + ); + } + bytes32 contentHash = keccak256(abi.encodePacked(contentHashes)); + + bytes memory dataToHash; + { + dataToHash = abi.encode( + REQUEST_TYPEHASH_ERC20, + req.uid, + req.tokenAddress, + req.expirationTimestamp, + contentHash + ); + } + + { + bytes32 _structHash = keccak256(dataToHash); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, _structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, typedDataHash); + + signature = abi.encodePacked(r, s, v); + } + } + + function _signReqERC721( + Airdrop.AirdropRequestERC721 memory req, + uint256 privateKey + ) internal view returns (bytes memory signature) { + bytes32[] memory contentHashes = new bytes32[](req.contents.length); + for (uint i = 0; i < req.contents.length; i++) { + contentHashes[i] = keccak256( + abi.encode(CONTENT_TYPEHASH_ERC721, req.contents[i].recipient, req.contents[i].tokenId) + ); + } + bytes32 contentHash = keccak256(abi.encodePacked(contentHashes)); + + bytes memory dataToHash; + { + dataToHash = abi.encode( + REQUEST_TYPEHASH_ERC721, + req.uid, + req.tokenAddress, + req.expirationTimestamp, + contentHash + ); + } + + { + bytes32 _structHash = keccak256(dataToHash); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, _structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, typedDataHash); + + signature = abi.encodePacked(r, s, v); + } + } + + function _signReqERC1155( + Airdrop.AirdropRequestERC1155 memory req, + uint256 privateKey + ) internal view returns (bytes memory signature) { + bytes32[] memory contentHashes = new bytes32[](req.contents.length); + for (uint i = 0; i < req.contents.length; i++) { + contentHashes[i] = keccak256( + abi.encode( + CONTENT_TYPEHASH_ERC1155, + req.contents[i].recipient, + req.contents[i].tokenId, + req.contents[i].amount + ) + ); + } + bytes32 contentHash = keccak256(abi.encodePacked(contentHashes)); + + bytes memory dataToHash; + { + dataToHash = abi.encode( + REQUEST_TYPEHASH_ERC1155, + req.uid, + req.tokenAddress, + req.expirationTimestamp, + contentHash + ); + } + + { + bytes32 _structHash = keccak256(dataToHash); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, _structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, typedDataHash); + + signature = abi.encodePacked(r, s, v); + } + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: Airdrop Push ERC20 + //////////////////////////////////////////////////////////////*/ + + function test_state_airdropPush_erc20() public { + erc20.mint(signer, 100 ether); + vm.prank(signer); + erc20.approve(address(airdrop), 100 ether); + + Airdrop.AirdropContentERC20[] memory contents = _getContentsERC20(10); + + vm.prank(signer); + + airdrop.airdropERC20(address(erc20), contents); + + uint256 totalAmount; + for (uint256 i = 0; i < contents.length; i++) { + totalAmount += contents[i].amount; + assertEq(erc20.balanceOf(contents[i].recipient), contents[i].amount); + } + assertEq(erc20.balanceOf(signer), 100 ether - totalAmount); + } + + function test_state_airdropPush_nativeToken() public { + vm.deal(signer, 100 ether); + + Airdrop.AirdropContentERC20[] memory contents = _getContentsERC20(10); + + uint256 totalAmount; + for (uint256 i = 0; i < contents.length; i++) { + totalAmount += contents[i].amount; + assertEq(contents[i].recipient.balance, 0); + } + + vm.prank(signer); + airdrop.airdropNativeToken{ value: totalAmount }(contents); + + for (uint256 i = 0; i < contents.length; i++) { + assertEq(contents[i].recipient.balance, contents[i].amount); + } + + assertEq(signer.balance, 100 ether - totalAmount); + } + + function test_revert_airdropPush_nativeToken() public { + vm.deal(signer, 100 ether); + + Airdrop.AirdropContentERC20[] memory contents = _getContentsERC20(10); + + vm.prank(signer); + vm.expectRevert(abi.encodeWithSelector(SafeTransferLib.ETHTransferFailed.selector)); + airdrop.airdropNativeToken{ value: 0 }(contents); + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: Airdrop Signature ERC20 + //////////////////////////////////////////////////////////////*/ + + function test_state_airdropSignature_erc20() public { + erc20.mint(signer, 100 ether); + vm.prank(signer); + erc20.approve(address(airdrop), 100 ether); + + Airdrop.AirdropContentERC20[] memory contents = _getContentsERC20(10); + Airdrop.AirdropRequestERC20 memory req = Airdrop.AirdropRequestERC20({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc20), + expirationTimestamp: 1000, + contents: contents + }); + bytes memory signature = _signReqERC20(req, privateKey); + + vm.prank(signer); + + airdrop.airdropERC20WithSignature(req, signature); + + uint256 totalAmount; + for (uint256 i = 0; i < contents.length; i++) { + totalAmount += contents[i].amount; + assertEq(erc20.balanceOf(contents[i].recipient), contents[i].amount); + } + assertEq(erc20.balanceOf(signer), 100 ether - totalAmount); + } + + function test_state_airdropSignature_erc20_eip1271() public { + // set mockSmartWallet as contract owner + vm.prank(signer); + airdrop.setOwner(address(mockSmartWallet)); + + // mint tokens to mockSmartWallet + erc20.mint(address(mockSmartWallet), 100 ether); + vm.prank(address(mockSmartWallet)); + erc20.approve(address(airdrop), 100 ether); + + Airdrop.AirdropContentERC20[] memory contents = _getContentsERC20(10); + Airdrop.AirdropRequestERC20 memory req = Airdrop.AirdropRequestERC20({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc20), + expirationTimestamp: 1000, + contents: contents + }); + + // sign with original EOA signer private key + bytes memory signature = _signReqERC20(req, privateKey); + + airdrop.airdropERC20WithSignature(req, signature); + + uint256 totalAmount; + for (uint256 i = 0; i < contents.length; i++) { + totalAmount += contents[i].amount; + assertEq(erc20.balanceOf(contents[i].recipient), contents[i].amount); + } + assertEq(erc20.balanceOf(address(mockSmartWallet)), 100 ether - totalAmount); + } + + function test_revert_airdropSignature_erc20_eip1271_invalidSignature() public { + // set mockSmartWallet as contract owner + vm.prank(signer); + airdrop.setOwner(address(mockSmartWallet)); + + // mint tokens to mockSmartWallet + erc20.mint(address(mockSmartWallet), 100 ether); + vm.prank(address(mockSmartWallet)); + erc20.approve(address(airdrop), 100 ether); + + Airdrop.AirdropContentERC20[] memory contents = _getContentsERC20(10); + Airdrop.AirdropRequestERC20 memory req = Airdrop.AirdropRequestERC20({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc20), + expirationTimestamp: 1000, + contents: contents + }); + + // sign with random private key + bytes memory signature = _signReqERC20(req, 123); + + vm.expectRevert(abi.encodeWithSelector(Airdrop.AirdropRequestInvalidSigner.selector)); + airdrop.airdropERC20WithSignature(req, signature); + } + + function test_revert_airdropSignature_erc20_expired() public { + erc20.mint(signer, 100 ether); + vm.prank(signer); + erc20.approve(address(airdrop), 100 ether); + + Airdrop.AirdropContentERC20[] memory contents = _getContentsERC20(10); + Airdrop.AirdropRequestERC20 memory req = Airdrop.AirdropRequestERC20({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc20), + expirationTimestamp: 1000, + contents: contents + }); + bytes memory signature = _signReqERC20(req, privateKey); + + vm.warp(1001); + + vm.prank(signer); + vm.expectRevert(abi.encodeWithSelector(Airdrop.AirdropRequestExpired.selector, req.expirationTimestamp)); + airdrop.airdropERC20WithSignature(req, signature); + } + + function test_revert_airdropSignature_erc20_alreadyProcessed() public { + erc20.mint(signer, 100 ether); + vm.prank(signer); + erc20.approve(address(airdrop), 100 ether); + + Airdrop.AirdropContentERC20[] memory contents = _getContentsERC20(10); + Airdrop.AirdropRequestERC20 memory req = Airdrop.AirdropRequestERC20({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc20), + expirationTimestamp: 1000, + contents: contents + }); + bytes memory signature = _signReqERC20(req, privateKey); + + vm.prank(signer); + + airdrop.airdropERC20WithSignature(req, signature); + + // try re-sending same request/signature + vm.expectRevert(abi.encodeWithSelector(Airdrop.AirdropRequestAlreadyProcessed.selector)); + airdrop.airdropERC20WithSignature(req, signature); + } + + function test_revert_airdropSignature_erc20_invalidSigner() public { + erc20.mint(signer, 100 ether); + vm.prank(signer); + erc20.approve(address(airdrop), 100 ether); + + Airdrop.AirdropContentERC20[] memory contents = _getContentsERC20(10); + Airdrop.AirdropRequestERC20 memory req = Airdrop.AirdropRequestERC20({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc20), + expirationTimestamp: 1000, + contents: contents + }); + bytes memory signature = _signReqERC20(req, 123); + + vm.prank(signer); + vm.expectRevert(abi.encodeWithSelector(Airdrop.AirdropRequestInvalidSigner.selector)); + airdrop.airdropERC20WithSignature(req, signature); + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: Airdrop Claim ERC20 + //////////////////////////////////////////////////////////////*/ + + function test_state_airdropClaim_erc20() public { + erc20.mint(signer, 100 ether); + vm.prank(signer); + erc20.approve(address(airdrop), 100 ether); + + string[] memory inputs = new string[](3); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRootAirdrop.ts"; + inputs[2] = Strings.toString(uint256(5)); + bytes memory result = vm.ffi(inputs); + bytes32 root = abi.decode(result, (bytes32)); + + // set merkle root + vm.prank(signer); + airdrop.setMerkleRoot(address(erc20), root, true); + + // generate proof + inputs[1] = "src/test/scripts/getProofAirdrop.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 quantity = 5; + + airdrop.claimERC20(address(erc20), receiver, quantity, proofs); + + assertEq(erc20.balanceOf(receiver), quantity); + assertEq(erc20.balanceOf(signer), 100 ether - quantity); + } + + function test_revert_airdropClaim_erc20_alreadyClaimed() public { + erc20.mint(signer, 100 ether); + vm.prank(signer); + erc20.approve(address(airdrop), 100 ether); + + string[] memory inputs = new string[](3); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRootAirdrop.ts"; + inputs[2] = Strings.toString(uint256(5)); + bytes memory result = vm.ffi(inputs); + bytes32 root = abi.decode(result, (bytes32)); + + // set merkle root + vm.prank(signer); + airdrop.setMerkleRoot(address(erc20), root, true); + + // generate proof + inputs[1] = "src/test/scripts/getProofAirdrop.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 quantity = 5; + + airdrop.claimERC20(address(erc20), receiver, quantity, proofs); + + // revert when claiming again + vm.expectRevert(abi.encodeWithSelector(Airdrop.AirdropAlreadyClaimed.selector)); + airdrop.claimERC20(address(erc20), receiver, quantity, proofs); + } + + function test_revert_airdropClaim_erc20_noMerkleRoot() public { + erc20.mint(signer, 100 ether); + vm.prank(signer); + erc20.approve(address(airdrop), 100 ether); + + bytes32[] memory proofs; + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 quantity = 5; + + // revert when claiming again + vm.expectRevert(abi.encodeWithSelector(Airdrop.AirdropNoMerkleRoot.selector)); + airdrop.claimERC20(address(erc20), receiver, quantity, proofs); + } + + function test_revert_airdropClaim_erc20_invalidProof() public { + erc20.mint(signer, 100 ether); + vm.prank(signer); + erc20.approve(address(airdrop), 100 ether); + + string[] memory inputs = new string[](3); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRootAirdrop.ts"; + inputs[2] = Strings.toString(uint256(5)); + bytes memory result = vm.ffi(inputs); + bytes32 root = abi.decode(result, (bytes32)); + + // set merkle root + vm.prank(signer); + airdrop.setMerkleRoot(address(erc20), root, true); + + // generate proof + inputs[1] = "src/test/scripts/getProofAirdrop.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + address receiver = address(0x12345); + uint256 quantity = 5; + + vm.expectRevert(abi.encodeWithSelector(Airdrop.AirdropInvalidProof.selector)); + airdrop.claimERC20(address(erc20), receiver, quantity, proofs); + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: Airdrop Push ERC721 + //////////////////////////////////////////////////////////////*/ + + function test_state_airdropPush_erc721() public { + erc721.mint(signer, 100); + vm.prank(signer); + erc721.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC721[] memory contents = _getContentsERC721(10); + + vm.prank(signer); + + airdrop.airdropERC721(address(erc721), contents); + + for (uint256 i = 0; i < contents.length; i++) { + assertEq(erc721.ownerOf(contents[i].tokenId), contents[i].recipient); + } + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: Airdrop Signature ERC721 + //////////////////////////////////////////////////////////////*/ + + function test_state_airdropSignature_erc721() public { + erc721.mint(signer, 1000); + vm.prank(signer); + erc721.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC721[] memory contents = _getContentsERC721(10); + Airdrop.AirdropRequestERC721 memory req = Airdrop.AirdropRequestERC721({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc721), + expirationTimestamp: 1000, + contents: contents + }); + bytes memory signature = _signReqERC721(req, privateKey); + + vm.prank(signer); + + airdrop.airdropERC721WithSignature(req, signature); + + for (uint256 i = 0; i < contents.length; i++) { + assertEq(erc721.ownerOf(contents[i].tokenId), contents[i].recipient); + } + } + + function test_state_airdropSignature_erc721_eip1271() public { + // set mockSmartWallet as contract owner + vm.prank(signer); + airdrop.setOwner(address(mockSmartWallet)); + + // mint tokens to mockSmartWallet + erc721.mint(address(mockSmartWallet), 1000); + vm.prank(address(mockSmartWallet)); + erc721.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC721[] memory contents = _getContentsERC721(10); + Airdrop.AirdropRequestERC721 memory req = Airdrop.AirdropRequestERC721({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc721), + expirationTimestamp: 1000, + contents: contents + }); + + // sign with original EOA signer private key + bytes memory signature = _signReqERC721(req, privateKey); + + airdrop.airdropERC721WithSignature(req, signature); + + for (uint256 i = 0; i < contents.length; i++) { + assertEq(erc721.ownerOf(contents[i].tokenId), contents[i].recipient); + } + } + + function test_revert_airdropSignature_erc721_eip1271_invalidSignature() public { + // set mockSmartWallet as contract owner + vm.prank(signer); + airdrop.setOwner(address(mockSmartWallet)); + + // mint tokens to mockSmartWallet + erc721.mint(address(mockSmartWallet), 1000); + vm.prank(address(mockSmartWallet)); + erc721.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC721[] memory contents = _getContentsERC721(10); + Airdrop.AirdropRequestERC721 memory req = Airdrop.AirdropRequestERC721({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc721), + expirationTimestamp: 1000, + contents: contents + }); + + // sign with random private key + bytes memory signature = _signReqERC721(req, 123); + + vm.expectRevert(abi.encodeWithSelector(Airdrop.AirdropRequestInvalidSigner.selector)); + airdrop.airdropERC721WithSignature(req, signature); + } + + function test_revert_airdropSignature_erc721_expired() public { + erc721.mint(signer, 1000); + vm.prank(signer); + erc721.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC721[] memory contents = _getContentsERC721(10); + Airdrop.AirdropRequestERC721 memory req = Airdrop.AirdropRequestERC721({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc721), + expirationTimestamp: 1000, + contents: contents + }); + bytes memory signature = _signReqERC721(req, privateKey); + + vm.warp(1001); + + vm.prank(signer); + vm.expectRevert(abi.encodeWithSelector(Airdrop.AirdropRequestExpired.selector, req.expirationTimestamp)); + airdrop.airdropERC721WithSignature(req, signature); + } + + function test_revert_airdropSignature_erc721_alreadyProcessed() public { + erc721.mint(signer, 1000); + vm.prank(signer); + erc721.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC721[] memory contents = _getContentsERC721(10); + Airdrop.AirdropRequestERC721 memory req = Airdrop.AirdropRequestERC721({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc721), + expirationTimestamp: 1000, + contents: contents + }); + bytes memory signature = _signReqERC721(req, privateKey); + + vm.prank(signer); + airdrop.airdropERC721WithSignature(req, signature); + + // send it again + vm.expectRevert(abi.encodeWithSelector(Airdrop.AirdropRequestAlreadyProcessed.selector)); + airdrop.airdropERC721WithSignature(req, signature); + } + + function test_revert_airdropSignature_erc721_invalidSigner() public { + erc721.mint(signer, 1000); + vm.prank(signer); + erc721.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC721[] memory contents = _getContentsERC721(10); + Airdrop.AirdropRequestERC721 memory req = Airdrop.AirdropRequestERC721({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc721), + expirationTimestamp: 1000, + contents: contents + }); + bytes memory signature = _signReqERC721(req, 123); + + vm.expectRevert(abi.encodeWithSelector(Airdrop.AirdropRequestInvalidSigner.selector)); + airdrop.airdropERC721WithSignature(req, signature); + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: Airdrop Claim ERC721 + //////////////////////////////////////////////////////////////*/ + + function test_state_airdropClaim_erc721() public { + erc721.mint(signer, 100); + vm.prank(signer); + erc721.setApprovalForAll(address(airdrop), true); + + string[] memory inputs = new string[](3); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRootAirdrop.ts"; + inputs[2] = Strings.toString(uint256(5)); + bytes memory result = vm.ffi(inputs); + bytes32 root = abi.decode(result, (bytes32)); + + // set merkle root + vm.prank(signer); + airdrop.setMerkleRoot(address(erc721), root, true); + + // generate proof + inputs[1] = "src/test/scripts/getProofAirdrop.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 tokenId = 5; + + airdrop.claimERC721(address(erc721), receiver, tokenId, proofs); + + assertEq(erc721.ownerOf(tokenId), receiver); + } + + function test_revert_airdropClaim_erc721_alreadyClaimed() public { + erc721.mint(signer, 100); + vm.prank(signer); + erc721.setApprovalForAll(address(airdrop), true); + + string[] memory inputs = new string[](3); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRootAirdrop.ts"; + inputs[2] = Strings.toString(uint256(5)); + bytes memory result = vm.ffi(inputs); + bytes32 root = abi.decode(result, (bytes32)); + + // set merkle root + vm.prank(signer); + airdrop.setMerkleRoot(address(erc721), root, true); + + // generate proof + inputs[1] = "src/test/scripts/getProofAirdrop.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 tokenId = 5; + + airdrop.claimERC721(address(erc721), receiver, tokenId, proofs); + + // revert when claiming again + vm.expectRevert(abi.encodeWithSelector(Airdrop.AirdropAlreadyClaimed.selector)); + airdrop.claimERC721(address(erc721), receiver, tokenId, proofs); + } + + function test_revert_airdropClaim_erc721_noMerkleRoot() public { + erc721.mint(signer, 100); + vm.prank(signer); + erc721.setApprovalForAll(address(airdrop), true); + + bytes32[] memory proofs; + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 tokenId = 5; + + vm.expectRevert(abi.encodeWithSelector(Airdrop.AirdropNoMerkleRoot.selector)); + airdrop.claimERC721(address(erc721), receiver, tokenId, proofs); + } + + function test_revert_airdropClaim_erc721_invalidProof() public { + erc721.mint(signer, 100); + vm.prank(signer); + erc721.setApprovalForAll(address(airdrop), true); + + string[] memory inputs = new string[](3); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRootAirdrop.ts"; + inputs[2] = Strings.toString(uint256(5)); + bytes memory result = vm.ffi(inputs); + bytes32 root = abi.decode(result, (bytes32)); + + // set merkle root + vm.prank(signer); + airdrop.setMerkleRoot(address(erc721), root, true); + + // generate proof + inputs[1] = "src/test/scripts/getProofAirdrop.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + address receiver = address(0x12345); + uint256 tokenId = 5; + + vm.expectRevert(abi.encodeWithSelector(Airdrop.AirdropInvalidProof.selector)); + airdrop.claimERC721(address(erc721), receiver, tokenId, proofs); + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: Airdrop Push ERC1155 + //////////////////////////////////////////////////////////////*/ + + function test_state_airdropPush_erc1155() public { + erc1155.mint(signer, 0, 100 ether); + vm.prank(signer); + erc1155.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC1155[] memory contents = _getContentsERC1155(10); + + vm.prank(signer); + + airdrop.airdropERC1155(address(erc1155), contents); + + for (uint256 i = 0; i < contents.length; i++) { + assertEq(erc1155.balanceOf(contents[i].recipient, contents[i].tokenId), contents[i].amount); + } + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: Airdrop Signature ERC1155 + //////////////////////////////////////////////////////////////*/ + + function test_state_airdropSignature_erc115() public { + erc1155.mint(signer, 0, 100 ether); + vm.prank(signer); + erc1155.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC1155[] memory contents = _getContentsERC1155(10); + Airdrop.AirdropRequestERC1155 memory req = Airdrop.AirdropRequestERC1155({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc1155), + expirationTimestamp: 1000, + contents: contents + }); + bytes memory signature = _signReqERC1155(req, privateKey); + + vm.prank(signer); + + airdrop.airdropERC1155WithSignature(req, signature); + + for (uint256 i = 0; i < contents.length; i++) { + assertEq(erc1155.balanceOf(contents[i].recipient, contents[i].tokenId), contents[i].amount); + } + } + + function test_state_airdropSignature_erc1155_eip1271() public { + // set mockSmartWallet as contract owner + vm.prank(signer); + airdrop.setOwner(address(mockSmartWallet)); + + // mint tokens to mockSmartWallet + erc1155.mint(address(mockSmartWallet), 0, 100 ether); + vm.prank(address(mockSmartWallet)); + erc1155.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC1155[] memory contents = _getContentsERC1155(10); + Airdrop.AirdropRequestERC1155 memory req = Airdrop.AirdropRequestERC1155({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc1155), + expirationTimestamp: 1000, + contents: contents + }); + + // sign with original EOA signer private key + bytes memory signature = _signReqERC1155(req, privateKey); + + airdrop.airdropERC1155WithSignature(req, signature); + + for (uint256 i = 0; i < contents.length; i++) { + assertEq(erc1155.balanceOf(contents[i].recipient, contents[i].tokenId), contents[i].amount); + } + } + + function test_revert_airdropSignature_erc1155_eip1271_invalidSignature() public { + // set mockSmartWallet as contract owner + vm.prank(signer); + airdrop.setOwner(address(mockSmartWallet)); + + // mint tokens to mockSmartWallet + erc1155.mint(address(mockSmartWallet), 0, 100 ether); + vm.prank(address(mockSmartWallet)); + erc1155.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC1155[] memory contents = _getContentsERC1155(10); + Airdrop.AirdropRequestERC1155 memory req = Airdrop.AirdropRequestERC1155({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc1155), + expirationTimestamp: 1000, + contents: contents + }); + + // sign with random private key + bytes memory signature = _signReqERC1155(req, 123); + + vm.expectRevert(abi.encodeWithSelector(Airdrop.AirdropRequestInvalidSigner.selector)); + airdrop.airdropERC1155WithSignature(req, signature); + } + + function test_revert_airdropSignature_erc115_expired() public { + erc1155.mint(signer, 0, 100 ether); + vm.prank(signer); + erc1155.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC1155[] memory contents = _getContentsERC1155(10); + Airdrop.AirdropRequestERC1155 memory req = Airdrop.AirdropRequestERC1155({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc1155), + expirationTimestamp: 1000, + contents: contents + }); + bytes memory signature = _signReqERC1155(req, privateKey); + + vm.warp(1001); + + vm.expectRevert(abi.encodeWithSelector(Airdrop.AirdropRequestExpired.selector, req.expirationTimestamp)); + airdrop.airdropERC1155WithSignature(req, signature); + } + + function test_revert_airdropSignature_erc115_alreadyProcessed() public { + erc1155.mint(signer, 0, 100 ether); + vm.prank(signer); + erc1155.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC1155[] memory contents = _getContentsERC1155(10); + Airdrop.AirdropRequestERC1155 memory req = Airdrop.AirdropRequestERC1155({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc1155), + expirationTimestamp: 1000, + contents: contents + }); + bytes memory signature = _signReqERC1155(req, privateKey); + + airdrop.airdropERC1155WithSignature(req, signature); + + // send it again + vm.expectRevert(abi.encodeWithSelector(Airdrop.AirdropRequestAlreadyProcessed.selector)); + airdrop.airdropERC1155WithSignature(req, signature); + } + + function test_revert_airdropSignature_erc115_invalidSigner() public { + erc1155.mint(signer, 0, 100 ether); + vm.prank(signer); + erc1155.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC1155[] memory contents = _getContentsERC1155(10); + Airdrop.AirdropRequestERC1155 memory req = Airdrop.AirdropRequestERC1155({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc1155), + expirationTimestamp: 1000, + contents: contents + }); + bytes memory signature = _signReqERC1155(req, 123); + + vm.expectRevert(abi.encodeWithSelector(Airdrop.AirdropRequestInvalidSigner.selector)); + airdrop.airdropERC1155WithSignature(req, signature); + } + /*/////////////////////////////////////////////////////////////// + Benchmark: Airdrop Claim ERC1155 + //////////////////////////////////////////////////////////////*/ + + function test_state_airdropClaim_erc1155() public { + erc1155.mint(signer, 0, 100 ether); + vm.prank(signer); + erc1155.setApprovalForAll(address(airdrop), true); + + string[] memory inputs = new string[](4); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRootAirdrop1155.ts"; + inputs[2] = Strings.toString(uint256(0)); + inputs[3] = Strings.toString(uint256(5)); + bytes memory result = vm.ffi(inputs); + bytes32 root = abi.decode(result, (bytes32)); + + // set merkle root + vm.prank(signer); + airdrop.setMerkleRoot(address(erc1155), root, true); + + // generate proof + inputs[1] = "src/test/scripts/getProofAirdrop1155.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 quantity = 5; + + airdrop.claimERC1155(address(erc1155), receiver, 0, quantity, proofs); + + assertEq(erc1155.balanceOf(receiver, 0), quantity); + assertEq(erc1155.balanceOf(signer, 0), 100 ether - quantity); + } + + function test_revert_airdropClaim_erc1155_alreadyClaimed() public { + erc1155.mint(signer, 0, 100 ether); + vm.prank(signer); + erc1155.setApprovalForAll(address(airdrop), true); + + string[] memory inputs = new string[](4); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRootAirdrop1155.ts"; + inputs[2] = Strings.toString(uint256(0)); + inputs[3] = Strings.toString(uint256(5)); + bytes memory result = vm.ffi(inputs); + bytes32 root = abi.decode(result, (bytes32)); + + // set merkle root + vm.prank(signer); + airdrop.setMerkleRoot(address(erc1155), root, true); + + // generate proof + inputs[1] = "src/test/scripts/getProofAirdrop1155.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 quantity = 5; + + airdrop.claimERC1155(address(erc1155), receiver, 0, quantity, proofs); + + // revert when claiming again + vm.expectRevert(abi.encodeWithSelector(Airdrop.AirdropAlreadyClaimed.selector)); + airdrop.claimERC1155(address(erc1155), receiver, 0, quantity, proofs); + } + + function test_revert_airdropClaim_erc1155_noMerkleRoot() public { + erc1155.mint(signer, 0, 100 ether); + vm.prank(signer); + erc1155.setApprovalForAll(address(airdrop), true); + + // generate proof + bytes32[] memory proofs; + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 quantity = 5; + + vm.expectRevert(abi.encodeWithSelector(Airdrop.AirdropNoMerkleRoot.selector)); + airdrop.claimERC1155(address(erc1155), receiver, 0, quantity, proofs); + } + + function test_revert_airdropClaim_erc1155_invalidProof() public { + erc1155.mint(signer, 0, 100 ether); + vm.prank(signer); + erc1155.setApprovalForAll(address(airdrop), true); + + string[] memory inputs = new string[](4); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRootAirdrop1155.ts"; + inputs[2] = Strings.toString(uint256(0)); + inputs[3] = Strings.toString(uint256(5)); + bytes memory result = vm.ffi(inputs); + bytes32 root = abi.decode(result, (bytes32)); + + // set merkle root + vm.prank(signer); + airdrop.setMerkleRoot(address(erc1155), root, true); + + // generate proof + inputs[1] = "src/test/scripts/getProofAirdrop1155.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + address receiver = address(0x12345); + uint256 quantity = 5; + + vm.expectRevert(abi.encodeWithSelector(Airdrop.AirdropInvalidProof.selector)); + airdrop.claimERC1155(address(erc1155), receiver, 0, quantity, proofs); + } +} diff --git a/src/test/airdrop/AirdropERC1155.t.sol b/src/test/airdrop/AirdropERC1155.t.sol new file mode 100644 index 000000000..161e9d364 --- /dev/null +++ b/src/test/airdrop/AirdropERC1155.t.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { AirdropERC1155, IAirdropERC1155 } from "contracts/prebuilts/unaudited/airdrop/AirdropERC1155.sol"; + +// Test imports +import { Wallet } from "../utils/Wallet.sol"; +import "../utils/BaseTest.sol"; + +contract AirdropERC1155Test is BaseTest { + AirdropERC1155 internal drop; + + Wallet internal tokenOwner; + + IAirdropERC1155.AirdropContent[] internal _contentsOne; + IAirdropERC1155.AirdropContent[] internal _contentsTwo; + + uint256 countOne; + uint256 countTwo; + + function setUp() public override { + super.setUp(); + + drop = AirdropERC1155(getContract("AirdropERC1155")); + + tokenOwner = getWallet(); + + erc1155.mint(address(tokenOwner), 0, 1000); + erc1155.mint(address(tokenOwner), 1, 2000); + erc1155.mint(address(tokenOwner), 2, 3000); + erc1155.mint(address(tokenOwner), 3, 4000); + erc1155.mint(address(tokenOwner), 4, 5000); + + tokenOwner.setApprovalForAllERC1155(address(erc1155), address(drop), true); + + countOne = 1000; + countTwo = 200; + + for (uint256 i = 0; i < countOne; i++) { + _contentsOne.push( + IAirdropERC1155.AirdropContent({ recipient: getActor(uint160(i)), tokenId: i % 5, amount: 5 }) + ); + } + + for (uint256 i = countOne; i < countOne + countTwo; i++) { + _contentsTwo.push( + IAirdropERC1155.AirdropContent({ recipient: getActor(uint160(i)), tokenId: i % 5, amount: 5 }) + ); + } + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: stateless airdrop + //////////////////////////////////////////////////////////////*/ + + function test_state_airdrop() public { + vm.prank(deployer); + drop.airdropERC1155(address(erc1155), address(tokenOwner), _contentsOne); + + for (uint256 i = 0; i < countOne; i++) { + assertEq(erc1155.balanceOf(_contentsOne[i].recipient, i % 5), 5); + } + + assertEq(erc1155.balanceOf(address(tokenOwner), 0), 0); + assertEq(erc1155.balanceOf(address(tokenOwner), 1), 1000); + assertEq(erc1155.balanceOf(address(tokenOwner), 2), 2000); + assertEq(erc1155.balanceOf(address(tokenOwner), 3), 3000); + assertEq(erc1155.balanceOf(address(tokenOwner), 4), 4000); + } + + function test_revert_airdrop_notOwner() public { + vm.prank(address(25)); + vm.expectRevert("Not authorized."); + drop.airdropERC1155(address(erc1155), address(tokenOwner), _contentsOne); + } + + function test_revert_airdrop_notApproved() public { + tokenOwner.setApprovalForAllERC1155(address(erc1155), address(drop), false); + + vm.startPrank(deployer); + vm.expectRevert("Not balance or approved"); + drop.airdropERC1155(address(erc1155), address(tokenOwner), _contentsOne); + vm.stopPrank(); + } +} + +contract AirdropERC1155GasTest is BaseTest { + AirdropERC1155 internal drop; + + Wallet internal tokenOwner; + + function setUp() public override { + super.setUp(); + + drop = AirdropERC1155(getContract("AirdropERC1155")); + + tokenOwner = getWallet(); + + erc1155.mint(address(tokenOwner), 0, 1000); + + tokenOwner.setApprovalForAllERC1155(address(erc1155), address(drop), true); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: gas benchmarks, etc. + //////////////////////////////////////////////////////////////*/ + + function test_safeTransferFrom_toEOA() public { + vm.prank(address(tokenOwner)); + erc1155.safeTransferFrom(address(tokenOwner), address(0x123), 0, 10, ""); + } + + function test_safeTransferFrom_toContract() public { + vm.prank(address(tokenOwner)); + erc1155.safeTransferFrom(address(tokenOwner), address(this), 0, 10, ""); + } + + function test_safeTransferFrom_toEOA_gasOverride() public { + vm.prank(address(tokenOwner)); + console.log(gasleft()); + erc1155.safeTransferFrom{ gas: 100_000 }(address(tokenOwner), address(this), 0, 10, ""); + console.log(gasleft()); + } + + function test_safeTransferFrom_toContract_gasOverride() public { + vm.prank(address(tokenOwner)); + console.log(gasleft()); + erc1155.safeTransferFrom{ gas: 100_000 }(address(tokenOwner), address(this), 0, 10, ""); + console.log(gasleft()); + } + + function onERC1155Received(address, address, uint256, uint256, bytes calldata) external pure returns (bytes4) { + return this.onERC1155Received.selector; + } +} diff --git a/src/test/airdrop/AirdropERC1155Claimable.t.sol b/src/test/airdrop/AirdropERC1155Claimable.t.sol new file mode 100644 index 000000000..4ce582d16 --- /dev/null +++ b/src/test/airdrop/AirdropERC1155Claimable.t.sol @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "contracts/prebuilts/unaudited/airdrop/AirdropERC1155Claimable.sol"; + +// Test imports +import { Wallet } from "../utils/Wallet.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { BaseTest } from "../utils/BaseTest.sol"; +import { Strings } from "contracts/lib/Strings.sol"; + +contract AirdropERC1155ClaimableTest is BaseTest { + address public implementation; + AirdropERC1155Claimable internal drop; + + function setUp() public override { + super.setUp(); + + address implementation = address(new AirdropERC1155Claimable()); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + drop = AirdropERC1155Claimable( + address( + new TWProxy( + implementation, + abi.encodeCall( + AirdropERC1155Claimable.initialize, + ( + forwarders(), + address(airdropTokenOwner), + address(erc1155), + _airdropTokenIdsERC1155, + _airdropAmountsERC1155, + 1000, + _airdropWalletClaimCountERC1155, + _airdropMerkleRootERC1155 + ) + ) + ) + ) + ); + + erc1155.mint(address(airdropTokenOwner), 0, 100); + erc1155.mint(address(airdropTokenOwner), 1, 100); + erc1155.mint(address(airdropTokenOwner), 2, 100); + erc1155.mint(address(airdropTokenOwner), 3, 100); + erc1155.mint(address(airdropTokenOwner), 4, 100); + + airdropTokenOwner.setApprovalForAllERC1155(address(erc1155), address(drop), true); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `claim` -- for allowlisted claimers + //////////////////////////////////////////////////////////////*/ + + function test_state_claim_allowlistedClaimer() public { + string[] memory inputs = new string[](3); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/getProofAirdrop.ts"; + inputs[2] = Strings.toString(uint256(5)); + + bytes memory result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 quantity = 2; + uint256 id = 0; + + uint256 _availableAmount = drop.availableAmount(id); + + vm.prank(receiver); + drop.claim(receiver, quantity, id, proofs, 5); + + assertEq(erc1155.balanceOf(receiver, id), quantity); + assertEq(drop.supplyClaimedByWallet(id, receiver), quantity); + assertEq(drop.availableAmount(id), _availableAmount - quantity); + } + + function test_revert_claim_notInAllowlist_invalidQty() public { + string[] memory inputs = new string[](3); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/getProofAirdrop.ts"; + inputs[2] = Strings.toString(uint256(4)); // generate proof with incorrect amount + + bytes memory result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 quantity = 2; + uint256 id = 0; + + vm.prank(receiver); + vm.expectRevert("invalid quantity."); + drop.claim(receiver, quantity, id, proofs, 5); + } + + function test_revert_claim_allowlistedClaimer_proofClaimed() public { + string[] memory inputs = new string[](3); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/getProofAirdrop.ts"; + inputs[2] = Strings.toString(uint256(5)); + + bytes memory result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 quantity = 2; + uint256 id = 0; + + vm.prank(receiver); + drop.claim(receiver, quantity, id, proofs, 5); + + quantity = 3; + + vm.prank(receiver); + drop.claim(receiver, quantity, id, proofs, 5); + + vm.prank(receiver); + vm.expectRevert("invalid quantity."); + drop.claim(receiver, quantity, id, proofs, 5); + } + + function test_state_claim_allowlistedClaimer_invalidQuantity() public { + string[] memory inputs = new string[](3); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/getProofAirdrop.ts"; + inputs[2] = Strings.toString(uint256(5)); + + bytes memory result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 quantity = 6; + uint256 id = 0; + + vm.prank(receiver); + vm.expectRevert("invalid quantity."); + drop.claim(receiver, quantity, id, proofs, 5); + } + + function test_revert_claim_allowlistedClaimer_airdropExpired() public { + string[] memory inputs = new string[](3); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/getProofAirdrop.ts"; + inputs[2] = Strings.toString(uint256(5)); + + bytes memory result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + vm.warp(1001); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 quantity = 5; + uint256 id = 0; + + vm.prank(receiver); + vm.expectRevert("airdrop expired."); + drop.claim(receiver, quantity, id, proofs, 5); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `claim` -- for open claiming + //////////////////////////////////////////////////////////////*/ + + function test_state_claim_nonAllowlistedClaimer() public { + address receiver = address(0x123); + uint256 quantity = 1; + bytes32[] memory proofs; + uint256 id = 0; + + uint256 _availableAmount = drop.availableAmount(id); + + vm.prank(receiver); + drop.claim(receiver, quantity, id, proofs, 0); + + assertEq(erc1155.balanceOf(receiver, id), quantity); + assertEq(drop.supplyClaimedByWallet(id, receiver), quantity); + assertEq(drop.availableAmount(id), _availableAmount - quantity); + } + + function test_revert_claim_nonAllowlistedClaimer_invalidQuantity() public { + address receiver = address(0x123); + uint256 quantity = 2; + bytes32[] memory proofs; + uint256 id = 0; + + vm.prank(receiver); + vm.expectRevert("invalid quantity."); + drop.claim(receiver, quantity, id, proofs, 0); + } + + function test_revert_claim_nonAllowlistedClaimer_exceedsAvailable() public { + uint256 id = 0; + uint256 _availableAmount = drop.availableAmount(id); + bytes32[] memory proofs; + + uint256 i = 0; + for (; i < _availableAmount; i++) { + address receiver = getActor(uint160(i)); + vm.prank(receiver); + drop.claim(receiver, 1, id, proofs, 0); + } + + address receiver = getActor(uint160(i)); + vm.prank(receiver); + vm.expectRevert("exceeds available tokens."); + drop.claim(receiver, 1, id, proofs, 0); + } +} diff --git a/src/test/airdrop/AirdropERC20.t.sol b/src/test/airdrop/AirdropERC20.t.sol new file mode 100644 index 000000000..54d0c4997 --- /dev/null +++ b/src/test/airdrop/AirdropERC20.t.sol @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { AirdropERC20, IAirdropERC20 } from "contracts/prebuilts/unaudited/airdrop/AirdropERC20.sol"; +import { CurrencyTransferLib } from "contracts/lib/CurrencyTransferLib.sol"; + +// Test imports +import { Wallet } from "../utils/Wallet.sol"; +import "../utils/BaseTest.sol"; + +import "../mocks/MockERC20NonCompliant.sol"; + +contract AirdropERC20Test is BaseTest { + AirdropERC20 internal drop; + + Wallet internal tokenOwner; + + IAirdropERC20.AirdropContent[] internal _contentsOne; + IAirdropERC20.AirdropContent[] internal _contentsTwo; + + uint256 countOne; + uint256 countTwo; + + function setUp() public override { + super.setUp(); + + drop = AirdropERC20(getContract("AirdropERC20")); + + tokenOwner = getWallet(); + + erc20.mint(address(tokenOwner), 10_000 ether); + tokenOwner.setAllowanceERC20(address(erc20), address(drop), type(uint256).max); + + countOne = 1000; + countTwo = 200; + + for (uint256 i = 0; i < countOne; i++) { + _contentsOne.push(IAirdropERC20.AirdropContent({ recipient: getActor(uint160(i)), amount: 10 ether })); + } + + for (uint256 i = countOne; i < countOne + countTwo; i++) { + _contentsTwo.push(IAirdropERC20.AirdropContent({ recipient: getActor(uint160(i)), amount: 10 ether })); + } + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: stateless airdrop + //////////////////////////////////////////////////////////////*/ + + function test_state_airdrop() public { + vm.prank(deployer); + drop.airdropERC20(address(erc20), address(tokenOwner), _contentsOne); + + for (uint256 i = 0; i < countOne; i++) { + assertEq(erc20.balanceOf(_contentsOne[i].recipient), _contentsOne[i].amount); + } + assertEq(erc20.balanceOf(address(tokenOwner)), 0); + } + + function test_revert_airdrop_insufficientValue() public { + vm.prank(deployer); + vm.expectRevert("Insufficient native token amount"); + drop.airdropERC20(CurrencyTransferLib.NATIVE_TOKEN, address(tokenOwner), _contentsOne); + } + + function test_revert_airdrop_notOwner() public { + vm.startPrank(address(25)); + vm.expectRevert("Not authorized."); + drop.airdropERC20(address(erc20), address(tokenOwner), _contentsOne); + vm.stopPrank(); + } + + function test_revert_airdrop_notApproved() public { + tokenOwner.setAllowanceERC20(address(erc20), address(drop), 0); + + vm.startPrank(deployer); + vm.expectRevert("Not balance or allowance"); + drop.airdropERC20(address(erc20), address(tokenOwner), _contentsOne); + vm.stopPrank(); + } +} + +contract AirdropERC20AuditTest is BaseTest { + AirdropERC20 internal drop; + + Wallet internal tokenOwner; + + IAirdropERC20.AirdropContent[] internal _contentsOne; + IAirdropERC20.AirdropContent[] internal _contentsTwo; + + uint256 countOne; + uint256 countTwo; + + MockERC20NonCompliant public erc20_nonCompliant; + + function setUp() public override { + super.setUp(); + + erc20_nonCompliant = new MockERC20NonCompliant(); + drop = AirdropERC20(getContract("AirdropERC20")); + + tokenOwner = getWallet(); + + erc20_nonCompliant.mint(address(tokenOwner), 10_000 ether); + tokenOwner.setAllowanceERC20(address(erc20_nonCompliant), address(drop), type(uint256).max); + + countOne = 1000; + countTwo = 200; + + for (uint256 i = 0; i < countOne; i++) { + _contentsOne.push(IAirdropERC20.AirdropContent({ recipient: getActor(uint160(i)), amount: 10 ether })); + } + + for (uint256 i = countOne; i < countOne + countTwo; i++) { + _contentsTwo.push(IAirdropERC20.AirdropContent({ recipient: getActor(uint160(i)), amount: 10 ether })); + } + } + + function test_process_payments_with_non_compliant_token() public { + vm.prank(deployer); + drop.airdropERC20(address(erc20_nonCompliant), address(tokenOwner), _contentsOne); + + // check balances after airdrop + for (uint256 i = 0; i < countOne; i++) { + assertEq(erc20_nonCompliant.balanceOf(_contentsOne[i].recipient), _contentsOne[i].amount); + } + assertEq(erc20_nonCompliant.balanceOf(address(tokenOwner)), 0); + } +} + +contract AirdropERC20GasTest is BaseTest { + AirdropERC20 internal drop; + + Wallet internal tokenOwner; + + function setUp() public override { + super.setUp(); + + drop = AirdropERC20(getContract("AirdropERC20")); + + tokenOwner = getWallet(); + + erc20.mint(address(tokenOwner), 10_000 ether); + tokenOwner.setAllowanceERC20(address(erc20), address(drop), type(uint256).max); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: gas benchmarks, etc. + //////////////////////////////////////////////////////////////*/ + + function test_transferNativeToken_toEOA() public { + vm.prank(address(tokenOwner)); + (bool success, bytes memory data) = address(0x123).call{ value: 1 ether }(""); + + // Silence warning: Return value of low-level calls not used. + (success, data) = (success, data); + } + + function test_transferNativeToken_toContract() public { + vm.prank(address(tokenOwner)); + (bool success, bytes memory data) = address(this).call{ value: 1 ether }(""); + + // Silence warning: Return value of low-level calls not used. + (success, data) = (success, data); + } + + function test_transferNativeToken_toEOA_gasOverride() public { + vm.prank(address(tokenOwner)); + console.log(gasleft()); + (bool success, bytes memory data) = address(0x123).call{ value: 1 ether, gas: 100_000 }(""); + + // Silence warning: Return value of low-level calls not used. + (success, data) = (success, data); + + console.log(gasleft()); + } + + function test_transferNativeToken_toContract_gasOverride() public { + vm.prank(address(tokenOwner)); + console.log(gasleft()); + (bool success, bytes memory data) = address(this).call{ value: 1 ether, gas: 100_000 }(""); + console.log(gasleft()); + + // Silence warning: Return value of low-level calls not used. + (success, data) = (success, data); + } +} diff --git a/src/test/airdrop/AirdropERC20Claimable.t.sol b/src/test/airdrop/AirdropERC20Claimable.t.sol new file mode 100644 index 000000000..efc5029a8 --- /dev/null +++ b/src/test/airdrop/AirdropERC20Claimable.t.sol @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "contracts/prebuilts/unaudited/airdrop/AirdropERC20Claimable.sol"; + +// Test imports +import { Wallet } from "../utils/Wallet.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import "../utils/BaseTest.sol"; + +contract AirdropERC20ClaimableTest is BaseTest { + address public implementation; + AirdropERC20Claimable internal drop; + + function setUp() public override { + super.setUp(); + + address implementation = address(new AirdropERC20Claimable()); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + drop = AirdropERC20Claimable( + address( + new TWProxy( + implementation, + abi.encodeCall( + AirdropERC20Claimable.initialize, + ( + forwarders(), + address(airdropTokenOwner), + address(erc20), + 10_000 ether, + 1000, + 1, + _airdropMerkleRootERC20 + ) + ) + ) + ) + ); + + erc20.mint(address(airdropTokenOwner), 10_000 ether); + airdropTokenOwner.setAllowanceERC20(address(erc20), address(drop), type(uint256).max); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `claim` -- for allowlisted claimers + //////////////////////////////////////////////////////////////*/ + + function test_state_claim_allowlistedClaimer() public { + string[] memory inputs = new string[](3); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/getProofAirdrop.ts"; + inputs[2] = Strings.toString(uint256(5)); + + bytes memory result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 quantity = 2; + + uint256 _availableAmount = drop.availableAmount(); + + vm.prank(receiver); + drop.claim(receiver, quantity, proofs, 5); + + assertEq(erc20.balanceOf(receiver), quantity); + assertEq(erc20.balanceOf(address(airdropTokenOwner)), _availableAmount - quantity); + assertEq(drop.supplyClaimedByWallet(receiver), quantity); + assertEq(drop.availableAmount(), _availableAmount - quantity); + } + + function test_revert_claim_notInAllowlist() public { + string[] memory inputs = new string[](3); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/getProofAirdrop.ts"; + inputs[2] = Strings.toString(uint256(4)); // generate proof with incorrect amount + + bytes memory result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 quantity = 2; + + vm.prank(receiver); + vm.expectRevert("invalid quantity."); + drop.claim(receiver, quantity, proofs, 4); + } + + function test_state_claim_allowlistedClaimer_maxAmountClaimed() public { + string[] memory inputs = new string[](3); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/getProofAirdrop.ts"; + inputs[2] = Strings.toString(uint256(5)); + + bytes memory result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 quantity = 2; + + vm.prank(receiver); + drop.claim(receiver, quantity, proofs, 5); + + quantity = 3; + + vm.prank(receiver); + drop.claim(receiver, quantity, proofs, 5); + + // claiming again after exhausting claim limit + vm.prank(receiver); + vm.expectRevert("invalid quantity."); + drop.claim(receiver, quantity, proofs, 5); + } + + function test_state_claim_allowlistedClaimer_invalidQuantity() public { + string[] memory inputs = new string[](3); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/getProofAirdrop.ts"; + inputs[2] = Strings.toString(uint256(5)); + + bytes memory result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 quantity = 6; + + vm.prank(receiver); + vm.expectRevert("invalid quantity."); + drop.claim(receiver, quantity, proofs, 5); + } + + function test_state_claim_allowlistedClaimer_airdropExpired() public { + string[] memory inputs = new string[](3); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/getProofAirdrop.ts"; + inputs[2] = Strings.toString(uint256(5)); + + bytes memory result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + vm.warp(1001); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 quantity = 5; + + vm.prank(receiver); + vm.expectRevert("airdrop expired."); + drop.claim(receiver, quantity, proofs, 5); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `claim` -- for open claiming + //////////////////////////////////////////////////////////////*/ + + function test_state_claim_nonAllowlistedClaimer() public { + address receiver = address(0x123); + uint256 quantity = 1; + bytes32[] memory proofs; + + uint256 _availableAmount = drop.availableAmount(); + + vm.prank(receiver); + drop.claim(receiver, quantity, proofs, 0); + + assertEq(erc20.balanceOf(receiver), quantity); + assertEq(erc20.balanceOf(address(airdropTokenOwner)), _availableAmount - quantity); + assertEq(drop.supplyClaimedByWallet(receiver), quantity); + assertEq(drop.availableAmount(), _availableAmount - quantity); + } + + function test_revert_claim_nonAllowlistedClaimer_invalidQuantity() public { + address receiver = address(0x123); + uint256 quantity = 2; + bytes32[] memory proofs; + + vm.prank(receiver); + vm.expectRevert("invalid quantity."); + drop.claim(receiver, quantity, proofs, 0); + } + + function test_revert_claim_nonAllowlistedClaimer_exceedsAvailable() public { + uint256 _availableAmount = drop.availableAmount(); + bytes32[] memory proofs; + + address receiver = getActor(uint160(2)); + vm.prank(receiver); + vm.expectRevert("exceeds available tokens."); + drop.claim(receiver, 10_001 ether, proofs, 0); + } +} diff --git a/src/test/airdrop/AirdropERC721.t.sol b/src/test/airdrop/AirdropERC721.t.sol new file mode 100644 index 000000000..6b579eb46 --- /dev/null +++ b/src/test/airdrop/AirdropERC721.t.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { AirdropERC721, IAirdropERC721 } from "contracts/prebuilts/unaudited/airdrop/AirdropERC721.sol"; + +// Test imports +import { Wallet } from "../utils/Wallet.sol"; +import "../utils/BaseTest.sol"; + +contract AirdropERC721Test is BaseTest { + AirdropERC721 internal drop; + + Wallet internal tokenOwner; + + IAirdropERC721.AirdropContent[] internal _contentsOne; + IAirdropERC721.AirdropContent[] internal _contentsTwo; + + uint256 countOne; + uint256 countTwo; + + function setUp() public override { + super.setUp(); + + drop = AirdropERC721(getContract("AirdropERC721")); + + tokenOwner = getWallet(); + + erc721.mint(address(tokenOwner), 1500); + tokenOwner.setApprovalForAllERC721(address(erc721), address(drop), true); + + countOne = 1000; + countTwo = 200; + + for (uint256 i = 0; i < countOne; i++) { + _contentsOne.push(IAirdropERC721.AirdropContent({ recipient: getActor(uint160(i)), tokenId: i })); + } + + for (uint256 i = countOne; i < countOne + countTwo; i++) { + _contentsTwo.push(IAirdropERC721.AirdropContent({ recipient: getActor(uint160(i)), tokenId: i })); + } + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: stateless airdrop + //////////////////////////////////////////////////////////////*/ + + function test_state_airdrop() public { + vm.prank(deployer); + drop.airdropERC721(address(erc721), address(tokenOwner), _contentsOne); + + for (uint256 i = 0; i < 1000; i++) { + assertEq(erc721.ownerOf(i), _contentsOne[i].recipient); + } + } + + function test_revert_airdrop_notOwner() public { + vm.prank(address(25)); + vm.expectRevert("Not authorized."); + drop.airdropERC721(address(erc721), address(tokenOwner), _contentsOne); + } + + function test_revert_airdrop_notApproved() public { + tokenOwner.setApprovalForAllERC721(address(erc721), address(drop), false); + + vm.startPrank(deployer); + vm.expectRevert("Not owner or approved"); + drop.airdropERC721(address(erc721), address(tokenOwner), _contentsOne); + vm.stopPrank(); + } +} + +contract AirdropERC721GasTest is BaseTest { + AirdropERC721 internal drop; + + Wallet internal tokenOwner; + + function setUp() public override { + super.setUp(); + + drop = AirdropERC721(getContract("AirdropERC721")); + + tokenOwner = getWallet(); + + erc721.mint(address(tokenOwner), 1500); + tokenOwner.setApprovalForAllERC721(address(erc721), address(drop), true); + + vm.startPrank(address(tokenOwner)); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: gas benchmarks, etc. + //////////////////////////////////////////////////////////////*/ + + function test_safeTransferFrom_toEOA() public { + erc721.safeTransferFrom(address(tokenOwner), address(0x123), 0); + } + + function test_safeTransferFrom_toContract() public { + erc721.safeTransferFrom(address(tokenOwner), address(this), 0); + } + + function test_safeTransferFrom_toEOA_gasOverride() public { + console.log(gasleft()); + erc721.safeTransferFrom{ gas: 100_000 }(address(tokenOwner), address(0x123), 0); + console.log(gasleft()); + } + + function test_safeTransferFrom_toContract_gasOverride() public { + console.log(gasleft()); + erc721.safeTransferFrom{ gas: 100_000 }(address(tokenOwner), address(this), 0); + console.log(gasleft()); + } + + function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) { + return this.onERC721Received.selector; + } +} diff --git a/src/test/airdrop/AirdropERC721Claimable.t.sol b/src/test/airdrop/AirdropERC721Claimable.t.sol new file mode 100644 index 000000000..917ac4043 --- /dev/null +++ b/src/test/airdrop/AirdropERC721Claimable.t.sol @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "contracts/prebuilts/unaudited/airdrop/AirdropERC721Claimable.sol"; + +// Test imports +import { Wallet } from "../utils/Wallet.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { BaseTest } from "../utils/BaseTest.sol"; +import { Strings } from "contracts/lib/Strings.sol"; + +contract AirdropERC721ClaimableTest is BaseTest { + address public implementation; + AirdropERC721Claimable internal drop; + + function setUp() public override { + super.setUp(); + + address implementation = address(new AirdropERC721Claimable()); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + drop = AirdropERC721Claimable( + address( + new TWProxy( + implementation, + abi.encodeCall( + AirdropERC721Claimable.initialize, + ( + forwarders(), + address(airdropTokenOwner), + address(erc721), + _airdropTokenIdsERC721, + 1000, + 1, + _airdropMerkleRootERC721 + ) + ) + ) + ) + ); + + erc721.mint(address(airdropTokenOwner), 1000); + airdropTokenOwner.setApprovalForAllERC721(address(erc721), address(drop), true); + } + + // /*/////////////////////////////////////////////////////////////// + // Unit tests: `claim` -- for allowlisted claimers + // //////////////////////////////////////////////////////////////*/ + + function test_state_claim_allowlistedClaimer() public { + string[] memory inputs = new string[](3); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/getProofAirdrop.ts"; + inputs[2] = Strings.toString(uint256(5)); + + bytes memory result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 quantity = 2; + + uint256 _availableAmount = drop.availableAmount(); + uint256 _nextIndex = drop.nextIndex(); + + vm.prank(receiver); + drop.claim(receiver, quantity, proofs, 5); + + for (uint256 i = 0; i < quantity; i++) { + assertEq(erc721.ownerOf(i), receiver); + } + assertEq(drop.nextIndex(), _nextIndex + quantity); + assertEq(drop.supplyClaimedByWallet(receiver), quantity); + assertEq(drop.availableAmount(), _availableAmount - quantity); + } + + function test_revert_claim_notInAllowlist() public { + string[] memory inputs = new string[](3); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/getProofAirdrop.ts"; + inputs[2] = Strings.toString(uint256(4)); // generate proof with incorrect amount + + bytes memory result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 quantity = 2; + + vm.prank(receiver); + vm.expectRevert("invalid quantity."); + drop.claim(receiver, quantity, proofs, 4); + } + + function test_revert_claim_allowlistedClaimer_proofClaimed() public { + string[] memory inputs = new string[](3); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/getProofAirdrop.ts"; + inputs[2] = Strings.toString(uint256(5)); + + bytes memory result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 quantity = 2; + + uint256 _availableAmount = drop.availableAmount(); + uint256 _nextIndex = drop.nextIndex(); + + vm.prank(receiver); + drop.claim(receiver, quantity, proofs, 5); + + for (uint256 i = 0; i < quantity; i++) { + assertEq(erc721.ownerOf(i), receiver); + } + assertEq(drop.nextIndex(), _nextIndex + quantity); + assertEq(drop.supplyClaimedByWallet(receiver), quantity); + assertEq(drop.availableAmount(), _availableAmount - quantity); + + quantity = 3; + + vm.prank(receiver); + drop.claim(receiver, quantity, proofs, 5); + + vm.prank(receiver); + vm.expectRevert("invalid quantity."); + drop.claim(receiver, quantity, proofs, 5); + } + + function test_state_claim_allowlistedClaimer_invalidQuantity() public { + string[] memory inputs = new string[](3); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/getProofAirdrop.ts"; + inputs[2] = Strings.toString(uint256(5)); + + bytes memory result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 quantity = 6; + + vm.prank(receiver); + vm.expectRevert("invalid quantity."); + drop.claim(receiver, quantity, proofs, 5); + } + + function test_state_claim_allowlistedClaimer_airdropExpired() public { + string[] memory inputs = new string[](3); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/getProofAirdrop.ts"; + inputs[2] = Strings.toString(uint256(5)); + + bytes memory result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + vm.warp(1001); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 quantity = 5; + + vm.prank(receiver); + vm.expectRevert("airdrop expired."); + drop.claim(receiver, quantity, proofs, 5); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `claim` -- for open claiming + //////////////////////////////////////////////////////////////*/ + + function test_state_claim_nonAllowlistedClaimer() public { + address receiver = address(0x123); + uint256 quantity = 1; + bytes32[] memory proofs; + + uint256 _availableAmount = drop.availableAmount(); + uint256 _nextIndex = drop.nextIndex(); + + vm.prank(receiver); + drop.claim(receiver, quantity, proofs, 0); + + assertEq(erc721.ownerOf(0), receiver); + assertEq(drop.nextIndex(), _nextIndex + quantity); + assertEq(drop.supplyClaimedByWallet(receiver), quantity); + assertEq(drop.availableAmount(), _availableAmount - quantity); + } + + function test_revert_claim_nonAllowlistedClaimer_invalidQuantity() public { + address receiver = address(0x123); + uint256 quantity = 2; + bytes32[] memory proofs; + + vm.prank(receiver); + vm.expectRevert("invalid quantity."); + drop.claim(receiver, quantity, proofs, 0); + } + + function test_revert_claim_nonAllowlistedClaimer_exceedsAvailable() public { + uint256 _availableAmount = drop.availableAmount(); + bytes32[] memory proofs; + + uint256 i = 0; + for (; i < _availableAmount; i++) { + address receiver = getActor(uint160(i)); + vm.prank(receiver); + drop.claim(receiver, 1, proofs, 0); + } + + address receiver = getActor(uint160(i)); + vm.prank(receiver); + vm.expectRevert("exceeds available tokens."); + drop.claim(receiver, 1, proofs, 0); + } +} diff --git a/src/test/benchmark/AccountBenchmark.t.sol b/src/test/benchmark/AccountBenchmark.t.sol new file mode 100644 index 000000000..c07faa649 --- /dev/null +++ b/src/test/benchmark/AccountBenchmark.t.sol @@ -0,0 +1,524 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test utils +import "../utils/BaseTest.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Account Abstraction setup for smart wallets. +import { EntryPoint, IEntryPoint } from "contracts/prebuilts/account/utils/EntryPoint.sol"; +import { PackedUserOperation } from "contracts/prebuilts/account/interfaces/PackedUserOperation.sol"; + +// Target +import { IAccountPermissions } from "contracts/extension/interface/IAccountPermissions.sol"; +import { AccountFactory } from "contracts/prebuilts/account/non-upgradeable/AccountFactory.sol"; +import { Account as SimpleAccount } from "contracts/prebuilts/account/non-upgradeable/Account.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +/// @dev This is a dummy contract to test contract interactions with Account. +contract Number { + uint256 public num; + + function setNum(uint256 _num) public { + num = _num; + } + + function doubleNum() public { + num *= 2; + } + + function incrementNum() public { + num += 1; + } +} + +contract AccountBenchmarkTest is BaseTest { + // Target contracts + EntryPoint private entrypoint; + AccountFactory private accountFactory; + + // Mocks + Number internal numberContract; + + // Test params + uint256 private accountAdminPKey = 100; + address private accountAdmin; + + uint256 private accountSignerPKey = 200; + address private accountSigner; + + uint256 private nonSignerPKey = 300; + address private nonSigner; + + // UserOp terminology: `sender` is the smart wallet. + address private sender = 0x0df2C3523703d165Aa7fA1a552f3F0B56275DfC6; + address payable private beneficiary = payable(address(0x45654)); + + bytes32 private uidCache = bytes32("random uid"); + + event AccountCreated(address indexed account, address indexed accountAdmin); + + function _signSignerPermissionRequest( + IAccountPermissions.SignerPermissionRequest memory _req + ) internal view returns (bytes memory signature) { + bytes32 typehashSignerPermissionRequest = keccak256( + "SignerPermissionRequest(address signer,uint8 isAdmin,address[] approvedTargets,uint256 nativeTokenLimitPerTransaction,uint128 permissionStartTimestamp,uint128 permissionEndTimestamp,uint128 reqValidityStartTimestamp,uint128 reqValidityEndTimestamp,bytes32 uid)" + ); + bytes32 nameHash = keccak256(bytes("Account")); + bytes32 versionHash = keccak256(bytes("1")); + bytes32 typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + bytes32 domainSeparator = keccak256(abi.encode(typehashEip712, nameHash, versionHash, block.chainid, sender)); + + bytes memory encodedRequest = bytes.concat( + abi.encode( + typehashSignerPermissionRequest, + _req.signer, + _req.isAdmin, + keccak256(abi.encodePacked(_req.approvedTargets)), + _req.nativeTokenLimitPerTransaction + ), + abi.encode( + _req.permissionStartTimestamp, + _req.permissionEndTimestamp, + _req.reqValidityStartTimestamp, + _req.reqValidityEndTimestamp, + _req.uid + ) + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(accountAdminPKey, typedDataHash); + signature = abi.encodePacked(r, s, v); + } + + function _setupUserOp( + uint256 _signerPKey, + bytes memory _initCode, + bytes memory _callDataForEntrypoint + ) internal returns (PackedUserOperation[] memory ops) { + uint256 nonce = entrypoint.getNonce(sender, 0); + PackedUserOperation memory op; + + { + uint128 verificationGasLimit = 500_000; + uint128 callGasLimit = 500_000; + bytes32 packedGasLimits = (bytes32(uint256(verificationGasLimit)) << 128) | bytes32(uint256(callGasLimit)); + + // Get user op fields + op = PackedUserOperation({ + sender: sender, + nonce: nonce, + initCode: _initCode, + callData: _callDataForEntrypoint, + accountGasLimits: packedGasLimits, + preVerificationGas: 500_000, + gasFees: 0, + paymasterAndData: bytes(""), + signature: bytes("") + }); + } + + // Sign UserOp + bytes32 opHash = EntryPoint(entrypoint).getUserOpHash(op); + bytes32 msgHash = ECDSA.toEthSignedMessageHash(opHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signerPKey, msgHash); + bytes memory userOpSignature = abi.encodePacked(r, s, v); + + address recoveredSigner = ECDSA.recover(msgHash, v, r, s); + address expectedSigner = vm.addr(_signerPKey); + assertEq(recoveredSigner, expectedSigner); + + op.signature = userOpSignature; + + // Store UserOp + ops = new PackedUserOperation[](1); + ops[0] = op; + } + + function _setupUserOpExecute( + uint256 _signerPKey, + bytes memory _initCode, + address _target, + uint256 _value, + bytes memory _callData + ) internal returns (PackedUserOperation[] memory) { + bytes memory callDataForEntrypoint = abi.encodeWithSignature( + "execute(address,uint256,bytes)", + _target, + _value, + _callData + ); + + return _setupUserOp(_signerPKey, _initCode, callDataForEntrypoint); + } + + function _setupUserOpExecuteBatch( + uint256 _signerPKey, + bytes memory _initCode, + address[] memory _target, + uint256[] memory _value, + bytes[] memory _callData + ) internal returns (PackedUserOperation[] memory) { + bytes memory callDataForEntrypoint = abi.encodeWithSignature( + "executeBatch(address[],uint256[],bytes[])", + _target, + _value, + _callData + ); + + return _setupUserOp(_signerPKey, _initCode, callDataForEntrypoint); + } + + function setUp() public override { + super.setUp(); + + // Setup signers. + accountAdmin = vm.addr(accountAdminPKey); + vm.deal(accountAdmin, 100 ether); + + accountSigner = vm.addr(accountSignerPKey); + nonSigner = vm.addr(nonSignerPKey); + + // Setup contracts + entrypoint = new EntryPoint(); + // deploy account factory + accountFactory = new AccountFactory(deployer, IEntryPoint(payable(address(entrypoint)))); + // deploy dummy contract + numberContract = new Number(); + } + + /*/////////////////////////////////////////////////////////////// + Test: creating an account + //////////////////////////////////////////////////////////////*/ + + /// @dev Create an account by directly calling the factory. + function test_state_createAccount_viaFactory() public { + accountFactory.createAccount(accountAdmin, bytes("")); + } + + /// @dev Create an account via Entrypoint. + function test_state_createAccount_viaEntrypoint() public { + vm.pauseGasMetering(); + bytes memory initCallData = abi.encodeWithSignature("createAccount(address,bytes)", accountAdmin, bytes("")); + bytes memory initCode = abi.encodePacked(abi.encodePacked(address(accountFactory)), initCallData); + + vm.resumeGasMetering(); + PackedUserOperation[] memory userOpCreateAccount = _setupUserOpExecute( + accountAdminPKey, + initCode, + address(0), + 0, + bytes("") + ); + + EntryPoint(entrypoint).handleOps(userOpCreateAccount, beneficiary); + } + + /*/////////////////////////////////////////////////////////////// + Test: performing a contract call + //////////////////////////////////////////////////////////////*/ + + function _setup_executeTransaction() internal { + bytes memory initCallData = abi.encodeWithSignature("createAccount(address,bytes)", accountAdmin, bytes("")); + bytes memory initCode = abi.encodePacked(abi.encodePacked(address(accountFactory)), initCallData); + + PackedUserOperation[] memory userOpCreateAccount = _setupUserOpExecute( + accountAdminPKey, + initCode, + address(0), + 0, + bytes("") + ); + + EntryPoint(entrypoint).handleOps(userOpCreateAccount, beneficiary); + } + + /// @dev Perform a state changing transaction directly via account. + function test_state_executeTransaction() public { + vm.pauseGasMetering(); + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + vm.resumeGasMetering(); + vm.prank(accountAdmin); + SimpleAccount(payable(account)).execute( + address(numberContract), + 0, + abi.encodeWithSignature("setNum(uint256)", 42) + ); + } + + /// @dev Perform many state changing transactions in a batch directly via account. + function test_state_executeBatchTransaction() public { + vm.pauseGasMetering(); + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + uint256 count = 3; + address[] memory targets = new address[](count); + uint256[] memory values = new uint256[](count); + bytes[] memory callData = new bytes[](count); + + for (uint256 i = 0; i < count; i += 1) { + targets[i] = address(numberContract); + values[i] = 0; + callData[i] = abi.encodeWithSignature("incrementNum()", i); + } + + vm.resumeGasMetering(); + vm.prank(accountAdmin); + SimpleAccount(payable(account)).executeBatch(targets, values, callData); + } + + /// @dev Perform a state changing transaction via Entrypoint. + function test_state_executeTransaction_viaEntrypoint() public { + vm.pauseGasMetering(); + _setup_executeTransaction(); + + vm.resumeGasMetering(); + PackedUserOperation[] memory userOp = _setupUserOpExecute( + accountAdminPKey, + bytes(""), + address(numberContract), + 0, + abi.encodeWithSignature("setNum(uint256)", 42) + ); + + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + } + + /// @dev Perform many state changing transactions in a batch via Entrypoint. + function test_state_executeBatchTransaction_viaEntrypoint() public { + vm.pauseGasMetering(); + _setup_executeTransaction(); + + uint256 count = 3; + address[] memory targets = new address[](count); + uint256[] memory values = new uint256[](count); + bytes[] memory callData = new bytes[](count); + + for (uint256 i = 0; i < count; i += 1) { + targets[i] = address(numberContract); + values[i] = 0; + callData[i] = abi.encodeWithSignature("incrementNum()", i); + } + + vm.resumeGasMetering(); + PackedUserOperation[] memory userOp = _setupUserOpExecuteBatch( + accountAdminPKey, + bytes(""), + targets, + values, + callData + ); + + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + } + + /// @dev Perform many state changing transactions in a batch via Entrypoint. + function test_state_executeBatchTransaction_viaAccountSigner() public { + vm.pauseGasMetering(); + _setup_executeTransaction(); + + uint256 count = 3; + address[] memory targets = new address[](count); + uint256[] memory values = new uint256[](count); + bytes[] memory callData = new bytes[](count); + + for (uint256 i = 0; i < count; i += 1) { + targets[i] = address(numberContract); + values[i] = 0; + callData[i] = abi.encodeWithSignature("incrementNum()", i); + } + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + address[] memory approvedTargets = new address[](1); + approvedTargets[0] = address(numberContract); + + vm.resumeGasMetering(); + IAccountPermissions.SignerPermissionRequest memory permissionsReq = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 0, + approvedTargets, + 1 ether, + 0, + type(uint128).max, + 0, + type(uint128).max, + uidCache + ); + + vm.prank(accountAdmin); + bytes memory sig = _signSignerPermissionRequest(permissionsReq); + SimpleAccount(payable(account)).setPermissionsForSigner(permissionsReq, sig); + + PackedUserOperation[] memory userOp = _setupUserOpExecuteBatch( + accountSignerPKey, + bytes(""), + targets, + values, + callData + ); + + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + } + + /// @dev Perform a state changing transaction via Entrypoint and a SIGNER_ROLE holder. + function test_state_executeTransaction_viaAccountSigner() public { + vm.pauseGasMetering(); + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + address[] memory approvedTargets = new address[](1); + approvedTargets[0] = address(numberContract); + + vm.resumeGasMetering(); + IAccountPermissions.SignerPermissionRequest memory permissionsReq = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 0, + approvedTargets, + 1 ether, + 0, + type(uint128).max, + 0, + type(uint128).max, + uidCache + ); + + vm.prank(accountAdmin); + bytes memory sig = _signSignerPermissionRequest(permissionsReq); + SimpleAccount(payable(account)).setPermissionsForSigner(permissionsReq, sig); + + PackedUserOperation[] memory userOp = _setupUserOpExecute( + accountSignerPKey, + bytes(""), + address(numberContract), + 0, + abi.encodeWithSignature("setNum(uint256)", 42) + ); + + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + } + + /*/////////////////////////////////////////////////////////////// + Test: receiving and sending native tokens + //////////////////////////////////////////////////////////////*/ + + /// @dev Send native tokens to an account. + function test_state_accountReceivesNativeTokens() public { + vm.pauseGasMetering(); + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + vm.resumeGasMetering(); + vm.prank(accountAdmin); + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory data) = payable(account).call{ value: 1000 }(""); + + // Silence warning: Return value of low-level calls not used. + (success, data) = (success, data); + } + + /// @dev Transfer native tokens out of an account. + function test_state_transferOutsNativeTokens() public { + vm.pauseGasMetering(); + _setup_executeTransaction(); + + uint256 value = 1000; + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + vm.prank(accountAdmin); + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory data) = payable(account).call{ value: value }(""); + + // Silence warning: Return value of low-level calls not used. + (success, data) = (success, data); + + address recipient = address(0x3456); + + vm.resumeGasMetering(); + PackedUserOperation[] memory userOp = _setupUserOpExecute( + accountAdminPKey, + bytes(""), + recipient, + value, + bytes("") + ); + + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + } + + /// @dev Add and remove a deposit for the account from the Entrypoint. + + function test_state_addAndWithdrawDeposit() public { + vm.pauseGasMetering(); + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + vm.resumeGasMetering(); + vm.startPrank(accountAdmin); + SimpleAccount(payable(account)).addDeposit{ value: 1000 }(); + + SimpleAccount(payable(account)).withdrawDepositTo(payable(accountSigner), 500); + vm.stopPrank(); + } + + /*/////////////////////////////////////////////////////////////// + Test: receiving ERC-721 and ERC-1155 NFTs + //////////////////////////////////////////////////////////////*/ + + /// @dev Send an ERC-721 NFT to an account. + function test_state_receiveERC721NFT() public { + vm.pauseGasMetering(); + _setup_executeTransaction(); + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + vm.resumeGasMetering(); + erc721.mint(account, 1); + } + + /// @dev Send an ERC-1155 NFT to an account. + function test_state_receiveERC1155NFT() public { + vm.pauseGasMetering(); + _setup_executeTransaction(); + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + vm.resumeGasMetering(); + erc1155.mint(account, 0, 1); + } + + /*/////////////////////////////////////////////////////////////// + Test: setting contract metadata + //////////////////////////////////////////////////////////////*/ + + /// @dev Set contract metadata via entrypoint. + function test_state_contractMetadata() public { + vm.pauseGasMetering(); + _setup_executeTransaction(); + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + vm.prank(accountAdmin); + SimpleAccount(payable(account)).setContractURI("https://example.com"); + + vm.resumeGasMetering(); + PackedUserOperation[] memory userOp = _setupUserOpExecute( + accountAdminPKey, + bytes(""), + address(account), + 0, + abi.encodeWithSignature("setContractURI(string)", "https://thirdweb.com") + ); + + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + } +} diff --git a/src/test/benchmark/AirdropBenchmark.t.sol b/src/test/benchmark/AirdropBenchmark.t.sol new file mode 100644 index 000000000..be93c7126 --- /dev/null +++ b/src/test/benchmark/AirdropBenchmark.t.sol @@ -0,0 +1,695 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { Airdrop } from "contracts/prebuilts/airdrop/Airdrop.sol"; + +// Test imports +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import "../utils/BaseTest.sol"; + +contract ERC721ReceiverCompliant is IERC721Receiver { + function onERC721Received( + address, + address, + uint256, + bytes calldata + ) external view virtual override returns (bytes4) { + return this.onERC721Received.selector; + } +} + +contract ERC1155ReceiverCompliant is IERC1155Receiver { + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes calldata data + ) external view virtual override returns (bytes4) { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external returns (bytes4) { + return this.onERC1155BatchReceived.selector; + } + + function supportsInterface(bytes4 interfaceId) external view returns (bool) {} +} + +contract AirdropBenchmarkTest is BaseTest { + Airdrop internal airdrop; + + bytes32 private constant CONTENT_TYPEHASH_ERC20 = + keccak256("AirdropContentERC20(address recipient,uint256 amount)"); + bytes32 private constant REQUEST_TYPEHASH_ERC20 = + keccak256( + "AirdropRequestERC20(bytes32 uid,address tokenAddress,uint256 expirationTimestamp,AirdropContentERC20[] contents)AirdropContentERC20(address recipient,uint256 amount)" + ); + + bytes32 private constant CONTENT_TYPEHASH_ERC721 = + keccak256("AirdropContentERC721(address recipient,uint256 tokenId)"); + bytes32 private constant REQUEST_TYPEHASH_ERC721 = + keccak256( + "AirdropRequestERC721(bytes32 uid,address tokenAddress,uint256 expirationTimestamp,AirdropContentERC721[] contents)AirdropContentERC721(address recipient,uint256 tokenId)" + ); + + bytes32 private constant CONTENT_TYPEHASH_ERC1155 = + keccak256("AirdropContentERC1155(address recipient,uint256 tokenId,uint256 amount)"); + bytes32 private constant REQUEST_TYPEHASH_ERC1155 = + keccak256( + "AirdropRequestERC1155(bytes32 uid,address tokenAddress,uint256 expirationTimestamp,AirdropContentERC1155[] contents)AirdropContentERC1155(address recipient,uint256 tokenId,uint256 amount)" + ); + + bytes32 private constant NAME_HASH = keccak256(bytes("Airdrop")); + bytes32 private constant VERSION_HASH = keccak256(bytes("1")); + bytes32 private constant TYPE_HASH_EIP712 = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + bytes32 internal domainSeparator; + + function setUp() public override { + super.setUp(); + + address impl = address(new Airdrop()); + + airdrop = Airdrop(payable(address(new TWProxy(impl, abi.encodeCall(Airdrop.initialize, (signer, "")))))); + + domainSeparator = keccak256( + abi.encode(TYPE_HASH_EIP712, NAME_HASH, VERSION_HASH, block.chainid, address(airdrop)) + ); + } + + function _getContentsERC20(uint256 length) internal pure returns (Airdrop.AirdropContentERC20[] memory contents) { + contents = new Airdrop.AirdropContentERC20[](length); + for (uint256 i = 0; i < length; i++) { + contents[i].recipient = address(uint160(i + 10)); + contents[i].amount = i + 10; + } + } + + function _getContentsERC721(uint256 length) internal pure returns (Airdrop.AirdropContentERC721[] memory contents) { + contents = new Airdrop.AirdropContentERC721[](length); + for (uint256 i = 0; i < length; i++) { + contents[i].recipient = address(uint160(i + 10)); + contents[i].tokenId = i; + } + } + + function _getContentsERC1155( + uint256 length + ) internal pure returns (Airdrop.AirdropContentERC1155[] memory contents) { + contents = new Airdrop.AirdropContentERC1155[](length); + for (uint256 i = 0; i < length; i++) { + contents[i].recipient = address(uint160(i + 10)); + contents[i].tokenId = 0; + contents[i].amount = i + 10; + } + } + + function _signReqERC20( + Airdrop.AirdropRequestERC20 memory req, + uint256 privateKey + ) internal view returns (bytes memory signature) { + bytes32[] memory contentHashes = new bytes32[](req.contents.length); + for (uint i = 0; i < req.contents.length; i++) { + contentHashes[i] = keccak256( + abi.encode(CONTENT_TYPEHASH_ERC20, req.contents[i].recipient, req.contents[i].amount) + ); + } + bytes32 contentHash = keccak256(abi.encodePacked(contentHashes)); + + bytes memory dataToHash; + { + dataToHash = abi.encode( + REQUEST_TYPEHASH_ERC20, + req.uid, + req.tokenAddress, + req.expirationTimestamp, + contentHash + ); + } + + { + bytes32 _structHash = keccak256(dataToHash); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, _structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, typedDataHash); + + signature = abi.encodePacked(r, s, v); + } + } + + function _signReqERC721( + Airdrop.AirdropRequestERC721 memory req, + uint256 privateKey + ) internal view returns (bytes memory signature) { + bytes32[] memory contentHashes = new bytes32[](req.contents.length); + for (uint i = 0; i < req.contents.length; i++) { + contentHashes[i] = keccak256( + abi.encode(CONTENT_TYPEHASH_ERC721, req.contents[i].recipient, req.contents[i].tokenId) + ); + } + bytes32 contentHash = keccak256(abi.encodePacked(contentHashes)); + + bytes memory dataToHash; + { + dataToHash = abi.encode( + REQUEST_TYPEHASH_ERC721, + req.uid, + req.tokenAddress, + req.expirationTimestamp, + contentHash + ); + } + + { + bytes32 _structHash = keccak256(dataToHash); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, _structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, typedDataHash); + + signature = abi.encodePacked(r, s, v); + } + } + + function _signReqERC1155( + Airdrop.AirdropRequestERC1155 memory req, + uint256 privateKey + ) internal view returns (bytes memory signature) { + bytes32[] memory contentHashes = new bytes32[](req.contents.length); + for (uint i = 0; i < req.contents.length; i++) { + contentHashes[i] = keccak256( + abi.encode( + CONTENT_TYPEHASH_ERC1155, + req.contents[i].recipient, + req.contents[i].tokenId, + req.contents[i].amount + ) + ); + } + bytes32 contentHash = keccak256(abi.encodePacked(contentHashes)); + + bytes memory dataToHash; + { + dataToHash = abi.encode( + REQUEST_TYPEHASH_ERC1155, + req.uid, + req.tokenAddress, + req.expirationTimestamp, + contentHash + ); + } + + { + bytes32 _structHash = keccak256(dataToHash); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, _structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, typedDataHash); + + signature = abi.encodePacked(r, s, v); + } + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: Airdrop Push ERC20 + //////////////////////////////////////////////////////////////*/ + + function test_benchmark_airdropPush_erc20_10() public { + vm.pauseGasMetering(); + + erc20.mint(signer, 100 ether); + vm.prank(signer); + erc20.approve(address(airdrop), 100 ether); + + Airdrop.AirdropContentERC20[] memory contents = _getContentsERC20(10); + + vm.prank(signer); + vm.resumeGasMetering(); + airdrop.airdropERC20(address(erc20), contents); + } + + function test_benchmark_airdropPush_erc20_100() public { + vm.pauseGasMetering(); + + erc20.mint(signer, 100 ether); + vm.prank(signer); + erc20.approve(address(airdrop), 100 ether); + + Airdrop.AirdropContentERC20[] memory contents = _getContentsERC20(100); + + vm.prank(signer); + vm.resumeGasMetering(); + airdrop.airdropERC20(address(erc20), contents); + } + + function test_benchmark_airdropPush_erc20_1000() public { + vm.pauseGasMetering(); + + erc20.mint(signer, 100 ether); + vm.prank(signer); + erc20.approve(address(airdrop), 100 ether); + + Airdrop.AirdropContentERC20[] memory contents = _getContentsERC20(1000); + + vm.prank(signer); + vm.resumeGasMetering(); + airdrop.airdropERC20(address(erc20), contents); + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: Airdrop Signature ERC20 + //////////////////////////////////////////////////////////////*/ + + function test_benchmark_airdropSignature_erc20_10() public { + vm.pauseGasMetering(); + + erc20.mint(signer, 100 ether); + vm.prank(signer); + erc20.approve(address(airdrop), 100 ether); + + Airdrop.AirdropContentERC20[] memory contents = _getContentsERC20(10); + Airdrop.AirdropRequestERC20 memory req = Airdrop.AirdropRequestERC20({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc20), + expirationTimestamp: 1000, + contents: contents + }); + bytes memory signature = _signReqERC20(req, privateKey); + + vm.prank(signer); + vm.resumeGasMetering(); + airdrop.airdropERC20WithSignature(req, signature); + } + + function test_benchmark_airdropSignature_erc20_100() public { + vm.pauseGasMetering(); + + erc20.mint(signer, 100 ether); + vm.prank(signer); + erc20.approve(address(airdrop), 100 ether); + + Airdrop.AirdropContentERC20[] memory contents = _getContentsERC20(100); + Airdrop.AirdropRequestERC20 memory req = Airdrop.AirdropRequestERC20({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc20), + expirationTimestamp: 1000, + contents: contents + }); + bytes memory signature = _signReqERC20(req, privateKey); + + vm.prank(signer); + vm.resumeGasMetering(); + airdrop.airdropERC20WithSignature(req, signature); + } + + function test_benchmark_airdropSignature_erc20_1000() public { + vm.pauseGasMetering(); + + erc20.mint(signer, 100 ether); + vm.prank(signer); + erc20.approve(address(airdrop), 100 ether); + + Airdrop.AirdropContentERC20[] memory contents = _getContentsERC20(1000); + Airdrop.AirdropRequestERC20 memory req = Airdrop.AirdropRequestERC20({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc20), + expirationTimestamp: 1000, + contents: contents + }); + bytes memory signature = _signReqERC20(req, privateKey); + + vm.prank(signer); + vm.resumeGasMetering(); + airdrop.airdropERC20WithSignature(req, signature); + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: Airdrop Claim ERC20 + //////////////////////////////////////////////////////////////*/ + + function test_benchmark_airdropClaim_erc20() public { + vm.pauseGasMetering(); + + erc20.mint(signer, 100 ether); + vm.prank(signer); + erc20.approve(address(airdrop), 100 ether); + + string[] memory inputs = new string[](3); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRootAirdrop.ts"; + inputs[2] = Strings.toString(uint256(5)); + bytes memory result = vm.ffi(inputs); + bytes32 root = abi.decode(result, (bytes32)); + + // set merkle root + vm.prank(signer); + airdrop.setMerkleRoot(address(erc20), root, true); + + // generate proof + inputs[1] = "src/test/scripts/getProofAirdrop.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 quantity = 5; + + vm.prank(receiver); + vm.resumeGasMetering(); + airdrop.claimERC20(address(erc20), receiver, quantity, proofs); + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: Airdrop Push ERC721 + //////////////////////////////////////////////////////////////*/ + + function test_benchmark_airdropPush_erc721_10() public { + vm.pauseGasMetering(); + + erc721.mint(signer, 100); + vm.prank(signer); + erc721.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC721[] memory contents = _getContentsERC721(10); + + vm.prank(signer); + vm.resumeGasMetering(); + airdrop.airdropERC721(address(erc721), contents); + } + + function test_benchmark_airdropPush_erc721_100() public { + vm.pauseGasMetering(); + + erc721.mint(signer, 100); + vm.prank(signer); + erc721.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC721[] memory contents = _getContentsERC721(100); + + vm.prank(signer); + vm.resumeGasMetering(); + airdrop.airdropERC721(address(erc721), contents); + } + + function test_benchmark_airdropPush_erc721_1000() public { + vm.pauseGasMetering(); + + erc721.mint(signer, 1000); + vm.prank(signer); + erc721.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC721[] memory contents = _getContentsERC721(1000); + + vm.prank(signer); + vm.resumeGasMetering(); + airdrop.airdropERC721(address(erc721), contents); + } + + function test_benchmark_airdropPush_erc721ReceiverCompliant() public { + vm.pauseGasMetering(); + + erc721.mint(signer, 100); + vm.prank(signer); + erc721.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC721[] memory contents = new Airdrop.AirdropContentERC721[](1); + + contents[0].recipient = address(new ERC721ReceiverCompliant()); + contents[0].tokenId = 0; + + vm.prank(signer); + vm.resumeGasMetering(); + airdrop.airdropERC721(address(erc721), contents); + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: Airdrop Signature ERC721 + //////////////////////////////////////////////////////////////*/ + + function test_benchmark_airdropSignature_erc721_10() public { + vm.pauseGasMetering(); + + erc721.mint(signer, 1000); + vm.prank(signer); + erc721.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC721[] memory contents = _getContentsERC721(10); + Airdrop.AirdropRequestERC721 memory req = Airdrop.AirdropRequestERC721({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc721), + expirationTimestamp: 1000, + contents: contents + }); + bytes memory signature = _signReqERC721(req, privateKey); + + vm.prank(signer); + vm.resumeGasMetering(); + airdrop.airdropERC721WithSignature(req, signature); + } + + function test_benchmark_airdropSignature_erc721_100() public { + vm.pauseGasMetering(); + + erc721.mint(signer, 1000); + vm.prank(signer); + erc721.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC721[] memory contents = _getContentsERC721(100); + Airdrop.AirdropRequestERC721 memory req = Airdrop.AirdropRequestERC721({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc721), + expirationTimestamp: 1000, + contents: contents + }); + bytes memory signature = _signReqERC721(req, privateKey); + + vm.prank(signer); + vm.resumeGasMetering(); + airdrop.airdropERC721WithSignature(req, signature); + } + + function test_benchmark_airdropSignature_erc721_1000() public { + vm.pauseGasMetering(); + + erc721.mint(signer, 1000); + vm.prank(signer); + erc721.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC721[] memory contents = _getContentsERC721(1000); + Airdrop.AirdropRequestERC721 memory req = Airdrop.AirdropRequestERC721({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc721), + expirationTimestamp: 1000, + contents: contents + }); + bytes memory signature = _signReqERC721(req, privateKey); + + vm.prank(signer); + vm.resumeGasMetering(); + airdrop.airdropERC721WithSignature(req, signature); + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: Airdrop Claim ERC721 + //////////////////////////////////////////////////////////////*/ + + function test_benchmark_airdropClaim_erc721() public { + vm.pauseGasMetering(); + + erc721.mint(signer, 100); + vm.prank(signer); + erc721.setApprovalForAll(address(airdrop), true); + + string[] memory inputs = new string[](3); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRootAirdrop.ts"; + inputs[2] = Strings.toString(uint256(5)); + bytes memory result = vm.ffi(inputs); + bytes32 root = abi.decode(result, (bytes32)); + + // set merkle root + vm.prank(signer); + airdrop.setMerkleRoot(address(erc721), root, true); + + // generate proof + inputs[1] = "src/test/scripts/getProofAirdrop.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 tokenId = 5; + + vm.prank(receiver); + vm.resumeGasMetering(); + airdrop.claimERC721(address(erc721), receiver, tokenId, proofs); + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: Airdrop Push ERC1155 + //////////////////////////////////////////////////////////////*/ + + function test_benchmark_airdropPush_erc1155_10() public { + vm.pauseGasMetering(); + + erc1155.mint(signer, 0, 100 ether); + vm.prank(signer); + erc1155.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC1155[] memory contents = _getContentsERC1155(10); + + vm.prank(signer); + vm.resumeGasMetering(); + airdrop.airdropERC1155(address(erc1155), contents); + } + + function test_benchmark_airdropPush_erc1155_100() public { + vm.pauseGasMetering(); + + erc1155.mint(signer, 0, 100 ether); + vm.prank(signer); + erc1155.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC1155[] memory contents = _getContentsERC1155(100); + + vm.prank(signer); + vm.resumeGasMetering(); + airdrop.airdropERC1155(address(erc1155), contents); + } + + function test_benchmark_airdropPush_erc1155_1000() public { + vm.pauseGasMetering(); + + erc1155.mint(signer, 0, 100 ether); + vm.prank(signer); + erc1155.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC1155[] memory contents = _getContentsERC1155(1000); + + vm.prank(signer); + vm.resumeGasMetering(); + airdrop.airdropERC1155(address(erc1155), contents); + } + + function test_benchmark_airdropPush_erc1155ReceiverCompliant() public { + vm.pauseGasMetering(); + + erc1155.mint(signer, 0, 100 ether); + vm.prank(signer); + erc1155.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC1155[] memory contents = new Airdrop.AirdropContentERC1155[](1); + + contents[0].recipient = address(new ERC1155ReceiverCompliant()); + contents[0].tokenId = 0; + contents[0].amount = 100; + + vm.prank(signer); + vm.resumeGasMetering(); + airdrop.airdropERC1155(address(erc1155), contents); + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: Airdrop Signature ERC1155 + //////////////////////////////////////////////////////////////*/ + + function test_benchmark_airdropSignature_erc115_10() public { + vm.pauseGasMetering(); + + erc1155.mint(signer, 0, 100 ether); + vm.prank(signer); + erc1155.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC1155[] memory contents = _getContentsERC1155(10); + Airdrop.AirdropRequestERC1155 memory req = Airdrop.AirdropRequestERC1155({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc1155), + expirationTimestamp: 1000, + contents: contents + }); + bytes memory signature = _signReqERC1155(req, privateKey); + + vm.prank(signer); + vm.resumeGasMetering(); + airdrop.airdropERC1155WithSignature(req, signature); + } + + function test_benchmark_airdropSignature_erc115_100() public { + vm.pauseGasMetering(); + + erc1155.mint(signer, 0, 100 ether); + vm.prank(signer); + erc1155.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC1155[] memory contents = _getContentsERC1155(100); + Airdrop.AirdropRequestERC1155 memory req = Airdrop.AirdropRequestERC1155({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc1155), + expirationTimestamp: 1000, + contents: contents + }); + bytes memory signature = _signReqERC1155(req, privateKey); + + vm.prank(signer); + vm.resumeGasMetering(); + airdrop.airdropERC1155WithSignature(req, signature); + } + + function test_benchmark_airdropSignature_erc115_1000() public { + vm.pauseGasMetering(); + + erc1155.mint(signer, 0, 100 ether); + vm.prank(signer); + erc1155.setApprovalForAll(address(airdrop), true); + + Airdrop.AirdropContentERC1155[] memory contents = _getContentsERC1155(1000); + Airdrop.AirdropRequestERC1155 memory req = Airdrop.AirdropRequestERC1155({ + uid: bytes32(uint256(1)), + tokenAddress: address(erc1155), + expirationTimestamp: 1000, + contents: contents + }); + bytes memory signature = _signReqERC1155(req, privateKey); + + vm.prank(signer); + vm.resumeGasMetering(); + airdrop.airdropERC1155WithSignature(req, signature); + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: Airdrop Claim ERC1155 + //////////////////////////////////////////////////////////////*/ + + function test_benchmark_airdropClaim_erc1155() public { + vm.pauseGasMetering(); + + erc1155.mint(signer, 0, 100 ether); + vm.prank(signer); + erc1155.setApprovalForAll(address(airdrop), true); + + string[] memory inputs = new string[](4); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRootAirdrop1155.ts"; + inputs[2] = Strings.toString(uint256(0)); + inputs[3] = Strings.toString(uint256(5)); + bytes memory result = vm.ffi(inputs); + bytes32 root = abi.decode(result, (bytes32)); + + // set merkle root + vm.prank(signer); + airdrop.setMerkleRoot(address(erc1155), root, true); + + // generate proof + inputs[1] = "src/test/scripts/getProofAirdrop1155.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + uint256 quantity = 5; + + vm.prank(receiver); + vm.resumeGasMetering(); + airdrop.claimERC1155(address(erc1155), receiver, 0, quantity, proofs); + } +} diff --git a/src/test/benchmark/AirdropERC1155Benchmark.t.sol b/src/test/benchmark/AirdropERC1155Benchmark.t.sol new file mode 100644 index 000000000..389903565 --- /dev/null +++ b/src/test/benchmark/AirdropERC1155Benchmark.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { AirdropERC1155, IAirdropERC1155 } from "contracts/prebuilts/unaudited/airdrop/AirdropERC1155.sol"; + +// Test imports +import { Wallet } from "../utils/Wallet.sol"; +import "../utils/BaseTest.sol"; + +contract AirdropERC1155BenchmarkTest is BaseTest { + AirdropERC1155 internal drop; + + Wallet internal tokenOwner; + + IAirdropERC1155.AirdropContent[] internal _contentsOne; + IAirdropERC1155.AirdropContent[] internal _contentsTwo; + + uint256 countOne; + uint256 countTwo; + + function setUp() public override { + super.setUp(); + + drop = AirdropERC1155(getContract("AirdropERC1155")); + + tokenOwner = getWallet(); + + erc1155.mint(address(tokenOwner), 0, 1000); + erc1155.mint(address(tokenOwner), 1, 2000); + erc1155.mint(address(tokenOwner), 2, 3000); + erc1155.mint(address(tokenOwner), 3, 4000); + erc1155.mint(address(tokenOwner), 4, 5000); + + tokenOwner.setApprovalForAllERC1155(address(erc1155), address(drop), true); + + countOne = 1000; + countTwo = 200; + + for (uint256 i = 0; i < countOne; i++) { + _contentsOne.push( + IAirdropERC1155.AirdropContent({ recipient: getActor(uint160(i)), tokenId: i % 5, amount: 5 }) + ); + } + + for (uint256 i = countOne; i < countOne + countTwo; i++) { + _contentsTwo.push( + IAirdropERC1155.AirdropContent({ recipient: getActor(uint160(i)), tokenId: i % 5, amount: 5 }) + ); + } + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: AirdropERC1155 + //////////////////////////////////////////////////////////////*/ + + function test_benchmark_airdropERC1155_airdrop() public { + vm.pauseGasMetering(); + vm.prank(deployer); + vm.resumeGasMetering(); + drop.airdropERC1155(address(erc1155), address(tokenOwner), _contentsOne); + } +} diff --git a/src/test/benchmark/AirdropERC20Benchmark.t.sol b/src/test/benchmark/AirdropERC20Benchmark.t.sol new file mode 100644 index 000000000..d36ba5f49 --- /dev/null +++ b/src/test/benchmark/AirdropERC20Benchmark.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { AirdropERC20, IAirdropERC20 } from "contracts/prebuilts/unaudited/airdrop/AirdropERC20.sol"; + +// Test imports +import { Wallet } from "../utils/Wallet.sol"; +import "../utils/BaseTest.sol"; + +import "../mocks/MockERC20NonCompliant.sol"; + +contract AirdropERC20BenchmarkTest is BaseTest { + AirdropERC20 internal drop; + + Wallet internal tokenOwner; + + IAirdropERC20.AirdropContent[] internal _contentsOne; + IAirdropERC20.AirdropContent[] internal _contentsTwo; + + uint256 countOne; + uint256 countTwo; + + function setUp() public override { + super.setUp(); + + drop = AirdropERC20(getContract("AirdropERC20")); + + tokenOwner = getWallet(); + + erc20.mint(address(tokenOwner), 10_000 ether); + tokenOwner.setAllowanceERC20(address(erc20), address(drop), type(uint256).max); + + countOne = 1000; + countTwo = 200; + + for (uint256 i = 0; i < countOne; i++) { + _contentsOne.push(IAirdropERC20.AirdropContent({ recipient: getActor(uint160(i)), amount: 10 ether })); + } + + for (uint256 i = countOne; i < countOne + countTwo; i++) { + _contentsTwo.push(IAirdropERC20.AirdropContent({ recipient: getActor(uint160(i)), amount: 10 ether })); + } + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: AirdropERC20 + //////////////////////////////////////////////////////////////*/ + + function test_benchmark_airdropERC20_airdrop() public { + vm.pauseGasMetering(); + vm.prank(deployer); + vm.resumeGasMetering(); + drop.airdropERC20(address(erc20), address(tokenOwner), _contentsOne); + } +} diff --git a/src/test/benchmark/AirdropERC721Benchmark.t.sol b/src/test/benchmark/AirdropERC721Benchmark.t.sol new file mode 100644 index 000000000..407636572 --- /dev/null +++ b/src/test/benchmark/AirdropERC721Benchmark.t.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { AirdropERC721, IAirdropERC721 } from "contracts/prebuilts/unaudited/airdrop/AirdropERC721.sol"; + +// Test imports +import { Wallet } from "../utils/Wallet.sol"; +import "../utils/BaseTest.sol"; + +contract AirdropERC721BenchmarkTest is BaseTest { + AirdropERC721 internal drop; + + Wallet internal tokenOwner; + + IAirdropERC721.AirdropContent[] internal _contentsOne; + IAirdropERC721.AirdropContent[] internal _contentsTwo; + + uint256 countOne; + uint256 countTwo; + + function setUp() public override { + super.setUp(); + + drop = AirdropERC721(getContract("AirdropERC721")); + + tokenOwner = getWallet(); + + erc721.mint(address(tokenOwner), 1500); + tokenOwner.setApprovalForAllERC721(address(erc721), address(drop), true); + + countOne = 1000; + countTwo = 200; + + for (uint256 i = 0; i < countOne; i++) { + _contentsOne.push(IAirdropERC721.AirdropContent({ recipient: getActor(uint160(i)), tokenId: i })); + } + + for (uint256 i = countOne; i < countOne + countTwo; i++) { + _contentsTwo.push(IAirdropERC721.AirdropContent({ recipient: getActor(uint160(i)), tokenId: i })); + } + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: AirdropERC721 + //////////////////////////////////////////////////////////////*/ + + function test_benchmark_airdropERC721_airdrop() public { + vm.pauseGasMetering(); + vm.prank(deployer); + vm.resumeGasMetering(); + drop.airdropERC721(address(erc721), address(tokenOwner), _contentsOne); + } +} diff --git a/src/test/benchmark/DropERC1155Benchmark.t.sol b/src/test/benchmark/DropERC1155Benchmark.t.sol new file mode 100644 index 000000000..093be4089 --- /dev/null +++ b/src/test/benchmark/DropERC1155Benchmark.t.sol @@ -0,0 +1,288 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC1155, IPermissions, ILazyMint } from "contracts/prebuilts/drop/DropERC1155.sol"; + +// Test imports +import "../utils/BaseTest.sol"; + +contract DropERC1155BenchmarkTest is BaseTest { + using Strings for uint256; + using Strings for address; + + event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI); + event TokenURIRevealed(uint256 indexed index, string revealedURI); + + DropERC1155 public drop; + + bytes private emptyEncodedBytes = abi.encode("", ""); + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + drop = DropERC1155(getContract("DropERC1155")); + + erc20.mint(deployer, 1_000 ether); + vm.deal(deployer, 1_000 ether); + } + + /*/////////////////////////////////////////////////////////////// + DropERC1155 benchmark + //////////////////////////////////////////////////////////////*/ + + function test_benchmark_dropERC1155_claim() public { + vm.pauseGasMetering(); + uint256 _tokenId = 0; + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "5"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + DropERC1155.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 5; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + DropERC1155.ClaimCondition[] memory conditions = new DropERC1155.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(_tokenId, conditions, false); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(drop), 10000); + + vm.prank(receiver, receiver); + vm.resumeGasMetering(); + drop.claim(receiver, _tokenId, 100, address(erc20), 5, alp, ""); + } + + function test_benchmark_dropERC1155_setClaimConditions_five_conditions() public { + vm.pauseGasMetering(); + uint256 _tokenId = 0; + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "5"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + DropERC1155.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 5; + alp.currency = address(erc20); + + vm.warp(1); + + DropERC1155.ClaimCondition[] memory conditions = new DropERC1155.ClaimCondition[](5); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + conditions[1].maxClaimableSupply = 600; + conditions[1].pricePerToken = 20; + conditions[1].startTimestamp = 100000; + + conditions[2].maxClaimableSupply = 700; + conditions[2].pricePerToken = 30; + conditions[2].startTimestamp = 200000; + + conditions[3].maxClaimableSupply = 800; + conditions[3].pricePerToken = 40; + conditions[3].startTimestamp = 300000; + + conditions[4].maxClaimableSupply = 700; + conditions[4].pricePerToken = 30; + conditions[4].startTimestamp = 400000; + + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + vm.resumeGasMetering(); + drop.setClaimConditions(_tokenId, conditions, false); + } + + function test_benchmark_dropERC1155_lazyMint() public { + vm.pauseGasMetering(); + vm.prank(deployer); + vm.resumeGasMetering(); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + } + + // function test_benchmark_dropERC1155_setClaimConditions_one_condition() public { + // vm.pauseGasMetering(); + // uint256 _tokenId = 0; + // string[] memory inputs = new string[](5); + + // inputs[0] = "node"; + // inputs[1] = "src/test/scripts/generateRoot.ts"; + // inputs[2] = "300"; + // inputs[3] = "5"; + // inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + // bytes memory result = vm.ffi(inputs); + // // revert(); + // bytes32 root = abi.decode(result, (bytes32)); + + // inputs[1] = "src/test/scripts/getProof.ts"; + // result = vm.ffi(inputs); + // bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + // DropERC1155.AllowlistProof memory alp; + // alp.proof = proofs; + // alp.quantityLimitPerWallet = 300; + // alp.pricePerToken = 5; + // alp.currency = address(erc20); + + // vm.warp(1); + + // address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + // DropERC1155.ClaimCondition[] memory conditions = new DropERC1155.ClaimCondition[](1); + // conditions[0].maxClaimableSupply = 500; + // conditions[0].quantityLimitPerWallet = 10; + // conditions[0].merkleRoot = root; + // conditions[0].pricePerToken = 10; + // conditions[0].currency = address(erc20); + + // vm.prank(deployer); + // drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + // vm.prank(deployer); + // vm.resumeGasMetering(); + // drop.setClaimConditions(_tokenId, conditions, false); + // } + + // function test_benchmark_dropERC1155_setClaimConditions_two_conditions() public { + // vm.pauseGasMetering(); + // uint256 _tokenId = 0; + // string[] memory inputs = new string[](5); + + // inputs[0] = "node"; + // inputs[1] = "src/test/scripts/generateRoot.ts"; + // inputs[2] = "300"; + // inputs[3] = "5"; + // inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + // bytes memory result = vm.ffi(inputs); + // // revert(); + // bytes32 root = abi.decode(result, (bytes32)); + + // inputs[1] = "src/test/scripts/getProof.ts"; + // result = vm.ffi(inputs); + // bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + // DropERC1155.AllowlistProof memory alp; + // alp.proof = proofs; + // alp.quantityLimitPerWallet = 300; + // alp.pricePerToken = 5; + // alp.currency = address(erc20); + + // vm.warp(1); + + // address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + // DropERC1155.ClaimCondition[] memory conditions = new DropERC1155.ClaimCondition[](2); + // conditions[0].maxClaimableSupply = 500; + // conditions[0].quantityLimitPerWallet = 10; + // conditions[0].merkleRoot = root; + // conditions[0].pricePerToken = 10; + // conditions[0].currency = address(erc20); + + // conditions[1].maxClaimableSupply = 600; + // conditions[1].pricePerToken = 20; + // conditions[1].startTimestamp = 100000; + + // vm.prank(deployer); + // drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + // vm.prank(deployer); + // vm.resumeGasMetering(); + // drop.setClaimConditions(_tokenId, conditions, false); + // } + + // function test_benchmark_dropERC1155_setClaimConditions_three_conditions() public { + // vm.pauseGasMetering(); + // uint256 _tokenId = 0; + // string[] memory inputs = new string[](5); + + // inputs[0] = "node"; + // inputs[1] = "src/test/scripts/generateRoot.ts"; + // inputs[2] = "300"; + // inputs[3] = "5"; + // inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + // bytes memory result = vm.ffi(inputs); + // // revert(); + // bytes32 root = abi.decode(result, (bytes32)); + + // inputs[1] = "src/test/scripts/getProof.ts"; + // result = vm.ffi(inputs); + // bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + // DropERC1155.AllowlistProof memory alp; + // alp.proof = proofs; + // alp.quantityLimitPerWallet = 300; + // alp.pricePerToken = 5; + // alp.currency = address(erc20); + + // vm.warp(1); + + // address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + // DropERC1155.ClaimCondition[] memory conditions = new DropERC1155.ClaimCondition[](3); + // conditions[0].maxClaimableSupply = 500; + // conditions[0].quantityLimitPerWallet = 10; + // conditions[0].merkleRoot = root; + // conditions[0].pricePerToken = 10; + // conditions[0].currency = address(erc20); + + // conditions[1].maxClaimableSupply = 600; + // conditions[1].pricePerToken = 20; + // conditions[1].startTimestamp = 100000; + + // conditions[2].maxClaimableSupply = 700; + // conditions[2].pricePerToken = 30; + // conditions[2].startTimestamp = 200000; + + // vm.prank(deployer); + // drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + // vm.prank(deployer); + // vm.resumeGasMetering(); + // drop.setClaimConditions(_tokenId, conditions, false); + // } +} diff --git a/src/test/benchmark/DropERC20Benchmark.t.sol b/src/test/benchmark/DropERC20Benchmark.t.sol new file mode 100644 index 000000000..53f432604 --- /dev/null +++ b/src/test/benchmark/DropERC20Benchmark.t.sol @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC20 } from "contracts/prebuilts/drop/DropERC20.sol"; + +// Test imports +import "../utils/BaseTest.sol"; + +contract DropERC20BenchmarkTest is BaseTest { + using Strings for uint256; + using Strings for address; + + DropERC20 public drop; + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + drop = DropERC20(getContract("DropERC20")); + + erc20.mint(deployer, 1_000 ether); + vm.deal(deployer, 1_000 ether); + } + + /*/////////////////////////////////////////////////////////////// + DropERC20 benchmark + //////////////////////////////////////////////////////////////*/ + + function test_benchmark_dropERC20_setClaimConditions_five_conditions() public { + vm.pauseGasMetering(); + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = Strings.toString(uint256(300 ether)); + inputs[3] = Strings.toString(uint256(1 ether)); + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + DropERC20.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300 ether; + alp.pricePerToken = 1 ether; + alp.currency = address(erc20); + + vm.warp(1); + + DropERC20.ClaimCondition[] memory conditions = new DropERC20.ClaimCondition[](5); + conditions[0].maxClaimableSupply = 500 ether; + conditions[0].quantityLimitPerWallet = 10 ether; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 5 ether; + conditions[0].currency = address(erc20); + + conditions[1].maxClaimableSupply = 600; + conditions[1].pricePerToken = 20; + conditions[1].startTimestamp = 100000; + + conditions[2].maxClaimableSupply = 700; + conditions[2].pricePerToken = 30; + conditions[2].startTimestamp = 200000; + + conditions[3].maxClaimableSupply = 800; + conditions[3].pricePerToken = 40; + conditions[3].startTimestamp = 300000; + + conditions[4].maxClaimableSupply = 700; + conditions[4].pricePerToken = 30; + conditions[4].startTimestamp = 400000; + + vm.prank(deployer); + vm.resumeGasMetering(); + drop.setClaimConditions(conditions, false); + } + + function test_benchmark_dropERC20_claim() public { + vm.pauseGasMetering(); + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = Strings.toString(uint256(300 ether)); + inputs[3] = Strings.toString(uint256(1 ether)); + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + DropERC20.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300 ether; + alp.pricePerToken = 1 ether; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + DropERC20.ClaimCondition[] memory conditions = new DropERC20.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500 ether; + conditions[0].quantityLimitPerWallet = 10 ether; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 5 ether; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + erc20.mint(receiver, 1000 ether); + vm.prank(receiver); + erc20.approve(address(drop), 1000 ether); + + vm.prank(receiver, receiver); + vm.resumeGasMetering(); + drop.claim(receiver, 100 ether, address(erc20), 1 ether, alp, ""); + } + + // function test_benchmark_dropERC20_setClaimConditions_one_condition() public { + // vm.pauseGasMetering(); + // string[] memory inputs = new string[](5); + + // inputs[0] = "node"; + // inputs[1] = "src/test/scripts/generateRoot.ts"; + // inputs[2] = Strings.toString(300 ether); + // inputs[3] = Strings.toString(1 ether); + // inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + // bytes memory result = vm.ffi(inputs); + // // revert(); + // bytes32 root = abi.decode(result, (bytes32)); + + // inputs[1] = "src/test/scripts/getProof.ts"; + // result = vm.ffi(inputs); + // bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + // DropERC20.AllowlistProof memory alp; + // alp.proof = proofs; + // alp.quantityLimitPerWallet = 300 ether; + // alp.pricePerToken = 1 ether; + // alp.currency = address(erc20); + + // vm.warp(1); + + // address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + // DropERC20.ClaimCondition[] memory conditions = new DropERC20.ClaimCondition[](1); + // conditions[0].maxClaimableSupply = 500 ether; + // conditions[0].quantityLimitPerWallet = 10 ether; + // conditions[0].merkleRoot = root; + // conditions[0].pricePerToken = 5 ether; + // conditions[0].currency = address(erc20); + + // vm.prank(deployer); + // vm.resumeGasMetering(); + // drop.setClaimConditions(conditions, false); + // } + + // function test_benchmark_dropERC20_setClaimConditions_two_conditions() public { + // vm.pauseGasMetering(); + // string[] memory inputs = new string[](5); + + // inputs[0] = "node"; + // inputs[1] = "src/test/scripts/generateRoot.ts"; + // inputs[2] = Strings.toString(300 ether); + // inputs[3] = Strings.toString(1 ether); + // inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + // bytes memory result = vm.ffi(inputs); + // // revert(); + // bytes32 root = abi.decode(result, (bytes32)); + + // inputs[1] = "src/test/scripts/getProof.ts"; + // result = vm.ffi(inputs); + // bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + // DropERC20.AllowlistProof memory alp; + // alp.proof = proofs; + // alp.quantityLimitPerWallet = 300 ether; + // alp.pricePerToken = 1 ether; + // alp.currency = address(erc20); + + // vm.warp(1); + + // address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + // DropERC20.ClaimCondition[] memory conditions = new DropERC20.ClaimCondition[](2); + // conditions[0].maxClaimableSupply = 500 ether; + // conditions[0].quantityLimitPerWallet = 10 ether; + // conditions[0].merkleRoot = root; + // conditions[0].pricePerToken = 5 ether; + // conditions[0].currency = address(erc20); + + // conditions[1].maxClaimableSupply = 600; + // conditions[1].pricePerToken = 20; + // conditions[1].startTimestamp = 100000; + + // vm.prank(deployer); + // vm.resumeGasMetering(); + // drop.setClaimConditions(conditions, false); + // } + + // function test_benchmark_dropERC20_setClaimConditions_three_conditions() public { + // vm.pauseGasMetering(); + // string[] memory inputs = new string[](5); + + // inputs[0] = "node"; + // inputs[1] = "src/test/scripts/generateRoot.ts"; + // inputs[2] = Strings.toString(300 ether); + // inputs[3] = Strings.toString(1 ether); + // inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + // bytes memory result = vm.ffi(inputs); + // // revert(); + // bytes32 root = abi.decode(result, (bytes32)); + + // inputs[1] = "src/test/scripts/getProof.ts"; + // result = vm.ffi(inputs); + // bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + // DropERC20.AllowlistProof memory alp; + // alp.proof = proofs; + // alp.quantityLimitPerWallet = 300 ether; + // alp.pricePerToken = 1 ether; + // alp.currency = address(erc20); + + // vm.warp(1); + + // address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + // DropERC20.ClaimCondition[] memory conditions = new DropERC20.ClaimCondition[](3); + // conditions[0].maxClaimableSupply = 500 ether; + // conditions[0].quantityLimitPerWallet = 10 ether; + // conditions[0].merkleRoot = root; + // conditions[0].pricePerToken = 5 ether; + // conditions[0].currency = address(erc20); + + // conditions[1].maxClaimableSupply = 600; + // conditions[1].pricePerToken = 20; + // conditions[1].startTimestamp = 100000; + + // conditions[2].maxClaimableSupply = 700; + // conditions[2].pricePerToken = 30; + // conditions[2].startTimestamp = 200000; + + // vm.prank(deployer); + // vm.resumeGasMetering(); + // drop.setClaimConditions(conditions, false); + // } +} diff --git a/src/test/benchmark/DropERC721Benchmark.t.sol b/src/test/benchmark/DropERC721Benchmark.t.sol new file mode 100644 index 000000000..d11507c06 --- /dev/null +++ b/src/test/benchmark/DropERC721Benchmark.t.sol @@ -0,0 +1,458 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC721, IDelayedReveal, ERC721AUpgradeable, IPermissions, ILazyMint } from "contracts/prebuilts/drop/DropERC721.sol"; + +// Test imports +import { IERC721AUpgradeable } from "erc721a-upgradeable/contracts/IERC721AUpgradeable.sol"; +import "../utils/BaseTest.sol"; + +contract DropERC721BenchmarkTest is BaseTest { + using Strings for uint256; + using Strings for address; + + DropERC721 public drop; + + bytes private emptyEncodedBytes = abi.encode("", ""); + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + drop = DropERC721(getContract("DropERC721")); + + erc20.mint(deployer, 1_000 ether); + vm.deal(deployer, 1_000 ether); + } + + /*/////////////////////////////////////////////////////////////// + DropERC721 benchmark + //////////////////////////////////////////////////////////////*/ + + function test_benchmark_dropERC721_claim_five_tokens() public { + vm.pauseGasMetering(); + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "5"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + DropERC721.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 5; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.prank(receiver, receiver); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(drop), 10000); + + vm.prank(receiver, receiver); + vm.resumeGasMetering(); + drop.claim(receiver, 5, address(erc20), 5, alp, ""); + } + + function test_benchmark_dropERC721_setClaimConditions_five_conditions() public { + vm.pauseGasMetering(); + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "5"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + DropERC721.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 5; + alp.currency = address(erc20); + + vm.warp(1); + + DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](5); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + conditions[1].maxClaimableSupply = 600; + conditions[1].pricePerToken = 20; + conditions[1].startTimestamp = 100000; + + conditions[2].maxClaimableSupply = 700; + conditions[2].pricePerToken = 30; + conditions[2].startTimestamp = 200000; + + conditions[3].maxClaimableSupply = 800; + conditions[3].pricePerToken = 40; + conditions[3].startTimestamp = 300000; + + conditions[4].maxClaimableSupply = 700; + conditions[4].pricePerToken = 30; + conditions[4].startTimestamp = 400000; + + vm.prank(deployer); + vm.resumeGasMetering(); + drop.setClaimConditions(conditions, false); + } + + function test_benchmark_dropERC721_lazyMint() public { + vm.pauseGasMetering(); + vm.prank(deployer); + vm.resumeGasMetering(); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + } + + function test_benchmark_dropERC721_lazyMint_for_delayed_reveal() public { + vm.pauseGasMetering(); + uint256 amountToLazyMint = 100; + string memory baseURI = "ipfs://"; + bytes memory encryptedBaseURI = "encryptedBaseURI://"; + bytes32 provenanceHash = bytes32("whatever"); + + vm.prank(deployer); + vm.resumeGasMetering(); + drop.lazyMint(amountToLazyMint, baseURI, abi.encode(encryptedBaseURI, provenanceHash)); + } + + function test_benchmark_dropERC721_reveal() public { + vm.pauseGasMetering(); + + bytes memory key = "key"; + uint256 amountToLazyMint = 100; + bytes memory secretURI = "ipfs://"; + string memory placeholderURI = "abcd://"; + bytes memory encryptedURI = drop.encryptDecrypt(secretURI, key); + bytes32 provenanceHash = keccak256(abi.encodePacked(secretURI, key, block.chainid)); + + vm.prank(deployer); + drop.lazyMint(amountToLazyMint, placeholderURI, abi.encode(encryptedURI, provenanceHash)); + + vm.prank(deployer); + vm.resumeGasMetering(); + drop.reveal(0, key); + } + + // function test_benchmark_dropERC721_claim_one_token() public { + // vm.pauseGasMetering(); + // string[] memory inputs = new string[](5); + + // inputs[0] = "node"; + // inputs[1] = "src/test/scripts/generateRoot.ts"; + // inputs[2] = "300"; + // inputs[3] = "5"; + // inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + // bytes memory result = vm.ffi(inputs); + // // revert(); + // bytes32 root = abi.decode(result, (bytes32)); + + // inputs[1] = "src/test/scripts/getProof.ts"; + // result = vm.ffi(inputs); + // bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + // DropERC721.AllowlistProof memory alp; + // alp.proof = proofs; + // alp.quantityLimitPerWallet = 300; + // alp.pricePerToken = 5; + // alp.currency = address(erc20); + + // vm.warp(1); + + // address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + // DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](1); + // conditions[0].maxClaimableSupply = 500; + // conditions[0].quantityLimitPerWallet = 10; + // conditions[0].merkleRoot = root; + // conditions[0].pricePerToken = 10; + // conditions[0].currency = address(erc20); + + // vm.prank(deployer); + // drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + // vm.prank(deployer); + // drop.setClaimConditions(conditions, false); + + // vm.prank(receiver, receiver); + + // erc20.mint(receiver, 10000); + // vm.prank(receiver); + // erc20.approve(address(drop), 10000); + + // vm.prank(receiver, receiver); + // vm.resumeGasMetering(); + // drop.claim(receiver, 1, address(erc20), 5, alp, ""); + // } + + // function test_benchmark_dropERC721_claim_two_tokens() public { + // vm.pauseGasMetering(); + // string[] memory inputs = new string[](5); + + // inputs[0] = "node"; + // inputs[1] = "src/test/scripts/generateRoot.ts"; + // inputs[2] = "300"; + // inputs[3] = "5"; + // inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + // bytes memory result = vm.ffi(inputs); + // // revert(); + // bytes32 root = abi.decode(result, (bytes32)); + + // inputs[1] = "src/test/scripts/getProof.ts"; + // result = vm.ffi(inputs); + // bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + // DropERC721.AllowlistProof memory alp; + // alp.proof = proofs; + // alp.quantityLimitPerWallet = 300; + // alp.pricePerToken = 5; + // alp.currency = address(erc20); + + // vm.warp(1); + + // address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + // DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](1); + // conditions[0].maxClaimableSupply = 500; + // conditions[0].quantityLimitPerWallet = 10; + // conditions[0].merkleRoot = root; + // conditions[0].pricePerToken = 10; + // conditions[0].currency = address(erc20); + + // vm.prank(deployer); + // drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + // vm.prank(deployer); + // drop.setClaimConditions(conditions, false); + + // vm.prank(receiver, receiver); + + // erc20.mint(receiver, 10000); + // vm.prank(receiver); + // erc20.approve(address(drop), 10000); + + // vm.prank(receiver, receiver); + // vm.resumeGasMetering(); + // drop.claim(receiver, 2, address(erc20), 5, alp, ""); + // } + + // function test_benchmark_dropERC721_claim_three_tokens() public { + // vm.pauseGasMetering(); + // string[] memory inputs = new string[](5); + + // inputs[0] = "node"; + // inputs[1] = "src/test/scripts/generateRoot.ts"; + // inputs[2] = "300"; + // inputs[3] = "5"; + // inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + // bytes memory result = vm.ffi(inputs); + // // revert(); + // bytes32 root = abi.decode(result, (bytes32)); + + // inputs[1] = "src/test/scripts/getProof.ts"; + // result = vm.ffi(inputs); + // bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + // DropERC721.AllowlistProof memory alp; + // alp.proof = proofs; + // alp.quantityLimitPerWallet = 300; + // alp.pricePerToken = 5; + // alp.currency = address(erc20); + + // vm.warp(1); + + // address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + // DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](1); + // conditions[0].maxClaimableSupply = 500; + // conditions[0].quantityLimitPerWallet = 10; + // conditions[0].merkleRoot = root; + // conditions[0].pricePerToken = 10; + // conditions[0].currency = address(erc20); + + // vm.prank(deployer); + // drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + // vm.prank(deployer); + // drop.setClaimConditions(conditions, false); + + // vm.prank(receiver, receiver); + + // erc20.mint(receiver, 10000); + // vm.prank(receiver); + // erc20.approve(address(drop), 10000); + + // vm.prank(receiver, receiver); + // vm.resumeGasMetering(); + // drop.claim(receiver, 3, address(erc20), 5, alp, ""); + // } + + // function test_benchmark_dropERC721_setClaimConditions_one_condition() public { + // vm.pauseGasMetering(); + // string[] memory inputs = new string[](5); + + // inputs[0] = "node"; + // inputs[1] = "src/test/scripts/generateRoot.ts"; + // inputs[2] = "300"; + // inputs[3] = "5"; + // inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + // bytes memory result = vm.ffi(inputs); + // // revert(); + // bytes32 root = abi.decode(result, (bytes32)); + + // inputs[1] = "src/test/scripts/getProof.ts"; + // result = vm.ffi(inputs); + // bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + // DropERC721.AllowlistProof memory alp; + // alp.proof = proofs; + // alp.quantityLimitPerWallet = 300; + // alp.pricePerToken = 5; + // alp.currency = address(erc20); + + // vm.warp(1); + + // address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + // DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](1); + // conditions[0].maxClaimableSupply = 500; + // conditions[0].quantityLimitPerWallet = 10; + // conditions[0].merkleRoot = root; + // conditions[0].pricePerToken = 10; + // conditions[0].currency = address(erc20); + + // vm.prank(deployer); + // vm.resumeGasMetering(); + // drop.setClaimConditions(conditions, false); + // } + + // function test_benchmark_dropERC721_setClaimConditions_two_conditions() public { + // vm.pauseGasMetering(); + // string[] memory inputs = new string[](5); + + // inputs[0] = "node"; + // inputs[1] = "src/test/scripts/generateRoot.ts"; + // inputs[2] = "300"; + // inputs[3] = "5"; + // inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + // bytes memory result = vm.ffi(inputs); + // // revert(); + // bytes32 root = abi.decode(result, (bytes32)); + + // inputs[1] = "src/test/scripts/getProof.ts"; + // result = vm.ffi(inputs); + // bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + // DropERC721.AllowlistProof memory alp; + // alp.proof = proofs; + // alp.quantityLimitPerWallet = 300; + // alp.pricePerToken = 5; + // alp.currency = address(erc20); + + // vm.warp(1); + + // address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + // DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](2); + // conditions[0].maxClaimableSupply = 500; + // conditions[0].quantityLimitPerWallet = 10; + // conditions[0].merkleRoot = root; + // conditions[0].pricePerToken = 10; + // conditions[0].currency = address(erc20); + + // conditions[1].maxClaimableSupply = 600; + // conditions[1].pricePerToken = 20; + // conditions[1].startTimestamp = 100000; + + // vm.prank(deployer); + // vm.resumeGasMetering(); + // drop.setClaimConditions(conditions, false); + // } + + // function test_benchmark_dropERC721_setClaimConditions_three_conditions() public { + // vm.pauseGasMetering(); + // string[] memory inputs = new string[](5); + + // inputs[0] = "node"; + // inputs[1] = "src/test/scripts/generateRoot.ts"; + // inputs[2] = "300"; + // inputs[3] = "5"; + // inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + // bytes memory result = vm.ffi(inputs); + // // revert(); + // bytes32 root = abi.decode(result, (bytes32)); + + // inputs[1] = "src/test/scripts/getProof.ts"; + // result = vm.ffi(inputs); + // bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + // DropERC721.AllowlistProof memory alp; + // alp.proof = proofs; + // alp.quantityLimitPerWallet = 300; + // alp.pricePerToken = 5; + // alp.currency = address(erc20); + + // vm.warp(1); + + // address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + // DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](3); + // conditions[0].maxClaimableSupply = 500; + // conditions[0].quantityLimitPerWallet = 10; + // conditions[0].merkleRoot = root; + // conditions[0].pricePerToken = 10; + // conditions[0].currency = address(erc20); + + // conditions[1].maxClaimableSupply = 600; + // conditions[1].pricePerToken = 20; + // conditions[1].startTimestamp = 100000; + + // conditions[2].maxClaimableSupply = 700; + // conditions[2].pricePerToken = 30; + // conditions[2].startTimestamp = 200000; + + // vm.prank(deployer); + // vm.resumeGasMetering(); + // drop.setClaimConditions(conditions, false); + // } +} diff --git a/src/test/benchmark/EditionStakeBenchmark.t.sol b/src/test/benchmark/EditionStakeBenchmark.t.sol new file mode 100644 index 000000000..f8b604dc4 --- /dev/null +++ b/src/test/benchmark/EditionStakeBenchmark.t.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { EditionStake } from "contracts/prebuilts/staking/EditionStake.sol"; + +// Test imports +import "../utils/BaseTest.sol"; + +contract EditionStakeBenchmarkTest is BaseTest { + EditionStake internal stakeContract; + + address internal stakerOne; + address internal stakerTwo; + + uint256 internal defaultTimeUnit; + uint256 internal defaultRewardsPerUnitTime; + + function setUp() public override { + super.setUp(); + + defaultTimeUnit = 60; + defaultRewardsPerUnitTime = 1; + + stakerOne = address(0x345); + stakerTwo = address(0x567); + + erc1155.mint(stakerOne, 0, 100); // mint 100 tokens with id 0 to stakerOne + erc1155.mint(stakerOne, 1, 100); // mint 100 tokens with id 1 to stakerOne + + erc1155.mint(stakerTwo, 0, 100); // mint 100 tokens with id 0 to stakerTwo + erc1155.mint(stakerTwo, 1, 100); // mint 100 tokens with id 1 to stakerTwo + + erc20.mint(deployer, 1000 ether); // mint reward tokens to contract admin + + stakeContract = EditionStake(payable(getContract("EditionStake"))); + + // set approvals + vm.prank(stakerOne); + erc1155.setApprovalForAll(address(stakeContract), true); + + vm.prank(stakerTwo); + erc1155.setApprovalForAll(address(stakeContract), true); + + vm.startPrank(deployer); + erc20.approve(address(stakeContract), type(uint256).max); + stakeContract.depositRewardTokens(100 ether); + // erc20.transfer(address(stakeContract), 100 ether); + vm.stopPrank(); + assertEq(stakeContract.getRewardTokenBalance(), 100 ether); + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: EditionStake + //////////////////////////////////////////////////////////////*/ + + function test_benchmark_editionStake_stake() public { + vm.pauseGasMetering(); + + vm.warp(1); + vm.prank(stakerOne); + vm.resumeGasMetering(); + stakeContract.stake(0, 50); + } + + function test_benchmark_editionStake_claimRewards() public { + vm.pauseGasMetering(); + vm.warp(1); + + // stake 50 tokens with token-id 0 + vm.prank(stakerOne); + stakeContract.stake(0, 50); + + //=================== warp timestamp to claim rewards + vm.roll(100); + vm.warp(1000); + + vm.prank(stakerOne); + vm.resumeGasMetering(); + stakeContract.claimRewards(0); + } + + function test_benchmark_editionStake_withdraw() public { + vm.pauseGasMetering(); + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(0, 50); + + vm.prank(stakerTwo); + stakeContract.stake(1, 20); + + //========== warp timestamp before withdraw + vm.roll(100); + vm.warp(1000); + + // withdraw partially for stakerOne + vm.prank(stakerOne); + vm.resumeGasMetering(); + stakeContract.withdraw(0, 40); + } +} diff --git a/src/test/benchmark/MultiwrapBenchmark.t.sol b/src/test/benchmark/MultiwrapBenchmark.t.sol new file mode 100644 index 000000000..aef57eccc --- /dev/null +++ b/src/test/benchmark/MultiwrapBenchmark.t.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { Multiwrap } from "contracts/prebuilts/multiwrap/Multiwrap.sol"; +import { ITokenBundle } from "contracts/extension/interface/ITokenBundle.sol"; + +// Test imports +import { MockERC20 } from "../mocks/MockERC20.sol"; +import { Wallet } from "../utils/Wallet.sol"; +import "../utils/BaseTest.sol"; + +contract MultiwrapBenchmarkTest is BaseTest { + /// @dev Emitted when tokens are wrapped. + event TokensWrapped( + address indexed wrapper, + address indexed recipientOfWrappedToken, + uint256 indexed tokenIdOfWrappedToken, + ITokenBundle.Token[] wrappedContents + ); + + /// @dev Emitted when tokens are unwrapped. + event TokensUnwrapped( + address indexed unwrapper, + address indexed recipientOfWrappedContents, + uint256 indexed tokenIdOfWrappedToken + ); + + /*/////////////////////////////////////////////////////////////// + Setup + //////////////////////////////////////////////////////////////*/ + + Multiwrap internal multiwrap; + + Wallet internal tokenOwner; + string internal uriForWrappedToken; + ITokenBundle.Token[] internal wrappedContent; + + function setUp() public override { + super.setUp(); + + // Get target contract + multiwrap = Multiwrap(payable(getContract("Multiwrap"))); + + // Set test vars + tokenOwner = getWallet(); + uriForWrappedToken = "ipfs://baseURI/"; + + wrappedContent.push( + ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 10 ether + }) + ); + wrappedContent.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 0, + totalAmount: 1 + }) + ); + wrappedContent.push( + ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 0, + totalAmount: 100 + }) + ); + + // Mint tokens-to-wrap to `tokenOwner` + erc20.mint(address(tokenOwner), 10 ether); + erc721.mint(address(tokenOwner), 1); + erc1155.mint(address(tokenOwner), 0, 100); + + // Token owner approves `Multiwrap` to transfer tokens. + tokenOwner.setAllowanceERC20(address(erc20), address(multiwrap), type(uint256).max); + tokenOwner.setApprovalForAllERC721(address(erc721), address(multiwrap), true); + tokenOwner.setApprovalForAllERC1155(address(erc1155), address(multiwrap), true); + + // Grant MINTER_ROLE / requisite wrapping permissions to `tokenOwer` + vm.prank(deployer); + multiwrap.grantRole(keccak256("MINTER_ROLE"), address(tokenOwner)); + } + + /*/////////////////////////////////////////////////////////////// + Multiwrap benchmark + //////////////////////////////////////////////////////////////*/ + + function test_benchmark_multiwrap_wrap() public { + vm.pauseGasMetering(); + address recipient = address(0x123); + vm.prank(address(tokenOwner)); + vm.resumeGasMetering(); + multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); + } + + function test_benchmark_multiwrap_unwrap() public { + vm.pauseGasMetering(); + // ===== setup: wrap tokens ===== + uint256 expectedIdForWrappedToken = multiwrap.nextTokenIdToMint(); + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); + + // ===== target test content ===== + + vm.prank(recipient); + vm.resumeGasMetering(); + multiwrap.unwrap(expectedIdForWrappedToken, recipient); + } +} diff --git a/src/test/benchmark/NFTStakeBenchmark.t.sol b/src/test/benchmark/NFTStakeBenchmark.t.sol new file mode 100644 index 000000000..0f81eef3d --- /dev/null +++ b/src/test/benchmark/NFTStakeBenchmark.t.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { NFTStake } from "contracts/prebuilts/staking/NFTStake.sol"; + +// Test imports +import "../utils/BaseTest.sol"; + +contract NFTStakeBenchmarkTest is BaseTest { + NFTStake internal stakeContract; + + address internal stakerOne; + address internal stakerTwo; + + uint256 internal timeUnit; + uint256 internal rewardsPerUnitTime; + + function setUp() public override { + super.setUp(); + + timeUnit = 60; + rewardsPerUnitTime = 1; + + stakerOne = address(0x345); + stakerTwo = address(0x567); + + erc721.mint(stakerOne, 5); // mint token id 0 to 4 + erc721.mint(stakerTwo, 5); // mint token id 5 to 9 + erc20.mint(deployer, 1000 ether); // mint reward tokens to contract admin + + stakeContract = NFTStake(payable(getContract("NFTStake"))); + + // set approvals + vm.prank(stakerOne); + erc721.setApprovalForAll(address(stakeContract), true); + + vm.prank(stakerTwo); + erc721.setApprovalForAll(address(stakeContract), true); + + vm.startPrank(deployer); + erc20.approve(address(stakeContract), type(uint256).max); + stakeContract.depositRewardTokens(100 ether); + // erc20.transfer(address(stakeContract), 100 ether); + vm.stopPrank(); + assertEq(stakeContract.getRewardTokenBalance(), 100 ether); + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: NFTStake + //////////////////////////////////////////////////////////////*/ + + function test_benchmark_nftStake_stake_five_tokens() public { + vm.pauseGasMetering(); + + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](5); + _tokenIdsOne[0] = 0; + _tokenIdsOne[1] = 1; + _tokenIdsOne[2] = 2; + _tokenIdsOne[3] = 3; + _tokenIdsOne[4] = 4; + + // stake 3 tokens + vm.prank(stakerOne); + vm.resumeGasMetering(); + stakeContract.stake(_tokenIdsOne); + } + + function test_benchmark_nftStake_claimRewards() public { + vm.pauseGasMetering(); + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](3); + _tokenIdsOne[0] = 0; + _tokenIdsOne[1] = 1; + _tokenIdsOne[2] = 2; + + // stake 3 tokens + vm.prank(stakerOne); + stakeContract.stake(_tokenIdsOne); + + //=================== warp timestamp to claim rewards + vm.roll(100); + vm.warp(1000); + vm.prank(stakerOne); + vm.resumeGasMetering(); + stakeContract.claimRewards(); + } + + function test_benchmark_nftStake_withdraw() public { + vm.pauseGasMetering(); + //================ first staker ====================== + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](3); + _tokenIdsOne[0] = 0; + _tokenIdsOne[1] = 1; + _tokenIdsOne[2] = 2; + + // stake 3 tokens + vm.prank(stakerOne); + stakeContract.stake(_tokenIdsOne); + + //========== warp timestamp before withdraw + vm.roll(100); + vm.warp(1000); + + uint256[] memory _tokensToWithdraw = new uint256[](1); + _tokensToWithdraw[0] = 1; + + vm.prank(stakerOne); + vm.resumeGasMetering(); + stakeContract.withdraw(_tokensToWithdraw); + } + + // function test_benchmark_nftStake_stake_one_token() public { + // vm.pauseGasMetering(); + + // vm.warp(1); + // uint256[] memory _tokenIdsOne = new uint256[](1); + // _tokenIdsOne[0] = 0; + + // // stake 3 tokens + // vm.prank(stakerOne); + // vm.resumeGasMetering(); + // stakeContract.stake(_tokenIdsOne); + // } + + // function test_benchmark_nftStake_stake_two_tokens() public { + // vm.pauseGasMetering(); + + // vm.warp(1); + // uint256[] memory _tokenIdsOne = new uint256[](2); + // _tokenIdsOne[0] = 0; + // _tokenIdsOne[1] = 1; + + // // stake 3 tokens + // vm.prank(stakerOne); + // vm.resumeGasMetering(); + // stakeContract.stake(_tokenIdsOne); + // } + + // function test_benchmark_nftStake_stake_three_tokens() public { + // vm.pauseGasMetering(); + + // vm.warp(1); + // uint256[] memory _tokenIdsOne = new uint256[](3); + // _tokenIdsOne[0] = 0; + // _tokenIdsOne[1] = 1; + // _tokenIdsOne[2] = 2; + + // // stake 3 tokens + // vm.prank(stakerOne); + // vm.resumeGasMetering(); + // stakeContract.stake(_tokenIdsOne); + // } +} diff --git a/src/test/benchmark/PackBenchmark.t.sol b/src/test/benchmark/PackBenchmark.t.sol new file mode 100644 index 000000000..5048713b0 --- /dev/null +++ b/src/test/benchmark/PackBenchmark.t.sol @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { Pack, IERC2981Upgradeable, IERC721Receiver, IERC1155Upgradeable } from "contracts/prebuilts/pack/Pack.sol"; +import { IPack } from "contracts/prebuilts/interface/IPack.sol"; +import { ITokenBundle } from "contracts/extension/interface/ITokenBundle.sol"; + +// Test imports +import { MockERC20 } from "../mocks/MockERC20.sol"; +import { Wallet } from "../utils/Wallet.sol"; +import "../utils/BaseTest.sol"; + +contract PackBenchmarkTest is BaseTest { + /// @notice Emitted when a set of packs is created. + event PackCreated(uint256 indexed packId, address recipient, uint256 totalPacksCreated); + + /// @notice Emitted when a pack is opened. + event PackOpened( + uint256 indexed packId, + address indexed opener, + uint256 numOfPacksOpened, + ITokenBundle.Token[] rewardUnitsDistributed + ); + + Pack internal pack; + + Wallet internal tokenOwner; + string internal packUri; + ITokenBundle.Token[] internal packContents; + ITokenBundle.Token[] internal additionalContents; + uint256[] internal numOfRewardUnits; + uint256[] internal additionalContentsRewardUnits; + + function setUp() public override { + super.setUp(); + + pack = Pack(payable(getContract("Pack"))); + + tokenOwner = getWallet(); + packUri = "ipfs://"; + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 0, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 0, + totalAmount: 100 + }) + ); + numOfRewardUnits.push(20); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 1000 ether + }) + ); + numOfRewardUnits.push(50); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 1, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 2, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 1000 ether + }) + ); + numOfRewardUnits.push(100); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 3, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 4, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 5, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 1, + totalAmount: 500 + }) + ); + numOfRewardUnits.push(50); + + erc20.mint(address(tokenOwner), 2000 ether); + erc721.mint(address(tokenOwner), 6); + erc1155.mint(address(tokenOwner), 0, 100); + erc1155.mint(address(tokenOwner), 1, 500); + + // additional contents, to check `addPackContents` + additionalContents.push( + ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 2, + totalAmount: 200 + }) + ); + additionalContentsRewardUnits.push(50); + + additionalContents.push( + ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 1000 ether + }) + ); + additionalContentsRewardUnits.push(100); + + tokenOwner.setAllowanceERC20(address(erc20), address(pack), type(uint256).max); + tokenOwner.setApprovalForAllERC721(address(erc721), address(pack), true); + tokenOwner.setApprovalForAllERC1155(address(erc1155), address(pack), true); + + vm.prank(deployer); + pack.grantRole(keccak256("MINTER_ROLE"), address(tokenOwner)); + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: Pack + //////////////////////////////////////////////////////////////*/ + + function test_benchmark_pack_createPack() public { + vm.pauseGasMetering(); + address recipient = address(1); + + vm.prank(address(tokenOwner)); + vm.resumeGasMetering(); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + function test_benchmark_pack_addPackContents() public { + vm.pauseGasMetering(); + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(1); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); + assertEq(packed.length, packContents.length); + + erc20.mint(address(tokenOwner), 1000 ether); + erc1155.mint(address(tokenOwner), 2, 200); + + vm.prank(address(tokenOwner)); + vm.resumeGasMetering(); + pack.addPackContents(packId, additionalContents, additionalContentsRewardUnits, recipient); + } + + function test_benchmark_pack_openPack() public { + vm.pauseGasMetering(); + vm.warp(1000); + uint256 packId = pack.nextTokenIdToMint(); + uint256 packsToOpen = 3; + address recipient = address(1); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); + + vm.prank(recipient, recipient); + vm.resumeGasMetering(); + pack.openPack(packId, packsToOpen); + } +} diff --git a/src/test/benchmark/PackVRFDirectBenchmark.t.sol b/src/test/benchmark/PackVRFDirectBenchmark.t.sol new file mode 100644 index 000000000..b20af8f7d --- /dev/null +++ b/src/test/benchmark/PackVRFDirectBenchmark.t.sol @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { PackVRFDirect, IERC2981Upgradeable, IERC721Receiver, IERC1155Upgradeable } from "contracts/prebuilts/pack/PackVRFDirect.sol"; +import { IPack } from "contracts/prebuilts/interface/IPack.sol"; +import { ITokenBundle } from "contracts/extension/interface/ITokenBundle.sol"; + +// Test imports +import { MockERC20 } from "../mocks/MockERC20.sol"; +import { Wallet } from "../utils/Wallet.sol"; +import "../utils/BaseTest.sol"; + +contract PackVRFDirectBenchmarkTest is BaseTest { + /// @notice Emitted when a set of packs is created. + event PackCreated(uint256 indexed packId, address recipient, uint256 totalPacksCreated); + + /// @notice Emitted when the opening of a pack is requested. + event PackOpenRequested(address indexed opener, uint256 indexed packId, uint256 amountToOpen, uint256 requestId); + + /// @notice Emitted when Chainlink VRF fulfills a random number request. + event PackRandomnessFulfilled(uint256 indexed packId, uint256 indexed requestId); + + /// @notice Emitted when a pack is opened. + event PackOpened( + uint256 indexed packId, + address indexed opener, + uint256 numOfPacksOpened, + ITokenBundle.Token[] rewardUnitsDistributed + ); + + PackVRFDirect internal pack; + + Wallet internal tokenOwner; + string internal packUri; + ITokenBundle.Token[] internal packContents; + ITokenBundle.Token[] internal additionalContents; + uint256[] internal numOfRewardUnits; + uint256[] internal additionalContentsRewardUnits; + + function setUp() public virtual override { + super.setUp(); + + pack = PackVRFDirect(payable(getContract("PackVRFDirect"))); + + tokenOwner = getWallet(); + packUri = "ipfs://"; + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 0, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 0, + totalAmount: 100 + }) + ); + numOfRewardUnits.push(20); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 1000 ether + }) + ); + numOfRewardUnits.push(50); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 1, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 2, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 1000 ether + }) + ); + numOfRewardUnits.push(100); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 3, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 4, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 5, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 1, + totalAmount: 500 + }) + ); + numOfRewardUnits.push(50); + + erc20.mint(address(tokenOwner), 2000 ether); + erc721.mint(address(tokenOwner), 6); + erc1155.mint(address(tokenOwner), 0, 100); + erc1155.mint(address(tokenOwner), 1, 500); + + // additional contents, to check `addPackContents` + additionalContents.push( + ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 2, + totalAmount: 200 + }) + ); + additionalContentsRewardUnits.push(50); + + additionalContents.push( + ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 1000 ether + }) + ); + additionalContentsRewardUnits.push(100); + + tokenOwner.setAllowanceERC20(address(erc20), address(pack), type(uint256).max); + tokenOwner.setApprovalForAllERC721(address(erc721), address(pack), true); + tokenOwner.setApprovalForAllERC1155(address(erc1155), address(pack), true); + + vm.prank(deployer); + pack.grantRole(keccak256("MINTER_ROLE"), address(tokenOwner)); + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: PackVRFDirect + //////////////////////////////////////////////////////////////*/ + + function test_benchmark_packvrf_createPack() public { + vm.pauseGasMetering(); + address recipient = address(1); + vm.prank(address(tokenOwner)); + vm.resumeGasMetering(); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + function test_benchmark_packvrf_openPackAndClaimRewards() public { + vm.pauseGasMetering(); + vm.warp(1000); + address recipient = address(1); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); + + vm.prank(recipient, recipient); + vm.resumeGasMetering(); + } + + function test_benchmark_packvrf_openPack() public { + vm.pauseGasMetering(); + vm.warp(1000); + uint256 packId = pack.nextTokenIdToMint(); + uint256 packsToOpen = 3; + address recipient = address(1); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); + + vm.prank(recipient, recipient); + vm.resumeGasMetering(); + pack.openPack(packId, packsToOpen); + } +} diff --git a/src/test/benchmark/SignatureDropBenchmark.t.sol b/src/test/benchmark/SignatureDropBenchmark.t.sol new file mode 100644 index 000000000..1c6607374 --- /dev/null +++ b/src/test/benchmark/SignatureDropBenchmark.t.sol @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { SignatureDrop, IDropSinglePhase, IDelayedReveal, ISignatureMintERC721, ERC721AUpgradeable, IPermissions, ILazyMint } from "contracts/prebuilts/signature-drop/SignatureDrop.sol"; + +// Test imports +import "erc721a-upgradeable/contracts/IERC721AUpgradeable.sol"; +import "../utils/BaseTest.sol"; + +contract SignatureDropBenchmarkTest is BaseTest { + using Strings for uint256; + using Strings for address; + + event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI); + event TokenURIRevealed(uint256 indexed index, string revealedURI); + event TokensMintedWithSignature( + address indexed signer, + address indexed mintedTo, + uint256 indexed tokenIdMinted, + SignatureDrop.MintRequest mintRequest + ); + + SignatureDrop public sigdrop; + address internal deployerSigner; + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + bytes private emptyEncodedBytes = abi.encode("", ""); + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + deployerSigner = signer; + sigdrop = SignatureDrop(getContract("SignatureDrop")); + + erc20.mint(deployerSigner, 1_000 ether); + vm.deal(deployerSigner, 1_000 ether); + + typehashMintRequest = keccak256( + "MintRequest(address to,address royaltyRecipient,uint256 royaltyBps,address primarySaleRecipient,string uri,uint256 quantity,uint256 pricePerToken,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + nameHash = keccak256(bytes("SignatureMintERC721")); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256(abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(sigdrop))); + } + + /*/////////////////////////////////////////////////////////////// + SignatureDrop benchmark + //////////////////////////////////////////////////////////////*/ + + function test_benchmark_signatureDrop_claim_five_tokens() public { + vm.pauseGasMetering(); + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + SignatureDrop.AllowlistProof memory alp; + alp.proof = proofs; + + SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployerSigner); + sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + vm.prank(deployerSigner); + sigdrop.setClaimConditions(conditions[0], false); + + vm.prank(getActor(5), getActor(5)); + vm.resumeGasMetering(); + sigdrop.claim(receiver, 5, address(0), 0, alp, ""); + } + + function test_benchmark_signatureDrop_setClaimConditions() public { + vm.pauseGasMetering(); + vm.warp(1); + bytes32[] memory proofs = new bytes32[](0); + + SignatureDrop.AllowlistProof memory alp; + alp.proof = proofs; + + SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployerSigner); + sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + vm.prank(deployerSigner); + vm.resumeGasMetering(); + sigdrop.setClaimConditions(conditions[0], false); + } + + function test_benchmark_signatureDrop_lazyMint() public { + vm.pauseGasMetering(); + vm.prank(deployerSigner); + vm.resumeGasMetering(); + sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); + } + + function test_benchmark_signatureDrop_lazyMint_for_delayed_reveal() public { + vm.pauseGasMetering(); + uint256 amountToLazyMint = 100; + string memory baseURI = "ipfs://"; + bytes memory encryptedBaseURI = "encryptedBaseURI://"; + bytes32 provenanceHash = bytes32("whatever"); + + vm.prank(deployerSigner); + vm.resumeGasMetering(); + sigdrop.lazyMint(amountToLazyMint, baseURI, abi.encode(encryptedBaseURI, provenanceHash)); + } + + function test_benchmark_signatureDrop_reveal() public { + vm.pauseGasMetering(); + + bytes memory key = "key"; + uint256 amountToLazyMint = 100; + bytes memory secretURI = "ipfs://"; + string memory placeholderURI = "abcd://"; + bytes memory encryptedURI = sigdrop.encryptDecrypt(secretURI, key); + bytes32 provenanceHash = keccak256(abi.encodePacked(secretURI, key, block.chainid)); + + vm.prank(deployerSigner); + sigdrop.lazyMint(amountToLazyMint, placeholderURI, abi.encode(encryptedURI, provenanceHash)); + + vm.prank(deployerSigner); + vm.resumeGasMetering(); + sigdrop.reveal(0, key); + } + + // function test_benchmark_signatureDrop_claim_one_token() public { + // vm.pauseGasMetering(); + // vm.warp(1); + + // address receiver = getActor(0); + // bytes32[] memory proofs = new bytes32[](0); + + // SignatureDrop.AllowlistProof memory alp; + // alp.proof = proofs; + + // SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); + // conditions[0].maxClaimableSupply = 100; + // conditions[0].quantityLimitPerWallet = 100; + + // vm.prank(deployerSigner); + // sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // vm.prank(deployerSigner); + // sigdrop.setClaimConditions(conditions[0], false); + + // vm.prank(getActor(5), getActor(5)); + // vm.resumeGasMetering(); + // sigdrop.claim(receiver, 1, address(0), 0, alp, ""); + // } + + // function test_benchmark_signatureDrop_claim_two_tokens() public { + // vm.pauseGasMetering(); + // vm.warp(1); + + // address receiver = getActor(0); + // bytes32[] memory proofs = new bytes32[](0); + + // SignatureDrop.AllowlistProof memory alp; + // alp.proof = proofs; + + // SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); + // conditions[0].maxClaimableSupply = 100; + // conditions[0].quantityLimitPerWallet = 100; + + // vm.prank(deployerSigner); + // sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // vm.prank(deployerSigner); + // sigdrop.setClaimConditions(conditions[0], false); + + // vm.prank(getActor(5), getActor(5)); + // vm.resumeGasMetering(); + // sigdrop.claim(receiver, 2, address(0), 0, alp, ""); + // } + + // function test_benchmark_signatureDrop_claim_three_tokens() public { + // vm.pauseGasMetering(); + // vm.warp(1); + + // address receiver = getActor(0); + // bytes32[] memory proofs = new bytes32[](0); + + // SignatureDrop.AllowlistProof memory alp; + // alp.proof = proofs; + + // SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); + // conditions[0].maxClaimableSupply = 100; + // conditions[0].quantityLimitPerWallet = 100; + + // vm.prank(deployerSigner); + // sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // vm.prank(deployerSigner); + // sigdrop.setClaimConditions(conditions[0], false); + + // vm.prank(getActor(5), getActor(5)); + // vm.resumeGasMetering(); + // sigdrop.claim(receiver, 3, address(0), 0, alp, ""); + // } +} diff --git a/src/test/benchmark/TokenERC1155Benchmark.t.sol b/src/test/benchmark/TokenERC1155Benchmark.t.sol new file mode 100644 index 000000000..4cd2563d0 --- /dev/null +++ b/src/test/benchmark/TokenERC1155Benchmark.t.sol @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { TokenERC1155, IPlatformFee } from "contracts/prebuilts/token/TokenERC1155.sol"; + +// Test imports +import "../utils/BaseTest.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; + +contract TokenERC1155BenchmarkTest is BaseTest { + using Strings for uint256; + + event TokensMinted(address indexed mintedTo, uint256 indexed tokenIdMinted, string uri, uint256 quantityMinted); + event TokensMintedWithSignature( + address indexed signer, + address indexed mintedTo, + uint256 indexed tokenIdMinted, + TokenERC1155.MintRequest mintRequest + ); + event OwnerUpdated(address indexed prevOwner, address indexed newOwner); + event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps); + event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, uint256 royaltyBps); + event PrimarySaleRecipientUpdated(address indexed recipient); + event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps); + + TokenERC1155 public tokenContract; + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + bytes private emptyEncodedBytes = abi.encode("", ""); + + TokenERC1155.MintRequest _mintrequest; + bytes _signature; + + address internal deployerSigner; + address internal recipient; + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + deployerSigner = signer; + recipient = address(0x123); + tokenContract = TokenERC1155(getContract("TokenERC1155")); + + erc20.mint(deployerSigner, 1_000); + vm.deal(deployerSigner, 1_000); + + erc20.mint(recipient, 1_000); + vm.deal(recipient, 1_000); + + typehashMintRequest = keccak256( + "MintRequest(address to,address royaltyRecipient,uint256 royaltyBps,address primarySaleRecipient,uint256 tokenId,string uri,uint256 quantity,uint256 pricePerToken,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + nameHash = keccak256(bytes("TokenERC1155")); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256( + abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(tokenContract)) + ); + + // construct default mintrequest + _mintrequest.to = recipient; + _mintrequest.royaltyRecipient = royaltyRecipient; + _mintrequest.royaltyBps = royaltyBps; + _mintrequest.primarySaleRecipient = saleRecipient; + _mintrequest.tokenId = type(uint256).max; + _mintrequest.uri = "ipfs://"; + _mintrequest.quantity = 100; + _mintrequest.pricePerToken = 0; + _mintrequest.currency = address(0); + _mintrequest.validityStartTimestamp = 1000; + _mintrequest.validityEndTimestamp = 2000; + _mintrequest.uid = bytes32(0); + + _signature = signMintRequest(_mintrequest, privateKey); + } + + function signMintRequest( + TokenERC1155.MintRequest memory _request, + uint256 _privateKey + ) internal view returns (bytes memory) { + bytes memory encodedRequest = bytes.concat( + abi.encode( + typehashMintRequest, + _request.to, + _request.royaltyRecipient, + _request.royaltyBps, + _request.primarySaleRecipient, + _request.tokenId, + keccak256(bytes(_request.uri)) + ), + abi.encode( + _request.quantity, + _request.pricePerToken, + _request.currency, + _request.validityStartTimestamp, + _request.validityEndTimestamp, + _request.uid + ) + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, typedDataHash); + bytes memory sig = abi.encodePacked(r, s, v); + + return sig; + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: TokenERC1155 + //////////////////////////////////////////////////////////////*/ + + function test_benchmark_tokenERC1155_mintWithSignature_pay_with_ERC20() public { + vm.pauseGasMetering(); + vm.warp(1000); + + // update mintrequest data + _mintrequest.pricePerToken = 1; + _mintrequest.currency = address(erc20); + _signature = signMintRequest(_mintrequest, privateKey); + + // approve erc20 tokens to tokenContract + vm.prank(recipient); + erc20.approve(address(tokenContract), _mintrequest.pricePerToken * _mintrequest.quantity); + + // mint with signature + vm.prank(recipient); + vm.resumeGasMetering(); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_benchmark_tokenERC1155_mintWithSignature_pay_with_native_token() public { + vm.pauseGasMetering(); + vm.warp(1000); + + // update mintrequest data + _mintrequest.pricePerToken = 1; + _mintrequest.currency = address(NATIVE_TOKEN); + _signature = signMintRequest(_mintrequest, privateKey); + + // mint with signature + vm.prank(recipient); + vm.resumeGasMetering(); + tokenContract.mintWithSignature{ value: _mintrequest.pricePerToken * _mintrequest.quantity }( + _mintrequest, + _signature + ); + } + + function test_benchmark_tokenERC1155_mintTo() public { + vm.pauseGasMetering(); + string memory _tokenURI = "tokenURI"; + uint256 _amount = 100; + + vm.prank(deployerSigner); + vm.resumeGasMetering(); + tokenContract.mintTo(recipient, type(uint256).max, _tokenURI, _amount); + } + + function test_benchmark_tokenERC1155_burn() public { + vm.pauseGasMetering(); + string memory _tokenURI = "tokenURI"; + uint256 _amount = 100; + + uint256 nextTokenId = tokenContract.nextTokenIdToMint(); + + vm.prank(deployerSigner); + tokenContract.mintTo(recipient, type(uint256).max, _tokenURI, _amount); + + vm.prank(recipient); + vm.resumeGasMetering(); + tokenContract.burn(recipient, nextTokenId, _amount); + } +} diff --git a/src/test/benchmark/TokenERC20Benchmark.t.sol b/src/test/benchmark/TokenERC20Benchmark.t.sol new file mode 100644 index 000000000..6b93bb25a --- /dev/null +++ b/src/test/benchmark/TokenERC20Benchmark.t.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { TokenERC20 } from "contracts/prebuilts/token/TokenERC20.sol"; + +// Test imports +import "../utils/BaseTest.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; + +contract TokenERC20BenchmarkTest is BaseTest { + using Strings for uint256; + + event TokensMinted(address indexed mintedTo, uint256 quantityMinted); + event TokensMintedWithSignature( + address indexed signer, + address indexed mintedTo, + TokenERC20.MintRequest mintRequest + ); + + event PrimarySaleRecipientUpdated(address indexed recipient); + event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps); + + TokenERC20 public tokenContract; + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + bytes32 internal permitTypehash; + + bytes private emptyEncodedBytes = abi.encode("", ""); + + TokenERC20.MintRequest _mintrequest; + bytes _signature; + + address internal deployerSigner; + address internal recipient; + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + deployerSigner = signer; + recipient = address(0x123); + tokenContract = TokenERC20(getContract("TokenERC20")); + + erc20.mint(deployerSigner, 1_000); + vm.deal(deployerSigner, 1_000); + + erc20.mint(recipient, 1_000); + vm.deal(recipient, 1_000); + + typehashMintRequest = keccak256( + "MintRequest(address to,address primarySaleRecipient,uint256 quantity,uint256 price,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + nameHash = keccak256(bytes(NAME)); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256( + abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(tokenContract)) + ); + permitTypehash = keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ); + + // construct default mintrequest + _mintrequest.to = recipient; + _mintrequest.primarySaleRecipient = saleRecipient; + _mintrequest.quantity = 100; + _mintrequest.price = 0; + _mintrequest.currency = address(0); + _mintrequest.validityStartTimestamp = 1000; + _mintrequest.validityEndTimestamp = 2000; + _mintrequest.uid = bytes32(0); + + _signature = signMintRequest(_mintrequest, privateKey); + } + + function signMintRequest( + TokenERC20.MintRequest memory _request, + uint256 _privateKey + ) internal view returns (bytes memory) { + bytes memory encodedRequest = abi.encode( + typehashMintRequest, + _request.to, + _request.primarySaleRecipient, + _request.quantity, + _request.price, + _request.currency, + _request.validityStartTimestamp, + _request.validityEndTimestamp, + _request.uid + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, typedDataHash); + bytes memory sig = abi.encodePacked(r, s, v); + + return sig; + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: TokenERC20 + //////////////////////////////////////////////////////////////*/ + + function test_benchmark_tokenERC20_mintWithSignature_pay_with_ERC20() public { + vm.pauseGasMetering(); + vm.warp(1000); + + // update mintrequest data + _mintrequest.price = 1; + _mintrequest.currency = address(erc20); + _signature = signMintRequest(_mintrequest, privateKey); + + // approve erc20 tokens to tokenContract + vm.prank(recipient); + erc20.approve(address(tokenContract), _mintrequest.price); + + // mint with signature + vm.prank(recipient); + vm.resumeGasMetering(); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_benchmark_tokenERC20_mintWithSignature_pay_with_native_token() public { + vm.pauseGasMetering(); + vm.warp(1000); + + // update mintrequest data + _mintrequest.price = 1; + _mintrequest.currency = address(NATIVE_TOKEN); + _signature = signMintRequest(_mintrequest, privateKey); + + // mint with signature + vm.prank(recipient); + vm.resumeGasMetering(); + tokenContract.mintWithSignature{ value: _mintrequest.price }(_mintrequest, _signature); + } + + function test_benchmark_tokenERC20_mintTo() public { + vm.pauseGasMetering(); + uint256 _amount = 100; + + vm.prank(deployerSigner); + vm.resumeGasMetering(); + tokenContract.mintTo(recipient, _amount); + } +} diff --git a/src/test/benchmark/TokenERC721Benchmark.t.sol b/src/test/benchmark/TokenERC721Benchmark.t.sol new file mode 100644 index 000000000..410709462 --- /dev/null +++ b/src/test/benchmark/TokenERC721Benchmark.t.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { TokenERC721 } from "contracts/prebuilts/token/TokenERC721.sol"; + +// Test imports +import "../utils/BaseTest.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; + +contract TokenERC721BenchmarkTest is BaseTest { + using Strings for uint256; + + event TokensMinted(address indexed mintedTo, uint256 indexed tokenIdMinted, string uri); + event TokensMintedWithSignature( + address indexed signer, + address indexed mintedTo, + uint256 indexed tokenIdMinted, + TokenERC721.MintRequest mintRequest + ); + event OwnerUpdated(address indexed prevOwner, address indexed newOwner); + event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps); + event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, uint256 royaltyBps); + event PrimarySaleRecipientUpdated(address indexed recipient); + event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps); + + TokenERC721 public tokenContract; + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + bytes private emptyEncodedBytes = abi.encode("", ""); + + TokenERC721.MintRequest _mintrequest; + bytes _signature; + + address internal deployerSigner; + address internal recipient; + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + deployerSigner = signer; + recipient = address(0x123); + tokenContract = TokenERC721(getContract("TokenERC721")); + + erc20.mint(deployerSigner, 1_000); + vm.deal(deployerSigner, 1_000); + + erc20.mint(recipient, 1_000); + vm.deal(recipient, 1_000); + + typehashMintRequest = keccak256( + "MintRequest(address to,address royaltyRecipient,uint256 royaltyBps,address primarySaleRecipient,string uri,uint256 price,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + nameHash = keccak256(bytes("TokenERC721")); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256( + abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(tokenContract)) + ); + + // construct default mintrequest + _mintrequest.to = recipient; + _mintrequest.royaltyRecipient = royaltyRecipient; + _mintrequest.royaltyBps = royaltyBps; + _mintrequest.primarySaleRecipient = saleRecipient; + _mintrequest.uri = "ipfs://"; + _mintrequest.price = 0; + _mintrequest.currency = address(0); + _mintrequest.validityStartTimestamp = 1000; + _mintrequest.validityEndTimestamp = 2000; + _mintrequest.uid = bytes32(0); + + _signature = signMintRequest(_mintrequest, privateKey); + } + + function signMintRequest( + TokenERC721.MintRequest memory _request, + uint256 _privateKey + ) internal view returns (bytes memory) { + bytes memory encodedRequest = abi.encode( + typehashMintRequest, + _request.to, + _request.royaltyRecipient, + _request.royaltyBps, + _request.primarySaleRecipient, + keccak256(bytes(_request.uri)), + _request.price, + _request.currency, + _request.validityStartTimestamp, + _request.validityEndTimestamp, + _request.uid + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, typedDataHash); + bytes memory sig = abi.encodePacked(r, s, v); + + return sig; + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: TokenERC721 + //////////////////////////////////////////////////////////////*/ + + function test_benchmark_tokenERC721_mintWithSignature_pay_with_ERC20() public { + vm.pauseGasMetering(); + vm.warp(1000); + + // update mintrequest data + _mintrequest.price = 1; + _mintrequest.currency = address(erc20); + _signature = signMintRequest(_mintrequest, privateKey); + + // approve erc20 tokens to tokenContract + vm.prank(recipient); + erc20.approve(address(tokenContract), 1); + + // mint with signature + vm.prank(recipient); + vm.resumeGasMetering(); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_benchmark_tokenERC721_mintWithSignature_pay_with_native_token() public { + vm.pauseGasMetering(); + vm.warp(1000); + + // update mintrequest data + _mintrequest.price = 1; + _mintrequest.currency = address(NATIVE_TOKEN); + _signature = signMintRequest(_mintrequest, privateKey); + + // mint with signature + vm.prank(recipient); + vm.resumeGasMetering(); + tokenContract.mintWithSignature{ value: 1 }(_mintrequest, _signature); + } + + function test_benchmark_tokenERC721_mintTo() public { + vm.pauseGasMetering(); + string memory _tokenURI = "tokenURI"; + + vm.prank(deployerSigner); + vm.resumeGasMetering(); + tokenContract.mintTo(recipient, _tokenURI); + } + + function test_benchmark_tokenERC721_burn() public { + vm.pauseGasMetering(); + string memory _tokenURI = "tokenURI"; + + uint256 nextTokenId = tokenContract.nextTokenIdToMint(); + + vm.prank(deployerSigner); + tokenContract.mintTo(recipient, _tokenURI); + + vm.prank(recipient); + vm.resumeGasMetering(); + tokenContract.burn(nextTokenId); + } +} diff --git a/src/test/benchmark/TokenStakeBenchmark.t.sol b/src/test/benchmark/TokenStakeBenchmark.t.sol new file mode 100644 index 000000000..5f639614e --- /dev/null +++ b/src/test/benchmark/TokenStakeBenchmark.t.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { TokenStake } from "contracts/prebuilts/staking/TokenStake.sol"; + +// Test imports + +import "../utils/BaseTest.sol"; + +contract TokenStakeBenchmarkTest is BaseTest { + TokenStake internal stakeContract; + + address internal stakerOne; + address internal stakerTwo; + + uint256 internal timeUnit; + uint256 internal rewardRatioNumerator; + uint256 internal rewardRatioDenominator; + + function setUp() public override { + super.setUp(); + + timeUnit = 60; + rewardRatioNumerator = 3; + rewardRatioDenominator = 50; + + stakerOne = address(0x345); + stakerTwo = address(0x567); + + erc20Aux.mint(stakerOne, 1000); // mint 1000 tokens to stakerOne + erc20Aux.mint(stakerTwo, 1000); // mint 1000 tokens to stakerTwo + + erc20.mint(deployer, 1000 ether); // mint reward tokens to contract admin + + stakeContract = TokenStake(payable(getContract("TokenStake"))); + + // set approvals + vm.prank(stakerOne); + erc20Aux.approve(address(stakeContract), type(uint256).max); + + vm.prank(stakerTwo); + erc20Aux.approve(address(stakeContract), type(uint256).max); + + vm.startPrank(deployer); + erc20.approve(address(stakeContract), type(uint256).max); + stakeContract.depositRewardTokens(100 ether); + // erc20.transfer(address(stakeContract), 100 ether); + vm.stopPrank(); + assertEq(stakeContract.getRewardTokenBalance(), 100 ether); + } + + /*/////////////////////////////////////////////////////////////// + Benchmark: TokenStake + //////////////////////////////////////////////////////////////*/ + + function test_benchmark_tokenStake_stake() public { + vm.pauseGasMetering(); + + vm.warp(1); + // stake 400 tokens + vm.prank(stakerOne); + vm.resumeGasMetering(); + stakeContract.stake(400); + } + + function test_benchmark_tokenStake_claimRewards() public { + vm.pauseGasMetering(); + vm.warp(1); + + // stake 50 tokens with token-id 0 + vm.prank(stakerOne); + stakeContract.stake(400); + + //=================== warp timestamp to claim rewards + vm.roll(100); + vm.warp(1000); + vm.prank(stakerOne); + vm.resumeGasMetering(); + stakeContract.claimRewards(); + } + + function test_benchmark_tokenStake_withdraw() public { + vm.pauseGasMetering(); + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(400); + + vm.prank(stakerTwo); + stakeContract.stake(200); + + //========== warp timestamp before withdraw + vm.roll(100); + vm.warp(1000); + + // withdraw partially for stakerOne + vm.prank(stakerOne); + vm.resumeGasMetering(); + stakeContract.withdraw(100); + } +} diff --git a/src/test/burn-to-claim-drop-BTT/BurnToClaimDropERC721.t.sol b/src/test/burn-to-claim-drop-BTT/BurnToClaimDropERC721.t.sol new file mode 100644 index 000000000..7c26fec50 --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/BurnToClaimDropERC721.t.sol @@ -0,0 +1,1885 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../utils/BaseTest.sol"; +import { BurnToClaimDropERC721 } from "contracts/prebuilts/unaudited/burn-to-claim-drop/BurnToClaimDropERC721.sol"; +import { BurnToClaimDrop721Logic, ERC721AUpgradeable, DelayedReveal, LazyMint, Drop, BurnToClaim, PrimarySale, PlatformFee } from "contracts/prebuilts/unaudited/burn-to-claim-drop/extension/BurnToClaimDrop721Logic.sol"; +import { PermissionsEnumerableImpl } from "contracts/extension/upgradeable/impl/PermissionsEnumerableImpl.sol"; +import { Royalty } from "contracts/extension/upgradeable/Royalty.sol"; +import { BatchMintMetadata } from "contracts/extension/upgradeable/BatchMintMetadata.sol"; +import { IBurnToClaim } from "contracts/extension/interface/IBurnToClaim.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import { Permissions } from "contracts/extension/Permissions.sol"; +import { PermissionsEnumerable } from "contracts/extension/PermissionsEnumerable.sol"; +import "erc721a-upgradeable/contracts/IERC721AUpgradeable.sol"; + +contract BurnToClaimDropERC721Test is BaseTest, IExtension { + using Strings for uint256; + using Strings for address; + + event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI); + event TokenURIRevealed(uint256 indexed index, string revealedURI); + + BurnToClaimDrop721Logic public drop; + + bytes private emptyEncodedBytes = abi.encode("", ""); + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address dropImpl = address(new BurnToClaimDropERC721(extensions)); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + drop = BurnToClaimDrop721Logic( + payable( + address( + new TWProxy( + dropImpl, + abi.encodeCall( + BurnToClaimDropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ) + ); + + erc20.mint(deployer, 1_000 ether); + vm.deal(deployer, 1_000 ether); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](2); + + // Extension: Permissions + address permissions = address(new PermissionsEnumerableImpl()); + + Extension memory extension_permissions; + extension_permissions.metadata = ExtensionMetadata({ + name: "Permissions", + metadataURI: "ipfs://Permissions", + implementation: permissions + }); + + extension_permissions.functions = new ExtensionFunction[](7); + extension_permissions.functions[0] = ExtensionFunction( + Permissions.hasRole.selector, + "hasRole(bytes32,address)" + ); + extension_permissions.functions[1] = ExtensionFunction( + Permissions.hasRoleWithSwitch.selector, + "hasRoleWithSwitch(bytes32,address)" + ); + extension_permissions.functions[2] = ExtensionFunction( + Permissions.grantRole.selector, + "grantRole(bytes32,address)" + ); + extension_permissions.functions[3] = ExtensionFunction( + Permissions.renounceRole.selector, + "renounceRole(bytes32,address)" + ); + extension_permissions.functions[4] = ExtensionFunction( + Permissions.revokeRole.selector, + "revokeRole(bytes32,address)" + ); + extension_permissions.functions[5] = ExtensionFunction( + PermissionsEnumerable.getRoleMemberCount.selector, + "getRoleMemberCount(bytes32)" + ); + extension_permissions.functions[6] = ExtensionFunction( + PermissionsEnumerable.getRoleMember.selector, + "getRoleMember(bytes32,uint256)" + ); + + extensions[0] = extension_permissions; + + address dropLogic = address(new BurnToClaimDrop721Logic()); + + Extension memory extension_drop; + extension_drop.metadata = ExtensionMetadata({ + name: "BurnToClaimDrop721Logic", + metadataURI: "ipfs://BurnToClaimDrop721Logic", + implementation: dropLogic + }); + + extension_drop.functions = new ExtensionFunction[](32); + extension_drop.functions[0] = ExtensionFunction(BurnToClaimDrop721Logic.tokenURI.selector, "tokenURI(uint256)"); + extension_drop.functions[1] = ExtensionFunction( + BurnToClaimDrop721Logic.lazyMint.selector, + "lazyMint(uint256,string,bytes)" + ); + extension_drop.functions[2] = ExtensionFunction( + BurnToClaimDrop721Logic.reveal.selector, + "reveal(uint256,bytes)" + ); + extension_drop.functions[3] = ExtensionFunction(Drop.claimCondition.selector, "claimCondition()"); + extension_drop.functions[4] = ExtensionFunction( + BatchMintMetadata.getBaseURICount.selector, + "getBaseURICount()" + ); + extension_drop.functions[5] = ExtensionFunction( + Drop.claim.selector, + "claim(address,uint256,address,uint256,(bytes32[],uint256,uint256,address),bytes)" + ); + extension_drop.functions[6] = ExtensionFunction( + Drop.setClaimConditions.selector, + "setClaimConditions((uint256,uint256,uint256,uint256,bytes32,uint256,address,string)[],bool)" + ); + extension_drop.functions[7] = ExtensionFunction( + Drop.getActiveClaimConditionId.selector, + "getActiveClaimConditionId()" + ); + extension_drop.functions[8] = ExtensionFunction( + Drop.getClaimConditionById.selector, + "getClaimConditionById(uint256)" + ); + extension_drop.functions[9] = ExtensionFunction( + Drop.getSupplyClaimedByWallet.selector, + "getSupplyClaimedByWallet(uint256,address)" + ); + extension_drop.functions[10] = ExtensionFunction(BurnToClaimDrop721Logic.totalMinted.selector, "totalMinted()"); + extension_drop.functions[11] = ExtensionFunction( + BurnToClaimDrop721Logic.nextTokenIdToMint.selector, + "nextTokenIdToMint()" + ); + extension_drop.functions[12] = ExtensionFunction( + IERC721Upgradeable.setApprovalForAll.selector, + "setApprovalForAll(address,bool)" + ); + extension_drop.functions[13] = ExtensionFunction( + IERC721Upgradeable.approve.selector, + "approve(address,uint256)" + ); + extension_drop.functions[14] = ExtensionFunction( + IERC721Upgradeable.transferFrom.selector, + "transferFrom(address,address,uint256)" + ); + extension_drop.functions[15] = ExtensionFunction(ERC721AUpgradeable.balanceOf.selector, "balanceOf(address)"); + extension_drop.functions[16] = ExtensionFunction( + DelayedReveal.encryptDecrypt.selector, + "encryptDecrypt(bytes,bytes)" + ); + extension_drop.functions[17] = ExtensionFunction( + BurnToClaimDrop721Logic.supportsInterface.selector, + "supportsInterface(bytes4)" + ); + extension_drop.functions[18] = ExtensionFunction(Royalty.royaltyInfo.selector, "royaltyInfo(uint256,uint256)"); + extension_drop.functions[19] = ExtensionFunction( + Royalty.getRoyaltyInfoForToken.selector, + "getRoyaltyInfoForToken(uint256)" + ); + extension_drop.functions[20] = ExtensionFunction( + Royalty.getDefaultRoyaltyInfo.selector, + "getDefaultRoyaltyInfo()" + ); + extension_drop.functions[21] = ExtensionFunction( + Royalty.setDefaultRoyaltyInfo.selector, + "setDefaultRoyaltyInfo(address,uint256)" + ); + extension_drop.functions[22] = ExtensionFunction( + Royalty.setRoyaltyInfoForToken.selector, + "setRoyaltyInfoForToken(uint256,address,uint256)" + ); + extension_drop.functions[23] = ExtensionFunction(IERC721.ownerOf.selector, "ownerOf(uint256)"); + extension_drop.functions[24] = ExtensionFunction(IERC1155.balanceOf.selector, "balanceOf(address,uint256)"); + extension_drop.functions[25] = ExtensionFunction( + BurnToClaim.setBurnToClaimInfo.selector, + "setBurnToClaimInfo((address,uint8,uint256,uint256,address))" + ); + extension_drop.functions[26] = ExtensionFunction( + BurnToClaim.getBurnToClaimInfo.selector, + "getBurnToClaimInfo()" + ); + extension_drop.functions[27] = ExtensionFunction( + BurnToClaim.verifyBurnToClaim.selector, + "verifyBurnToClaim(address,uint256,uint256)" + ); + extension_drop.functions[28] = ExtensionFunction( + BurnToClaimDrop721Logic.burnAndClaim.selector, + "burnAndClaim(uint256,uint256)" + ); + extension_drop.functions[29] = ExtensionFunction( + BurnToClaimDrop721Logic.nextTokenIdToClaim.selector, + "nextTokenIdToClaim()" + ); + extension_drop.functions[30] = ExtensionFunction( + PrimarySale.setPrimarySaleRecipient.selector, + "setPrimarySaleRecipient(address)" + ); + extension_drop.functions[31] = ExtensionFunction( + PlatformFee.setPlatformFeeInfo.selector, + "setPlatformFeeInfo(address,uint256)" + ); + + extensions[1] = extension_drop; + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: misc. + //////////////////////////////////////////////////////////////*/ + + /** + * note: Tests whether contract reverts when a non-holder renounces a role. + */ + function test_revert_nonHolder_renounceRole() public { + address caller = address(0x123); + bytes32 role = keccak256("MINTER_ROLE"); + + vm.prank(caller); + vm.expectRevert( + abi.encodePacked( + "Permissions: account ", + Strings.toHexString(uint160(caller), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ); + + Permissions(address(drop)).renounceRole(role, caller); + } + + /** + * note: Tests whether contract reverts when a role admin revokes a role for a non-holder. + */ + function test_revert_revokeRoleForNonHolder() public { + address target = address(0x123); + bytes32 role = keccak256("MINTER_ROLE"); + + vm.prank(deployer); + vm.expectRevert( + abi.encodePacked( + "Permissions: account ", + Strings.toHexString(uint160(target), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ); + + Permissions(address(drop)).revokeRole(role, target); + } + + /** + * @dev Tests whether contract reverts when a role is granted to an existent role holder. + */ + function test_revert_grant_role_to_account_with_role() public { + bytes32 role = keccak256("ABC_ROLE"); + address receiver = getActor(0); + + vm.startPrank(deployer); + + Permissions(address(drop)).grantRole(role, receiver); + + vm.expectRevert("Can only grant to non holders"); + Permissions(address(drop)).grantRole(role, receiver); + + vm.stopPrank(); + } + + /** + * @dev Tests contract state for Transfer role. + */ + function test_state_grant_transferRole() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + + // check if admin and address(0) have transfer role in the beginning + bool checkAddressZero = Permissions(address(drop)).hasRole(role, address(0)); + bool checkAdmin = Permissions(address(drop)).hasRole(role, deployer); + assertTrue(checkAddressZero); + assertTrue(checkAdmin); + + // check if transfer role can be granted to a non-holder + address receiver = getActor(0); + vm.startPrank(deployer); + Permissions(address(drop)).grantRole(role, receiver); + + // expect revert when granting to a holder + vm.expectRevert("Can only grant to non holders"); + Permissions(address(drop)).grantRole(role, receiver); + + // check if receiver has transfer role + bool checkReceiver = Permissions(address(drop)).hasRole(role, receiver); + assertTrue(checkReceiver); + + // check if role is correctly revoked + Permissions(address(drop)).revokeRole(role, receiver); + checkReceiver = Permissions(address(drop)).hasRole(role, receiver); + assertFalse(checkReceiver); + Permissions(address(drop)).revokeRole(role, address(0)); + checkAddressZero = Permissions(address(drop)).hasRole(role, address(0)); + assertFalse(checkAddressZero); + + vm.stopPrank(); + } + + /** + * @dev Tests contract state for Transfer role. + */ + function test_state_getRoleMember_transferRole() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + + uint256 roleMemberCount = PermissionsEnumerable(address(drop)).getRoleMemberCount(role); + assertEq(roleMemberCount, 2); + + address roleMember = PermissionsEnumerable(address(drop)).getRoleMember(role, 1); + assertEq(roleMember, address(0)); + + vm.startPrank(deployer); + Permissions(address(drop)).grantRole(role, address(2)); + Permissions(address(drop)).grantRole(role, address(3)); + Permissions(address(drop)).grantRole(role, address(4)); + + roleMemberCount = PermissionsEnumerable(address(drop)).getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(address(drop)).getRoleMember(role, i)); + } + console.log(""); + + Permissions(address(drop)).revokeRole(role, address(2)); + roleMemberCount = PermissionsEnumerable(address(drop)).getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(address(drop)).getRoleMember(role, i)); + } + console.log(""); + + Permissions(address(drop)).revokeRole(role, address(0)); + roleMemberCount = PermissionsEnumerable(address(drop)).getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(address(drop)).getRoleMember(role, i)); + } + console.log(""); + + Permissions(address(drop)).grantRole(role, address(5)); + roleMemberCount = PermissionsEnumerable(address(drop)).getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(address(drop)).getRoleMember(role, i)); + } + console.log(""); + + Permissions(address(drop)).grantRole(role, address(0)); + roleMemberCount = PermissionsEnumerable(address(drop)).getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(address(drop)).getRoleMember(role, i)); + } + console.log(""); + + Permissions(address(drop)).grantRole(role, address(6)); + roleMemberCount = PermissionsEnumerable(address(drop)).getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(address(drop)).getRoleMember(role, i)); + } + console.log(""); + } + + /** + * note: Testing transfer of tokens when transfer-role is restricted + */ + function test_claim_transferRole() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + drop.claim(receiver, 1, address(0), 0, alp, ""); + + // revoke transfer role from address(0) + vm.prank(deployer); + Permissions(address(drop)).revokeRole(keccak256("TRANSFER_ROLE"), address(0)); + vm.startPrank(receiver); + vm.expectRevert("!Transfer-Role"); + drop.transferFrom(receiver, address(123), 0); + } + + /** + * @dev Tests whether role member count is incremented correctly. + */ + function test_member_count_incremented_properly_when_role_granted() public { + bytes32 role = keccak256("ABC_ROLE"); + address receiver = getActor(0); + + vm.startPrank(deployer); + uint256 roleMemberCount = PermissionsEnumerable(address(drop)).getRoleMemberCount(role); + + assertEq(roleMemberCount, 0); + + Permissions(address(drop)).grantRole(role, receiver); + + assertEq(PermissionsEnumerable(address(drop)).getRoleMemberCount(role), 1); + + vm.stopPrank(); + } + + function test_claimCondition_with_startTimestamp() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].startTimestamp = 100; + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.warp(99); + vm.prank(getActor(5), getActor(5)); + vm.expectRevert("!CONDITION."); + drop.claim(receiver, 1, address(0), 0, alp, ""); + + vm.warp(100); + vm.prank(getActor(4), getActor(4)); + drop.claim(receiver, 1, address(0), 0, alp, ""); + } + + /*/////////////////////////////////////////////////////////////// + Primary sale and Platform fee tests + //////////////////////////////////////////////////////////////*/ + + /// note: Test whether transaction reverts when adding address(0) as primary sale recipient at deploy time + function test_revert_deploy_emptyPrimarySaleRecipient() public { + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address dropImpl = address(new BurnToClaimDropERC721(extensions)); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + vm.expectRevert("Invalid recipient"); + drop = BurnToClaimDrop721Logic( + payable( + address( + new TWProxy( + dropImpl, + abi.encodeCall( + BurnToClaimDropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + address(0), + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ) + ); + } + + /// note: Test whether transaction reverts when adding address(0) as primary sale recipient + function test_revert_emptyPrimarySaleRecipient() public { + vm.prank(deployer); + vm.expectRevert("Invalid recipient"); + drop.setPrimarySaleRecipient(address(0)); + } + + /// note: Test whether transaction reverts when adding address(0) as platform fee recipient at deploy time + function test_revert_deploy_emptyPlatformFeeRecipient() public { + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address dropImpl = address(new BurnToClaimDropERC721(extensions)); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + vm.expectRevert("Invalid recipient"); + drop = BurnToClaimDrop721Logic( + payable( + address( + new TWProxy( + dropImpl, + abi.encodeCall( + BurnToClaimDropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + address(0) + ) + ) + ) + ) + ) + ); + } + + /// note: Test whether transaction reverts when adding address(0) as platform fee recipient + function test_revert_emptyPlatformFeeRecipient() public { + vm.prank(deployer); + vm.expectRevert("Invalid recipient"); + drop.setPlatformFeeInfo(address(0), 100); + } + + /*/////////////////////////////////////////////////////////////// + Lazy Mint Tests + //////////////////////////////////////////////////////////////*/ + + /* + * note: Testing state changes; lazy mint a batch of tokens with no encrypted base URI. + */ + function test_state_lazyMint_noEncryptedURI() public { + uint256 amountToLazyMint = 100; + string memory baseURI = "ipfs://"; + + uint256 nextTokenIdToMintBefore = drop.nextTokenIdToMint(); + + vm.startPrank(deployer); + uint256 batchId = drop.lazyMint(amountToLazyMint, baseURI, emptyEncodedBytes); + + assertEq(nextTokenIdToMintBefore + amountToLazyMint, drop.nextTokenIdToMint()); + assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); + + for (uint256 i = 0; i < amountToLazyMint; i += 1) { + string memory uri = drop.tokenURI(i); + console.log(uri); + assertEq(uri, string(abi.encodePacked(baseURI, i.toString()))); + } + + vm.stopPrank(); + } + + /* + * note: Testing state changes; lazy mint a batch of tokens with encrypted base URI. + */ + function test_state_lazyMint_withEncryptedURI() public { + uint256 amountToLazyMint = 100; + string memory baseURI = "ipfs://"; + bytes memory encryptedBaseURI = "encryptedBaseURI://"; + bytes32 provenanceHash = bytes32("whatever"); + + uint256 nextTokenIdToMintBefore = drop.nextTokenIdToMint(); + + vm.startPrank(deployer); + uint256 batchId = drop.lazyMint(amountToLazyMint, baseURI, abi.encode(encryptedBaseURI, provenanceHash)); + + assertEq(nextTokenIdToMintBefore + amountToLazyMint, drop.nextTokenIdToMint()); + assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); + + for (uint256 i = 0; i < amountToLazyMint; i += 1) { + string memory uri = drop.tokenURI(i); + console.log(uri); + assertEq(uri, string(abi.encodePacked(baseURI, "0"))); + } + + vm.stopPrank(); + } + + /** + * note: Testing revert condition; an address without MINTER_ROLE calls lazyMint function. + */ + function test_revert_lazyMint_MINTER_ROLE() public { + vm.expectRevert("Not authorized"); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + } + + /* + * note: Testing revert condition; calling tokenURI for invalid batch id. + */ + function test_revert_lazyMint_URIForNonLazyMintedToken() public { + vm.startPrank(deployer); + + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + vm.expectRevert("Invalid tokenId"); + drop.tokenURI(100); + + vm.stopPrank(); + } + + /** + * note: Testing event emission; tokens lazy minted. + */ + function test_event_lazyMint_TokensLazyMinted() public { + vm.startPrank(deployer); + + vm.expectEmit(true, false, false, true); + emit TokensLazyMinted(0, 99, "ipfs://", emptyEncodedBytes); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + vm.stopPrank(); + } + + /* + * note: Fuzz testing state changes; lazy mint a batch of tokens with no encrypted base URI. + */ + function test_fuzz_lazyMint_noEncryptedURI(uint256 x) public { + vm.assume(x > 0); + + uint256 amountToLazyMint = x; + string memory baseURI = "ipfs://"; + + uint256 nextTokenIdToMintBefore = drop.nextTokenIdToMint(); + + vm.startPrank(deployer); + uint256 batchId = drop.lazyMint(amountToLazyMint, baseURI, emptyEncodedBytes); + + assertEq(nextTokenIdToMintBefore + amountToLazyMint, drop.nextTokenIdToMint()); + assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); + + string memory uri = drop.tokenURI(0); + assertEq(uri, string(abi.encodePacked(baseURI, uint256(0).toString()))); + + uri = drop.tokenURI(x - 1); + assertEq(uri, string(abi.encodePacked(baseURI, uint256(x - 1).toString()))); + + /** + * note: this loop takes too long to run with fuzz tests. + */ + // for(uint256 i = 0; i < amountToLazyMint; i += 1) { + // string memory uri = drop.tokenURI(i); + // console.log(uri); + // assertEq(uri, string(abi.encodePacked(baseURI, i.toString()))); + // } + + vm.stopPrank(); + } + + /* + * note: Fuzz testing state changes; lazy mint a batch of tokens with encrypted base URI. + */ + function test_fuzz_lazyMint_withEncryptedURI(uint256 x) public { + vm.assume(x > 0); + + uint256 amountToLazyMint = x; + string memory baseURI = "ipfs://"; + bytes memory encryptedBaseURI = "encryptedBaseURI://"; + bytes32 provenanceHash = bytes32("whatever"); + + uint256 nextTokenIdToMintBefore = drop.nextTokenIdToMint(); + + vm.startPrank(deployer); + uint256 batchId = drop.lazyMint(amountToLazyMint, baseURI, abi.encode(encryptedBaseURI, provenanceHash)); + + assertEq(nextTokenIdToMintBefore + amountToLazyMint, drop.nextTokenIdToMint()); + assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); + + string memory uri = drop.tokenURI(0); + assertEq(uri, string(abi.encodePacked(baseURI, "0"))); + + uri = drop.tokenURI(x - 1); + assertEq(uri, string(abi.encodePacked(baseURI, "0"))); + + /** + * note: this loop takes too long to run with fuzz tests. + */ + // for(uint256 i = 0; i < amountToLazyMint; i += 1) { + // string memory uri = drop.tokenURI(1); + // assertEq(uri, string(abi.encodePacked(baseURI, "0"))); + // } + + vm.stopPrank(); + } + + /* + * note: Fuzz testing; a batch of tokens, and nextTokenIdToMint + */ + function test_fuzz_lazyMint_batchMintAndNextTokenIdToMint(uint256 x) public { + vm.assume(x > 0); + vm.startPrank(deployer); + + if (x == 0) { + vm.expectRevert("Zero amount"); + } + drop.lazyMint(x, "ipfs://", emptyEncodedBytes); + + uint256 slot = stdstore.target(address(drop)).sig("nextTokenIdToMint()").find(); + bytes32 loc = bytes32(slot); + uint256 nextTokenIdToMint = uint256(vm.load(address(drop), loc)); + + assertEq(nextTokenIdToMint, x); + vm.stopPrank(); + } + + /*/////////////////////////////////////////////////////////////// + Delayed Reveal Tests + //////////////////////////////////////////////////////////////*/ + + /* + * note: Testing state changes; URI revealed for a batch of tokens. + */ + function test_state_reveal() public { + vm.startPrank(deployer); + + bytes memory key = "key"; + uint256 amountToLazyMint = 100; + bytes memory secretURI = "ipfs://"; + string memory placeholderURI = "abcd://"; + bytes memory encryptedURI = drop.encryptDecrypt(secretURI, key); + bytes32 provenanceHash = keccak256(abi.encodePacked(secretURI, key, block.chainid)); + + drop.lazyMint(amountToLazyMint, placeholderURI, abi.encode(encryptedURI, provenanceHash)); + + for (uint256 i = 0; i < amountToLazyMint; i += 1) { + string memory uri = drop.tokenURI(i); + assertEq(uri, string(abi.encodePacked(placeholderURI, "0"))); + } + + string memory revealedURI = drop.reveal(0, key); + assertEq(revealedURI, string(secretURI)); + + for (uint256 i = 0; i < amountToLazyMint; i += 1) { + string memory uri = drop.tokenURI(i); + assertEq(uri, string(abi.encodePacked(secretURI, i.toString()))); + } + + vm.stopPrank(); + } + + /** + * note: Testing revert condition; an address without MINTER_ROLE calls reveal function. + */ + function test_revert_reveal_MINTER_ROLE() public { + bytes memory key = "key"; + bytes memory encryptedURI = drop.encryptDecrypt("ipfs://", key); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); + vm.prank(deployer); + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + + vm.prank(deployer); + drop.reveal(0, "key"); + + vm.expectRevert("not minter."); + drop.reveal(0, "key"); + } + + /* + * note: Testing revert condition; trying to reveal URI for non-existent batch. + */ + function test_revert_reveal_revealingNonExistentBatch() public { + vm.startPrank(deployer); + + bytes memory key = "key"; + bytes memory encryptedURI = drop.encryptDecrypt("ipfs://", key); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + drop.reveal(0, "key"); + + console.log(drop.getBaseURICount()); + + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + vm.expectRevert("Invalid index"); + drop.reveal(2, "key"); + + vm.stopPrank(); + } + + /* + * note: Testing revert condition; already revealed URI. + */ + function test_revert_delayedReveal_alreadyRevealed() public { + vm.startPrank(deployer); + + bytes memory key = "key"; + bytes memory encryptedURI = drop.encryptDecrypt("ipfs://", key); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + drop.reveal(0, "key"); + + vm.expectRevert("Nothing to reveal"); + drop.reveal(0, "key"); + + vm.stopPrank(); + } + + /* + * note: Testing state changes; revealing URI with an incorrect key. + */ + function test_revert_reveal_incorrectKey() public { + vm.startPrank(deployer); + + bytes memory key = "key"; + bytes memory encryptedURI = drop.encryptDecrypt("ipfs://", key); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + + vm.expectRevert(); + string memory revealedURI = drop.reveal(0, "keyy"); + + vm.stopPrank(); + } + + /** + * note: Testing event emission; TokenURIRevealed. + */ + function test_event_reveal_TokenURIRevealed() public { + vm.startPrank(deployer); + + bytes memory key = "key"; + bytes memory encryptedURI = drop.encryptDecrypt("ipfs://", key); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + + vm.expectEmit(true, false, false, true); + emit TokenURIRevealed(0, "ipfs://"); + drop.reveal(0, "key"); + + vm.stopPrank(); + } + + /*/////////////////////////////////////////////////////////////// + Claim Tests + //////////////////////////////////////////////////////////////*/ + + /** + * note: Testing revert condition; not enough minted tokens. + */ + function test_revert_claimCondition_notEnoughMintedTokens() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 200; + + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.expectRevert("!Tokens"); + vm.prank(getActor(6), getActor(6)); + drop.claim(receiver, 101, address(0), 0, alp, ""); + } + + /** + * note: Testing revert condition; exceed max claimable supply. + */ + function test_revert_claimCondition_exceedMaxClaimableSupply() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 200; + + vm.prank(deployer); + drop.lazyMint(200, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + drop.claim(receiver, 100, address(0), 0, alp, ""); + + vm.expectRevert("!MaxSupply"); + vm.prank(getActor(6), getActor(6)); + drop.claim(receiver, 1, address(0), 0, alp, ""); + } + + /** + * note: Testing quantity limit restriction when no allowlist present. + */ + function test_fuzz_claim_noAllowlist(uint256 x) public { + vm.assume(x != 0); + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = x; + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + bytes memory errorQty = "!Qty"; + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(errorQty); + drop.claim(receiver, 0, address(0), 0, alp, ""); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(errorQty); + drop.claim(receiver, 101, address(0), 0, alp, ""); + + vm.prank(deployer); + drop.setClaimConditions(conditions, true); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(errorQty); + drop.claim(receiver, 101, address(0), 0, alp, ""); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price set to 0 + */ + function test_state_claim_allowlisted_SetQuantityZeroPrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "0"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 0; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, 100, address(erc20), 0, alp, ""); // claims for free, because allowlist price is 0 + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), 100); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price set to non-zero value + */ + function test_state_claim_allowlisted_SetQuantityPrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "5"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 5; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.prank(receiver, receiver); + vm.expectRevert("!PriceOrCurrency"); + drop.claim(receiver, 100, address(erc20), 0, alp, ""); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(drop), 10000); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, 100, address(erc20), 5, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), 100); + assertEq(erc20.balanceOf(receiver), 10000 - 500); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price not set; should default to general price and currency + */ + function test_state_claim_allowlisted_SetQuantityDefaultPrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = Strings.toString(type(uint256).max); // this implies that general price is applicable + inputs[4] = "0x0000000000000000000000000000000000000000"; + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = type(uint256).max; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(drop), 10000); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, 100, address(erc20), 10, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), 100); + assertEq(erc20.balanceOf(receiver), 10000 - 1000); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to 0 => should default to general limit + * - allowlist price set to some value different than general price + */ + function test_state_claim_allowlisted_DefaultQuantitySomePrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "0"; // this implies that general limit is applicable + inputs[3] = "5"; + inputs[4] = "0x0000000000000000000000000000000000000000"; // general currency will be applicable + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 0; + alp.pricePerToken = 5; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(drop), 10000); + + bytes memory errorQty = "!Qty"; + vm.prank(receiver, receiver); + vm.expectRevert(errorQty); + drop.claim(receiver, 100, address(erc20), 5, alp, ""); // trying to claim more than general limit + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, 10, address(erc20), 5, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), 10); + assertEq(erc20.balanceOf(receiver), 10000 - 50); + } + + function test_fuzz_claim_merkleProof(uint256 x) public { + vm.assume(x > 10 && x < 500); + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = Strings.toString(x); + inputs[3] = "0"; + inputs[4] = "0x0000000000000000000000000000000000000000"; + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = x; + alp.pricePerToken = 0; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + + // bytes32[] memory proofs = new bytes32[](0); + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = x; + conditions[0].quantityLimitPerWallet = 1; + conditions[0].merkleRoot = root; + + vm.prank(deployer); + drop.lazyMint(2 * x, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, x - 5, address(0), 0, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), x - 5); + + bytes memory errorQty = "!Qty"; + + vm.prank(receiver, receiver); + vm.expectRevert(errorQty); + drop.claim(receiver, 6, address(0), 0, alp, ""); + + vm.prank(receiver, receiver); + drop.claim(receiver, 5, address(0), 0, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), x); + + vm.prank(receiver, receiver); + vm.expectRevert(errorQty); + drop.claim(receiver, 5, address(0), 0, alp, ""); // quantity limit already claimed + } + + /** + * note: Testing state changes; reset eligibility of claim conditions and claiming again for same condition id. + */ + function test_state_claimCondition_resetEligibility() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + drop.claim(receiver, 100, address(0), 0, alp, ""); + + bytes memory errorQty = "!Qty"; + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(errorQty); + drop.claim(receiver, 100, address(0), 0, alp, ""); + + vm.prank(deployer); + drop.setClaimConditions(conditions, true); + + vm.prank(getActor(5), getActor(5)); + drop.claim(receiver, 100, address(0), 0, alp, ""); + } + + /*/////////////////////////////////////////////////////////////// + setClaimConditions + //////////////////////////////////////////////////////////////*/ + + function test_claimCondition_startIdAndCount() public { + vm.startPrank(deployer); + + uint256 currentStartId = 0; + uint256 count = 0; + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](2); + conditions[0].startTimestamp = 0; + conditions[0].maxClaimableSupply = 10; + conditions[1].startTimestamp = 1; + conditions[1].maxClaimableSupply = 10; + + drop.setClaimConditions(conditions, false); + (currentStartId, count) = drop.claimCondition(); + assertEq(currentStartId, 0); + assertEq(count, 2); + + drop.setClaimConditions(conditions, false); + (currentStartId, count) = drop.claimCondition(); + assertEq(currentStartId, 0); + assertEq(count, 2); + + drop.setClaimConditions(conditions, true); + (currentStartId, count) = drop.claimCondition(); + assertEq(currentStartId, 2); + assertEq(count, 2); + + drop.setClaimConditions(conditions, true); + (currentStartId, count) = drop.claimCondition(); + assertEq(currentStartId, 4); + assertEq(count, 2); + } + + function test_claimCondition_startPhase() public { + vm.startPrank(deployer); + + uint256 activeConditionId = 0; + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](3); + conditions[0].startTimestamp = 10; + conditions[0].maxClaimableSupply = 11; + conditions[0].quantityLimitPerWallet = 12; + conditions[1].startTimestamp = 20; + conditions[1].maxClaimableSupply = 21; + conditions[1].quantityLimitPerWallet = 22; + conditions[2].startTimestamp = 30; + conditions[2].maxClaimableSupply = 31; + conditions[2].quantityLimitPerWallet = 32; + drop.setClaimConditions(conditions, false); + + vm.expectRevert("!CONDITION."); + drop.getActiveClaimConditionId(); + + vm.warp(10); + activeConditionId = drop.getActiveClaimConditionId(); + assertEq(activeConditionId, 0); + assertEq(drop.getClaimConditionById(activeConditionId).startTimestamp, 10); + assertEq(drop.getClaimConditionById(activeConditionId).maxClaimableSupply, 11); + assertEq(drop.getClaimConditionById(activeConditionId).quantityLimitPerWallet, 12); + + vm.warp(20); + activeConditionId = drop.getActiveClaimConditionId(); + assertEq(activeConditionId, 1); + assertEq(drop.getClaimConditionById(activeConditionId).startTimestamp, 20); + assertEq(drop.getClaimConditionById(activeConditionId).maxClaimableSupply, 21); + assertEq(drop.getClaimConditionById(activeConditionId).quantityLimitPerWallet, 22); + + vm.warp(30); + activeConditionId = drop.getActiveClaimConditionId(); + assertEq(activeConditionId, 2); + assertEq(drop.getClaimConditionById(activeConditionId).startTimestamp, 30); + assertEq(drop.getClaimConditionById(activeConditionId).maxClaimableSupply, 31); + assertEq(drop.getClaimConditionById(activeConditionId).quantityLimitPerWallet, 32); + + vm.warp(40); + assertEq(drop.getActiveClaimConditionId(), 2); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function test_delayedReveal_withNewLazyMintedEmptyBatch() public { + vm.startPrank(deployer); + + bytes memory encryptedURI = drop.encryptDecrypt("ipfs://", "key"); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", "key", block.chainid)); + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + drop.reveal(0, "key"); + + string memory uri = drop.tokenURI(1); + assertEq(uri, string(abi.encodePacked("ipfs://", "1"))); + + bytes memory newEncryptedURI = drop.encryptDecrypt("ipfs://secret", "key"); + vm.expectRevert("0 amt"); + drop.lazyMint(0, "", abi.encode(newEncryptedURI, provenanceHash)); + + vm.stopPrank(); + } + + /*/////////////////////////////////////////////////////////////// + Burn To Claim + //////////////////////////////////////////////////////////////*/ + + function test_state_burnAndClaim_1155Origin_zeroMintPrice() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc1155); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC1155; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 0; + burnToClaimInfo.currency = address(erc20); + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // check details correctly saved + BurnToClaimDrop721Logic.BurnToClaimInfo memory savedInfo = drop.getBurnToClaimInfo(); + assertEq(savedInfo.originContractAddress, burnToClaimInfo.originContractAddress); + assertTrue(savedInfo.tokenType == burnToClaimInfo.tokenType); + assertEq(savedInfo.tokenId, burnToClaimInfo.tokenId); + assertEq(savedInfo.mintPriceForNewToken, burnToClaimInfo.mintPriceForNewToken); + assertEq(savedInfo.currency, burnToClaimInfo.currency); + + // mint some erc1155 to a claimer + address claimer = getActor(0); + erc1155.mint(claimer, 0, 10); + assertEq(erc1155.balanceOf(claimer, 0), 10); + vm.prank(claimer); + erc1155.setApprovalForAll(address(drop), true); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // burn and claim + vm.prank(claimer); + drop.burnAndClaim(0, 10); + + // check state + assertEq(erc1155.balanceOf(claimer, 0), 0); + assertEq(drop.balanceOf(claimer), 10); + assertEq(drop.nextTokenIdToClaim(), 10); + } + + function test_state_burnAndClaim_1155Origin_nonZeroMintPrice() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc1155); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC1155; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 1; + burnToClaimInfo.currency = address(erc20); + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // check details correctly saved + BurnToClaimDrop721Logic.BurnToClaimInfo memory savedInfo = drop.getBurnToClaimInfo(); + assertEq(savedInfo.originContractAddress, burnToClaimInfo.originContractAddress); + assertTrue(savedInfo.tokenType == burnToClaimInfo.tokenType); + assertEq(savedInfo.tokenId, burnToClaimInfo.tokenId); + assertEq(savedInfo.mintPriceForNewToken, burnToClaimInfo.mintPriceForNewToken); + assertEq(savedInfo.currency, burnToClaimInfo.currency); + + // mint some erc1155 to a claimer + address claimer = getActor(0); + erc1155.mint(claimer, 0, 10); + assertEq(erc1155.balanceOf(claimer, 0), 10); + vm.prank(claimer); + erc1155.setApprovalForAll(address(drop), true); + + // mint erc20 to claimer, to pay claim price + erc20.mint(claimer, 100); + vm.prank(claimer); + erc20.approve(address(drop), type(uint256).max); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // burn and claim + vm.prank(claimer); + drop.burnAndClaim(0, 10); + + // check state + assertEq(erc1155.balanceOf(claimer, 0), 0); + assertEq(erc20.balanceOf(claimer), 90); + assertEq(erc20.balanceOf(saleRecipient), 10); + assertEq(drop.balanceOf(claimer), 10); + assertEq(drop.nextTokenIdToClaim(), 10); + } + + function test_state_burnAndClaim_1155Origin_nonZeroMintPrice_nativeToken() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc1155); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC1155; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 1; + burnToClaimInfo.currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // check details correctly saved + BurnToClaimDrop721Logic.BurnToClaimInfo memory savedInfo = drop.getBurnToClaimInfo(); + assertEq(savedInfo.originContractAddress, burnToClaimInfo.originContractAddress); + assertTrue(savedInfo.tokenType == burnToClaimInfo.tokenType); + assertEq(savedInfo.tokenId, burnToClaimInfo.tokenId); + assertEq(savedInfo.mintPriceForNewToken, burnToClaimInfo.mintPriceForNewToken); + assertEq(savedInfo.currency, burnToClaimInfo.currency); + + // mint some erc1155 to a claimer + address claimer = getActor(0); + erc1155.mint(claimer, 0, 10); + assertEq(erc1155.balanceOf(claimer, 0), 10); + vm.prank(claimer); + erc1155.setApprovalForAll(address(drop), true); + + // deal ether to claimer, to pay claim price + vm.deal(claimer, 100); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // burn and claim + vm.prank(claimer); + drop.burnAndClaim{ value: 10 }(0, 10); + + // check state + assertEq(erc1155.balanceOf(claimer, 0), 0); + assertEq(claimer.balance, 90); + assertEq(saleRecipient.balance, 10); + assertEq(drop.balanceOf(claimer), 10); + assertEq(drop.nextTokenIdToClaim(), 10); + } + + function test_state_burnAndClaim_721Origin_zeroMintPrice() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc721); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC721; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 0; + burnToClaimInfo.currency = address(erc20); + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // check details correctly saved + BurnToClaimDrop721Logic.BurnToClaimInfo memory savedInfo = drop.getBurnToClaimInfo(); + assertEq(savedInfo.originContractAddress, burnToClaimInfo.originContractAddress); + assertTrue(savedInfo.tokenType == burnToClaimInfo.tokenType); + assertEq(savedInfo.tokenId, burnToClaimInfo.tokenId); + assertEq(savedInfo.mintPriceForNewToken, burnToClaimInfo.mintPriceForNewToken); + assertEq(savedInfo.currency, burnToClaimInfo.currency); + + // mint some erc721 to a claimer + address claimer = getActor(0); + erc721.mint(claimer, 10); + assertEq(erc721.balanceOf(claimer), 10); + vm.prank(claimer); + erc721.setApprovalForAll(address(drop), true); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // burn and claim + vm.prank(claimer); + drop.burnAndClaim(0, 1); + + // check state + assertEq(erc721.balanceOf(claimer), 9); + assertEq(drop.balanceOf(claimer), 1); + assertEq(drop.nextTokenIdToClaim(), 1); + + vm.expectRevert("ERC721: invalid token ID"); // because the token doesn't exist anymore + erc721.ownerOf(0); + } + + function test_state_burnAndClaim_721Origin_nonZeroMintPrice() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc721); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC721; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 1; + burnToClaimInfo.currency = address(erc20); + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // check details correctly saved + BurnToClaimDrop721Logic.BurnToClaimInfo memory savedInfo = drop.getBurnToClaimInfo(); + assertEq(savedInfo.originContractAddress, burnToClaimInfo.originContractAddress); + assertTrue(savedInfo.tokenType == burnToClaimInfo.tokenType); + assertEq(savedInfo.tokenId, burnToClaimInfo.tokenId); + assertEq(savedInfo.mintPriceForNewToken, burnToClaimInfo.mintPriceForNewToken); + assertEq(savedInfo.currency, burnToClaimInfo.currency); + + // mint some erc721 to a claimer + address claimer = getActor(0); + erc721.mint(claimer, 10); + assertEq(erc721.balanceOf(claimer), 10); + vm.prank(claimer); + erc721.setApprovalForAll(address(drop), true); + + // mint erc20 to claimer, to pay claim price + erc20.mint(claimer, 100); + vm.prank(claimer); + erc20.approve(address(drop), type(uint256).max); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // burn and claim + vm.prank(claimer); + drop.burnAndClaim(0, 1); + + // check state + assertEq(erc721.balanceOf(claimer), 9); + assertEq(drop.balanceOf(claimer), 1); + assertEq(drop.nextTokenIdToClaim(), 1); + assertEq(erc20.balanceOf(claimer), 99); + assertEq(erc20.balanceOf(saleRecipient), 1); + + vm.expectRevert("ERC721: invalid token ID"); // because the token doesn't exist anymore + erc721.ownerOf(0); + } + + function test_state_burnAndClaim_721Origin_nonZeroMintPrice_nativeToken() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc721); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC721; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 1; + burnToClaimInfo.currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // check details correctly saved + BurnToClaimDrop721Logic.BurnToClaimInfo memory savedInfo = drop.getBurnToClaimInfo(); + assertEq(savedInfo.originContractAddress, burnToClaimInfo.originContractAddress); + assertTrue(savedInfo.tokenType == burnToClaimInfo.tokenType); + assertEq(savedInfo.tokenId, burnToClaimInfo.tokenId); + assertEq(savedInfo.mintPriceForNewToken, burnToClaimInfo.mintPriceForNewToken); + assertEq(savedInfo.currency, burnToClaimInfo.currency); + + // mint some erc721 to a claimer + address claimer = getActor(0); + erc721.mint(claimer, 10); + assertEq(erc721.balanceOf(claimer), 10); + vm.prank(claimer); + erc721.setApprovalForAll(address(drop), true); + + // deal ether to claimer, to pay claim price + vm.deal(claimer, 100); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // burn and claim + vm.prank(claimer); + drop.burnAndClaim{ value: 1 }(0, 1); + + // check state + assertEq(erc721.balanceOf(claimer), 9); + assertEq(drop.balanceOf(claimer), 1); + assertEq(drop.nextTokenIdToClaim(), 1); + assertEq(claimer.balance, 99); + assertEq(saleRecipient.balance, 1); + + vm.expectRevert("ERC721: invalid token ID"); // because the token doesn't exist anymore + erc721.ownerOf(0); + } + + function test_revert_burnAndClaim_originNotSet() public { + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // burn and claim + vm.expectRevert(); + drop.burnAndClaim(0, 1); + } + + function test_revert_burnAndClaim_noLazyMintedTokens() public { + // burn and claim + vm.expectRevert("!Tokens"); + drop.burnAndClaim(0, 1); + } + + function test_revert_burnAndClaim_invalidTokenId() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc1155); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC1155; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 0; + burnToClaimInfo.currency = address(erc20); + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // mint some erc1155 to a claimer + address claimer = getActor(0); + erc1155.mint(claimer, 0, 10); + assertEq(erc1155.balanceOf(claimer, 0), 10); + vm.prank(claimer); + erc1155.setApprovalForAll(address(drop), true); + + // burn and claim + vm.prank(claimer); + vm.expectRevert("Invalid token Id"); + drop.burnAndClaim(1, 1); + } + + function test_revert_burnAndClaim_notEnoughBalance() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc1155); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC1155; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 0; + burnToClaimInfo.currency = address(erc20); + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // mint some erc1155 to a claimer + address claimer = getActor(0); + erc1155.mint(claimer, 0, 10); + assertEq(erc1155.balanceOf(claimer, 0), 10); + vm.prank(claimer); + erc1155.setApprovalForAll(address(drop), true); + + // burn and claim + vm.prank(claimer); + vm.expectRevert("!Balance"); + drop.burnAndClaim(0, 11); + } + + function test_revert_burnAndClaim_notOwnerOfToken() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc721); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC721; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 1; + burnToClaimInfo.currency = address(erc20); + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // mint some erc721 to a claimer + address claimer = getActor(0); + erc721.mint(claimer, 10); + assertEq(erc721.balanceOf(claimer), 10); + vm.prank(claimer); + erc721.setApprovalForAll(address(drop), true); + + // mint erc721 to another address + erc721.mint(address(0x567), 5); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // burn and claim + vm.prank(claimer); + vm.expectRevert("!Owner"); + drop.burnAndClaim(11, 1); + } + + /*/////////////////////////////////////////////////////////////// + Extension Role and Upgradeability + //////////////////////////////////////////////////////////////*/ + + // function test_addExtension() public { + // address permissionsNew = address(new PermissionsEnumerableImpl()); + + // Extension memory extension_permissions_new; + // extension_permissions_new.metadata = ExtensionMetadata({ + // name: "PermissionsNew", + // metadataURI: "ipfs://PermissionsNew", + // implementation: permissionsNew + // }); + + // extension_permissions_new.functions = new ExtensionFunction[](4); + // extension_permissions_new.functions[0] = ExtensionFunction( + // Permissions.hasRole.selector, + // "hasRole(bytes32,address)" + // ); + // extension_permissions_new.functions[1] = ExtensionFunction( + // Permissions.hasRoleWithSwitch.selector, + // "hasRoleWithSwitch(bytes32,address)" + // ); + // extension_permissions_new.functions[2] = ExtensionFunction( + // Permissions.grantRole.selector, + // "grantRole(bytes32,address)" + // ); + // extension_permissions_new.functions[3] = ExtensionFunction( + // PermissionsEnumerable.getRoleMemberCount.selector, + // "getRoleMemberCount(bytes32)" + // ); + + // // cast drop to router type + // BurnToClaimDropERC721 dropRouter = BurnToClaimDropERC721(payable(address(drop))); + + // vm.prank(deployer); + // dropRouter.addExtension(extension_permissions_new); + + // // assertEq( + // // dropRouter.getExtensionForFunction(PermissionsEnumerable.getRoleMemberCount.selector).name, + // // "PermissionsNew" + // // ); + + // // assertEq( + // // dropRouter.getExtensionForFunction(PermissionsEnumerable.getRoleMemberCount.selector).implementation, + // // permissionsNew + // // ); + // } + + function test_revert_addExtension_NotAuthorized() public { + Extension memory extension_permissions_new; + + // cast drop to router type + BurnToClaimDropERC721 dropRouter = BurnToClaimDropERC721(payable(address(drop))); + + vm.prank(address(0x123)); + vm.expectRevert("ExtensionManager: unauthorized."); + dropRouter.addExtension(extension_permissions_new); + } + + function test_revert_addExtension_deployerRenounceExtensionRole() public { + Extension memory extension_permissions_new; + + // cast drop to router type + BurnToClaimDropERC721 dropRouter = BurnToClaimDropERC721(payable(address(drop))); + + vm.prank(deployer); + Permissions(address(drop)).renounceRole(keccak256("EXTENSION_ROLE"), deployer); + + vm.prank(deployer); + vm.expectRevert("ExtensionManager: unauthorized."); + dropRouter.addExtension(extension_permissions_new); + + vm.startPrank(deployer); + vm.expectRevert( + abi.encodePacked( + "Permissions: account ", + Strings.toHexString(uint160(deployer), 20), + " is missing role ", + Strings.toHexString(uint256(keccak256("EXTENSION_ROLE")), 32) + ) + ); + Permissions(address(drop)).grantRole(keccak256("EXTENSION_ROLE"), address(0x12345)); + vm.stopPrank(); + } +} diff --git a/src/test/burn-to-claim-drop-BTT/logic/burn-and-claim/burnAndClaim.t.sol b/src/test/burn-to-claim-drop-BTT/logic/burn-and-claim/burnAndClaim.t.sol new file mode 100644 index 000000000..eb188e56a --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/logic/burn-and-claim/burnAndClaim.t.sol @@ -0,0 +1,802 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../../utils/BaseTest.sol"; +import { BurnToClaimDropERC721 } from "contracts/prebuilts/unaudited/burn-to-claim-drop/BurnToClaimDropERC721.sol"; +import { BurnToClaimDrop721Logic, ERC721AUpgradeable, DelayedReveal, LazyMint, Drop, BurnToClaim, PrimarySale, PlatformFee } from "contracts/prebuilts/unaudited/burn-to-claim-drop/extension/BurnToClaimDrop721Logic.sol"; +import { PermissionsEnumerableImpl } from "contracts/extension/upgradeable/impl/PermissionsEnumerableImpl.sol"; +import { Royalty } from "contracts/extension/upgradeable/Royalty.sol"; +import { BatchMintMetadata } from "contracts/extension/upgradeable/BatchMintMetadata.sol"; +import { IBurnToClaim } from "contracts/extension/interface/IBurnToClaim.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import { Permissions } from "contracts/extension/Permissions.sol"; +import "erc721a-upgradeable/contracts/IERC721AUpgradeable.sol"; + +contract BurnToClaimDropERC721Logic_BurnAndClaim is BaseTest, IExtension { + using Strings for uint256; + using Strings for address; + + event TokensBurnedAndClaimed( + address indexed originContract, + address indexed tokenOwner, + uint256 indexed burnTokenId, + uint256 quantity + ); + + BurnToClaimDrop721Logic public drop; + uint256 internal _tokenId; + uint256 internal _quantity; + uint256 internal _msgValue; + uint256[] internal batchIds; + address internal caller; + bytes internal data; + IBurnToClaim.BurnToClaimInfo internal info; + + bytes private emptyEncodedBytes = abi.encode("", ""); + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address dropImpl = address(new BurnToClaimDropERC721(extensions)); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + drop = BurnToClaimDrop721Logic( + payable( + address( + new TWProxy( + dropImpl, + abi.encodeCall( + BurnToClaimDropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ) + ); + + caller = getActor(5); + + erc20.mint(deployer, 1_000 ether); + vm.deal(deployer, 1_000 ether); + erc20.mint(caller, 1_000 ether); + vm.deal(caller, 1_000 ether); + + erc721.mint(deployer, 100); + erc721NonBurnable.mint(deployer, 100); + + erc1155NonBurnable.mint(deployer, 0, 100); + erc1155.mint(deployer, 0, 100); + erc1155.mint(deployer, 1, 100); + + vm.startPrank(deployer); + erc721.setApprovalForAll(address(drop), true); + erc1155.setApprovalForAll(address(drop), true); + erc20.approve(address(drop), type(uint256).max); + vm.stopPrank(); + + vm.startPrank(caller); + erc721.setApprovalForAll(address(drop), true); + erc1155.setApprovalForAll(address(drop), true); + erc20.approve(address(drop), type(uint256).max); + vm.stopPrank(); + + // startId = 0; + // mint 5 batches + // vm.startPrank(deployer); + // for (uint256 i = 0; i < 5; i++) { + // uint256 _amount = (i + 1) * 10; + // uint256 batchId = startId + _amount; + // batchIds.push(batchId); + + // string memory baseURI = Strings.toString(batchId); + // startId = drop.lazyMint(_amount, baseURI, ""); + // } + // vm.stopPrank(); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](2); + + // Extension: Permissions + address permissions = address(new PermissionsEnumerableImpl()); + + Extension memory extension_permissions; + extension_permissions.metadata = ExtensionMetadata({ + name: "Permissions", + metadataURI: "ipfs://Permissions", + implementation: permissions + }); + + extension_permissions.functions = new ExtensionFunction[](1); + extension_permissions.functions[0] = ExtensionFunction( + Permissions.hasRole.selector, + "hasRole(bytes32,address)" + ); + + extensions[0] = extension_permissions; + + address dropLogic = address(new BurnToClaimDrop721Logic()); + + Extension memory extension_drop; + extension_drop.metadata = ExtensionMetadata({ + name: "BurnToClaimDrop721Logic", + metadataURI: "ipfs://BurnToClaimDrop721Logic", + implementation: dropLogic + }); + + extension_drop.functions = new ExtensionFunction[](10); + extension_drop.functions[0] = ExtensionFunction(BurnToClaimDrop721Logic.tokenURI.selector, "tokenURI(uint256)"); + extension_drop.functions[1] = ExtensionFunction( + BurnToClaimDrop721Logic.lazyMint.selector, + "lazyMint(uint256,string,bytes)" + ); + extension_drop.functions[2] = ExtensionFunction( + BurnToClaimDrop721Logic.setMaxTotalMinted.selector, + "setMaxTotalMinted(uint256)" + ); + extension_drop.functions[3] = ExtensionFunction( + BurnToClaimDrop721Logic.nextTokenIdToMint.selector, + "nextTokenIdToMint()" + ); + extension_drop.functions[4] = ExtensionFunction( + BurnToClaimDrop721Logic.burnAndClaim.selector, + "burnAndClaim(uint256,uint256)" + ); + extension_drop.functions[5] = ExtensionFunction( + BurnToClaim.getBurnToClaimInfo.selector, + "getBurnToClaimInfo()" + ); + extension_drop.functions[6] = ExtensionFunction( + BurnToClaim.setBurnToClaimInfo.selector, + "setBurnToClaimInfo((address,uint8,uint256,uint256,address))" + ); + extension_drop.functions[7] = ExtensionFunction( + BurnToClaimDrop721Logic.nextTokenIdToClaim.selector, + "nextTokenIdToClaim()" + ); + extension_drop.functions[8] = ExtensionFunction(ERC721AUpgradeable.balanceOf.selector, "balanceOf(address)"); + extension_drop.functions[9] = ExtensionFunction(ERC721AUpgradeable.ownerOf.selector, "ownerOf(uint256)"); + + extensions[1] = extension_drop; + } + + function test_burnAndClaim_notEnoughLazyMintedTokens() public { + vm.expectRevert("!Tokens"); + drop.burnAndClaim(0, 1); + } + + modifier whenEnoughLazyMintedTokens() { + vm.prank(deployer); + drop.lazyMint(1000, "ipfs://", ""); + _; + } + + function test_burnAndClaim_exceedMaxTotalMint() public whenEnoughLazyMintedTokens { + vm.prank(deployer); + drop.setMaxTotalMinted(1); //set max total mint cap as 1 + + vm.expectRevert("exceed max total mint cap."); + drop.burnAndClaim(0, 2); + } + + modifier whenNotExceedMaxTotalMinted() { + vm.prank(deployer); + drop.setMaxTotalMinted(1000); + _; + } + + function test_burnAndClaim_burnToClaimInfoNotSet() public whenEnoughLazyMintedTokens whenNotExceedMaxTotalMinted { + // it will fail when verifyClaim tries to check owner/balance on nft contract which is still address(0) + vm.expectRevert(); + drop.burnAndClaim(0, 1); + } + + // ================== + // ======= Test branch: burn-to-claim origin contract is ERC721 + // ================== + + modifier whenBurnToClaimInfoSetERC721() { + info = IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc721NonBurnable), + tokenType: IBurnToClaim.TokenType.ERC721, + tokenId: 0, + mintPriceForNewToken: 0, + currency: address(erc20) + }); + + vm.prank(deployer); + drop.setBurnToClaimInfo(info); + + _; + } + + function test_burnAndClaim_ERC721_invalidQuantity() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSetERC721 + { + vm.expectRevert("Invalid amount"); + drop.burnAndClaim(0, 0); + } + + modifier whenValidQuantityERC721() { + _quantity = 1; + _; + } + + function test_burnAndClaim_ERC721_notOwner() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSetERC721 + whenValidQuantityERC721 + { + vm.expectRevert("!Owner"); + drop.burnAndClaim(_tokenId, _quantity); + } + + modifier whenCorrectOwnerERC721() { + vm.startPrank(deployer); + erc721NonBurnable.transferFrom(deployer, caller, _tokenId); + erc721.transferFrom(deployer, caller, _tokenId); + vm.stopPrank(); + _; + } + + function test_burnAndClaim_ERC721_notBurnable() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSetERC721 + whenValidQuantityERC721 + whenCorrectOwnerERC721 + { + vm.expectRevert(); // `EvmError: Revert` when trying to burn on a non-burnable contract + vm.prank(caller); + drop.burnAndClaim(_tokenId, _quantity); + } + + modifier whenBurnToClaimInfoSet_ERC721Burnable() { + info = IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc721), + tokenType: IBurnToClaim.TokenType.ERC721, + tokenId: 0, + mintPriceForNewToken: 0, + currency: address(erc20) + }); + + vm.prank(deployer); + drop.setBurnToClaimInfo(info); + + _; + } + + function test_burnAndClaim_ERC721_mintPriceZero_msgValueNonZero() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC721Burnable + whenValidQuantityERC721 + whenCorrectOwnerERC721 + { + vm.expectRevert("!Value"); + vm.prank(caller); + drop.burnAndClaim{ value: 1 }(_tokenId, _quantity); + } + + modifier whenMsgValueZero() { + _msgValue = 0; + _; + } + + function test_burnAndClaim_ERC721_mintPriceZero() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC721Burnable + whenValidQuantityERC721 + whenCorrectOwnerERC721 + whenMsgValueZero + { + // state before + uint256 _nextTokenIdToClaim = drop.nextTokenIdToClaim(); + + // burn and claim + vm.prank(caller); + drop.burnAndClaim{ value: _msgValue }(_tokenId, _quantity); + + // check state after + vm.expectRevert(); // because token non-existent after burning + erc721.ownerOf(_tokenId); + + assertEq(drop.balanceOf(caller), _quantity); + assertEq(drop.ownerOf(_nextTokenIdToClaim), caller); + assertEq(drop.nextTokenIdToClaim(), _nextTokenIdToClaim + _quantity); + } + + function test_burnAndClaim_ERC721_mintPriceZero_event() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC721Burnable + whenValidQuantityERC721 + whenCorrectOwnerERC721 + whenMsgValueZero + { + vm.prank(caller); + vm.expectEmit(true, true, true, true); + emit TokensBurnedAndClaimed(address(erc721), caller, _tokenId, _quantity); + drop.burnAndClaim{ value: _msgValue }(_tokenId, _quantity); + } + + modifier whenBurnToClaimInfoSet_ERC721Burnable_nonZeroPriceNativeToken() { + info = IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc721), + tokenType: IBurnToClaim.TokenType.ERC721, + tokenId: 0, + mintPriceForNewToken: 100, + currency: NATIVE_TOKEN + }); + + vm.prank(deployer); + drop.setBurnToClaimInfo(info); + + _; + } + + function test_burnAndClaim_ERC721_mintPriceNonZero_nativeToken_incorrectMsgValue() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC721Burnable_nonZeroPriceNativeToken + whenValidQuantityERC721 + whenCorrectOwnerERC721 + { + uint256 incorrectTotalPrice = (info.mintPriceForNewToken * _quantity) + 1; + + vm.prank(caller); + vm.expectRevert("Invalid msg value"); + drop.burnAndClaim{ value: incorrectTotalPrice }(_tokenId, _quantity); + } + + modifier whenCorrectMsgValue() { + _msgValue = info.mintPriceForNewToken * _quantity; + _; + } + + function test_burnAndClaim_ERC721_mintPriceNonZero_nativeToken() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC721Burnable_nonZeroPriceNativeToken + whenValidQuantityERC721 + whenCorrectOwnerERC721 + whenCorrectMsgValue + { + // state before + uint256 _nextTokenIdToClaim = drop.nextTokenIdToClaim(); + assertEq(platformFeeRecipient.balance, 0); + assertEq(saleRecipient.balance, 0); + assertEq(caller.balance, 1000 ether); + + // burn and claim + vm.prank(caller); + drop.burnAndClaim{ value: _msgValue }(_tokenId, _quantity); + + // check state after + uint256 totalPrice = (info.mintPriceForNewToken * _quantity); + uint256 _platformFee = (totalPrice * platformFeeBps) / 10_000; + uint256 _saleProceeds = totalPrice - _platformFee; + vm.expectRevert(); // because token non-existent after burning + erc721.ownerOf(_tokenId); + + assertEq(drop.balanceOf(caller), _quantity); + assertEq(drop.ownerOf(_nextTokenIdToClaim), caller); + assertEq(drop.nextTokenIdToClaim(), _nextTokenIdToClaim + _quantity); + assertEq(platformFeeRecipient.balance, _platformFee); + assertEq(saleRecipient.balance, _saleProceeds); + assertEq(caller.balance, 1000 ether - totalPrice); + } + + function test_burnAndClaim_ERC721_mintPriceNonZero_nativeToken_event() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC721Burnable_nonZeroPriceNativeToken + whenValidQuantityERC721 + whenCorrectOwnerERC721 + whenCorrectMsgValue + { + vm.prank(caller); + vm.expectEmit(true, true, true, true); + emit TokensBurnedAndClaimed(address(erc721), caller, _tokenId, _quantity); + drop.burnAndClaim{ value: _msgValue }(_tokenId, _quantity); + } + + modifier whenBurnToClaimInfoSet_ERC721Burnable_nonZeroPriceERC20() { + info = IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc721), + tokenType: IBurnToClaim.TokenType.ERC721, + tokenId: 0, + mintPriceForNewToken: 100, + currency: address(erc20) + }); + + vm.prank(deployer); + drop.setBurnToClaimInfo(info); + + _; + } + + function test_burnAndClaim_ERC721_mintPriceNonZero_ERC20_nonZeroMsgValue() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC721Burnable_nonZeroPriceERC20 + whenValidQuantityERC721 + whenCorrectOwnerERC721 + { + vm.prank(caller); + vm.expectRevert("Invalid msg value"); + drop.burnAndClaim{ value: 1 }(_tokenId, _quantity); + } + + function test_burnAndClaim_ERC721_mintPriceNonZero_ERC20() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC721Burnable_nonZeroPriceERC20 + whenValidQuantityERC721 + whenCorrectOwnerERC721 + whenMsgValueZero + { + // state before + uint256 _nextTokenIdToClaim = drop.nextTokenIdToClaim(); + assertEq(erc20.balanceOf(platformFeeRecipient), 0); + assertEq(erc20.balanceOf(saleRecipient), 0); + assertEq(erc20.balanceOf(caller), 1000 ether); + + // burn and claim + vm.prank(caller); + drop.burnAndClaim{ value: _msgValue }(_tokenId, _quantity); + + // check state after + uint256 totalPrice = (info.mintPriceForNewToken * _quantity); + uint256 _platformFee = (totalPrice * platformFeeBps) / 10_000; + uint256 _saleProceeds = totalPrice - _platformFee; + vm.expectRevert(); // because token non-existent after burning + erc721.ownerOf(_tokenId); + + assertEq(drop.balanceOf(caller), _quantity); + assertEq(drop.ownerOf(_nextTokenIdToClaim), caller); + assertEq(drop.nextTokenIdToClaim(), _nextTokenIdToClaim + _quantity); + assertEq(erc20.balanceOf(platformFeeRecipient), _platformFee); + assertEq(erc20.balanceOf(saleRecipient), _saleProceeds); + assertEq(erc20.balanceOf(caller), 1000 ether - totalPrice); + } + + function test_burnAndClaim_ERC721_mintPriceNonZero_ERC20_event() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC721Burnable_nonZeroPriceERC20 + whenValidQuantityERC721 + whenCorrectOwnerERC721 + whenMsgValueZero + { + vm.prank(caller); + vm.expectEmit(true, true, true, true); + emit TokensBurnedAndClaimed(address(erc721), caller, _tokenId, _quantity); + drop.burnAndClaim{ value: _msgValue }(_tokenId, _quantity); + } + + // ================== + // ======= Test branch: burn-to-claim origin contract is ERC1155 + // ================== + + modifier whenBurnToClaimInfoSetERC1155() { + info = IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc1155NonBurnable), + tokenType: IBurnToClaim.TokenType.ERC1155, + tokenId: 0, + mintPriceForNewToken: 0, + currency: address(erc20) + }); + + vm.prank(deployer); + drop.setBurnToClaimInfo(info); + + _; + } + + function test_burnAndClaim_ERC1155_invalidTokenId() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSetERC1155 + { + vm.expectRevert("Invalid token Id"); + drop.burnAndClaim(1, 1); + } + + modifier whenValidTokenIdERC1155() { + _quantity = 1; + _tokenId = 0; + _; + } + + function test_burnAndClaim_ERC1155_notEnoughBalance() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSetERC1155 + whenValidTokenIdERC1155 + { + vm.expectRevert("!Balance"); + vm.prank(caller); + drop.burnAndClaim(_tokenId, _quantity); + } + + modifier whenEnoughBalanceERC1155() { + vm.startPrank(deployer); + erc1155NonBurnable.safeTransferFrom(deployer, caller, _tokenId, 100, ""); + erc1155.safeTransferFrom(deployer, caller, _tokenId, 100, ""); + vm.stopPrank(); + _; + } + + function test_burnAndClaim_ERC1155_notBurnable() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSetERC1155 + whenValidTokenIdERC1155 + whenEnoughBalanceERC1155 + { + vm.expectRevert(); // `EvmError: Revert` when trying to burn on a non-burnable contract + vm.prank(caller); + drop.burnAndClaim(_tokenId, _quantity); + } + + modifier whenBurnToClaimInfoSet_ERC1155Burnable() { + info = IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc1155), + tokenType: IBurnToClaim.TokenType.ERC1155, + tokenId: 0, + mintPriceForNewToken: 0, + currency: address(erc20) + }); + + vm.prank(deployer); + drop.setBurnToClaimInfo(info); + + _; + } + + function test_burnAndClaim_ERC1155_mintPriceZero_msgValueNonZero() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC1155Burnable + whenValidTokenIdERC1155 + whenEnoughBalanceERC1155 + { + vm.expectRevert("!Value"); + vm.prank(caller); + drop.burnAndClaim{ value: 1 }(_tokenId, _quantity); + } + + function test_burnAndClaim_ERC1155_mintPriceZero() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC1155Burnable + whenValidTokenIdERC1155 + whenEnoughBalanceERC1155 + whenMsgValueZero + { + // state before + uint256 _nextTokenIdToClaim = drop.nextTokenIdToClaim(); + + // burn and claim + vm.prank(caller); + drop.burnAndClaim{ value: _msgValue }(_tokenId, _quantity); + + // check state after + assertEq(erc1155.balanceOf(caller, _tokenId), 100 - _quantity); + assertEq(drop.balanceOf(caller), _quantity); + assertEq(drop.ownerOf(_nextTokenIdToClaim), caller); + assertEq(drop.nextTokenIdToClaim(), _nextTokenIdToClaim + _quantity); + } + + function test_burnAndClaim_ERC1155_mintPriceZero_event() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC1155Burnable + whenValidTokenIdERC1155 + whenEnoughBalanceERC1155 + whenMsgValueZero + { + vm.prank(caller); + vm.expectEmit(true, true, true, true); + emit TokensBurnedAndClaimed(address(erc1155), caller, _tokenId, _quantity); + drop.burnAndClaim{ value: _msgValue }(_tokenId, _quantity); + } + + modifier whenBurnToClaimInfoSet_ERC1155Burnable_nonZeroPriceNativeToken() { + info = IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc1155), + tokenType: IBurnToClaim.TokenType.ERC1155, + tokenId: 0, + mintPriceForNewToken: 100, + currency: NATIVE_TOKEN + }); + + vm.prank(deployer); + drop.setBurnToClaimInfo(info); + + _; + } + + function test_burnAndClaim_ERC1155_mintPriceNonZero_nativeToken_incorrectMsgValue() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC1155Burnable_nonZeroPriceNativeToken + whenValidTokenIdERC1155 + whenEnoughBalanceERC1155 + { + uint256 incorrectTotalPrice = (info.mintPriceForNewToken * _quantity) + 1; + + vm.prank(caller); + vm.expectRevert("Invalid msg value"); + drop.burnAndClaim{ value: incorrectTotalPrice }(_tokenId, _quantity); + } + + function test_burnAndClaim_ERC1155_mintPriceNonZero_nativeToken() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC1155Burnable_nonZeroPriceNativeToken + whenValidTokenIdERC1155 + whenEnoughBalanceERC1155 + whenCorrectMsgValue + { + // state before + uint256 _nextTokenIdToClaim = drop.nextTokenIdToClaim(); + assertEq(platformFeeRecipient.balance, 0); + assertEq(saleRecipient.balance, 0); + assertEq(caller.balance, 1000 ether); + + // burn and claim + vm.prank(caller); + drop.burnAndClaim{ value: _msgValue }(_tokenId, _quantity); + + // check state after + uint256 totalPrice = (info.mintPriceForNewToken * _quantity); + uint256 _platformFee = (totalPrice * platformFeeBps) / 10_000; + uint256 _saleProceeds = totalPrice - _platformFee; + + assertEq(erc1155.balanceOf(caller, _tokenId), 100 - _quantity); + assertEq(drop.balanceOf(caller), _quantity); + assertEq(drop.ownerOf(_nextTokenIdToClaim), caller); + assertEq(drop.nextTokenIdToClaim(), _nextTokenIdToClaim + _quantity); + assertEq(platformFeeRecipient.balance, _platformFee); + assertEq(saleRecipient.balance, _saleProceeds); + assertEq(caller.balance, 1000 ether - totalPrice); + } + + function test_burnAndClaim_ERC1155_mintPriceNonZero_nativeToken_event() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC1155Burnable_nonZeroPriceNativeToken + whenValidTokenIdERC1155 + whenEnoughBalanceERC1155 + whenCorrectMsgValue + { + vm.prank(caller); + vm.expectEmit(true, true, true, true); + emit TokensBurnedAndClaimed(address(erc1155), caller, _tokenId, _quantity); + drop.burnAndClaim{ value: _msgValue }(_tokenId, _quantity); + } + + modifier whenBurnToClaimInfoSet_ERC1155Burnable_nonZeroPriceERC20() { + info = IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc1155), + tokenType: IBurnToClaim.TokenType.ERC1155, + tokenId: 0, + mintPriceForNewToken: 100, + currency: address(erc20) + }); + + vm.prank(deployer); + drop.setBurnToClaimInfo(info); + + _; + } + + function test_burnAndClaim_ERC1155_mintPriceNonZero_ERC20_nonZeroMsgValue() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC1155Burnable_nonZeroPriceERC20 + whenValidTokenIdERC1155 + whenEnoughBalanceERC1155 + { + vm.prank(caller); + vm.expectRevert("Invalid msg value"); + drop.burnAndClaim{ value: 1 }(_tokenId, _quantity); + } + + function test_burnAndClaim_ERC1155_mintPriceNonZero_ERC20() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC1155Burnable_nonZeroPriceERC20 + whenValidTokenIdERC1155 + whenEnoughBalanceERC1155 + whenMsgValueZero + { + // state before + uint256 _nextTokenIdToClaim = drop.nextTokenIdToClaim(); + assertEq(erc20.balanceOf(platformFeeRecipient), 0); + assertEq(erc20.balanceOf(saleRecipient), 0); + assertEq(erc20.balanceOf(caller), 1000 ether); + + // burn and claim + vm.prank(caller); + drop.burnAndClaim{ value: _msgValue }(_tokenId, _quantity); + + // check state after + uint256 totalPrice = (info.mintPriceForNewToken * _quantity); + uint256 _platformFee = (totalPrice * platformFeeBps) / 10_000; + uint256 _saleProceeds = totalPrice - _platformFee; + + assertEq(erc1155.balanceOf(caller, _tokenId), 100 - _quantity); + assertEq(drop.balanceOf(caller), _quantity); + assertEq(drop.ownerOf(_nextTokenIdToClaim), caller); + assertEq(drop.nextTokenIdToClaim(), _nextTokenIdToClaim + _quantity); + assertEq(erc20.balanceOf(platformFeeRecipient), _platformFee); + assertEq(erc20.balanceOf(saleRecipient), _saleProceeds); + assertEq(erc20.balanceOf(caller), 1000 ether - totalPrice); + } + + function test_burnAndClaim_ERC1155_mintPriceNonZero_ERC20_event() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC1155Burnable_nonZeroPriceERC20 + whenValidTokenIdERC1155 + whenEnoughBalanceERC1155 + whenMsgValueZero + { + vm.prank(caller); + vm.expectEmit(true, true, true, true); + emit TokensBurnedAndClaimed(address(erc1155), caller, _tokenId, _quantity); + drop.burnAndClaim{ value: _msgValue }(_tokenId, _quantity); + } +} diff --git a/src/test/burn-to-claim-drop-BTT/logic/burn-and-claim/burnAndClaim.tree b/src/test/burn-to-claim-drop-BTT/logic/burn-and-claim/burnAndClaim.tree new file mode 100644 index 000000000..00e6af1b9 --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/logic/burn-and-claim/burnAndClaim.tree @@ -0,0 +1,83 @@ +burnAndClaim(uint256 _burnTokenId, uint256 _quantity) +├── when the sum of `_quantity` and total minted is greater than nextTokenIdToLazyMint +│ └── it should revert ✅ +└── when the sum of `_quantity` and total minted less than or equal to nextTokenIdToLazyMint + └── when maxTotalMinted is not zero ✅ // TODO when zero + └── when the sum of `_quantity` and total minted greater than maxTotalMinted + │ └── it should revert ✅ + └── when the sum of `_quantity` and total minted less than or equal to maxTotalMinted + ├── when burn-to-claim info is not set + │ └── it should revert ✅ + └── when burn-to-claim info is set, with token type ERC721 + │ ├── when `_quantity` is not 1 + │ │ └── it should revert ✅ + │ └── when `_quantity` param is 1 + │ ├── when caller (i.e. _dropMsgSender) is not the actual token owner + │ │ └── it should revert ✅ + │ └── when caller is the actual token owner + │ ├── when the origin ERC721 contract is not burnable + │ │ └── it should revert ✅ + │ └── when the origin ERC721 contract is burnable + │ └── when mint price (i.e. pricePerToken) is zero + │ │ └── when msg.value is not zero + │ │ │ └── it should revert ✅ + │ │ └── when msg.value is zero + │ │ └── it should successfully burn the token with given tokenId for the token owner ✅ + │ │ └── it should mint new tokens to caller ✅ + │ │ └── it should emit TokensBurnedAndClaimed event ✅ + │ └── when mint price is not zero + │ └── when currency is native token + │ │ └── when msg.value is not equal to total price + │ │ │ └── it should revert ✅ + │ │ └── when msg.value is equal to total price + │ │ └── it should successfully burn the token with given tokenId for the token owner ✅ + │ │ └── it should mint new tokens to caller ✅ + │ │ └── (transfer to sale recipient) ✅ + │ │ └── (transfer to fee recipient) ✅ + │ │ └── it should emit TokensBurnedAndClaimed event ✅ + │ └── when currency is some ERC20 token + │ └── when msg.value is not zero + │ │ └── it should revert ✅ + │ └── when msg.value is zero + │ └── it should successfully burn the token with given tokenId for the token owner ✅ + │ └── it should mint new tokens to caller ✅ + │ └── (transfer to sale recipient) ✅ + │ └── (transfer to fee recipient) ✅ + │ └── it should emit TokensBurnedAndClaimed event ✅ + └── when burn-to-claim info is set, with token type ERC1155 + ├── when `_burnTokenId` param doesn't match eligible tokenId + │ └── it should revert ✅ + └── when `_burnTokenId` param matches eligible tokenId + ├── when caller (i.e. _dropMsgSender) has balance less than quantity param + │ └── it should revert ✅ + └── when caller has balance greater than or equal to quantity param + ├── when the origin ERC1155 contract is not burnable + │ └── it should revert ✅ + └── when the origin ERC1155 contract is burnable + └── when mint price (i.e. pricePerToken) is zero + │ └── when msg.value is not zero + │ │ └── it should revert ✅ + │ └── when msg.value is zero + │ └── it should successfully burn the token with given tokenId for the token owner ✅ + │ └── it should mint new tokens to caller ✅ + │ └── it should emit TokensBurnedAndClaimed event ✅ + └── when mint price is not zero + └── when currency is native token + │ └── when msg.value is not equal to total price + │ │ └── it should revert ✅ + │ └── when msg.value is equal to total price + │ └── it should successfully burn the token with given tokenId for the token owner ✅ + │ └── it should mint new tokens to caller ✅ + │ └── (transfer to sale recipient) ✅ + │ └── (transfer to fee recipient) ✅ + │ └── it should emit TokensBurnedAndClaimed event ✅ + └── when currency is some ERC20 token + └── when msg.value is not zero + │ └── it should revert ✅ + └── when msg.value is zero + └── it should successfully burn the token with given tokenId for the token owner ✅ + └── it should mint new tokens to caller ✅ + └── (transfer to sale recipient) ✅ + └── (transfer to fee recipient) ✅ + └── it should emit TokensBurnedAndClaimed event ✅ + diff --git a/src/test/burn-to-claim-drop-BTT/logic/lazy-mint/lazyMint.t.sol b/src/test/burn-to-claim-drop-BTT/logic/lazy-mint/lazyMint.t.sol new file mode 100644 index 000000000..e9b19f812 --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/logic/lazy-mint/lazyMint.t.sol @@ -0,0 +1,265 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../../utils/BaseTest.sol"; +import { BurnToClaimDropERC721 } from "contracts/prebuilts/unaudited/burn-to-claim-drop/BurnToClaimDropERC721.sol"; +import { BurnToClaimDrop721Logic, ERC721AUpgradeable, DelayedReveal, LazyMint, Drop, BurnToClaim, PrimarySale, PlatformFee } from "contracts/prebuilts/unaudited/burn-to-claim-drop/extension/BurnToClaimDrop721Logic.sol"; +import { PermissionsEnumerableImpl } from "contracts/extension/upgradeable/impl/PermissionsEnumerableImpl.sol"; +import { Royalty } from "contracts/extension/upgradeable/Royalty.sol"; +import { BatchMintMetadata } from "contracts/extension/upgradeable/BatchMintMetadata.sol"; +import { IBurnToClaim } from "contracts/extension/interface/IBurnToClaim.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "erc721a-upgradeable/contracts/IERC721AUpgradeable.sol"; +import { Permissions } from "contracts/extension/Permissions.sol"; + +contract BurnToClaimDropERC721Logic_LazyMint is BaseTest, IExtension { + using Strings for uint256; + using Strings for address; + + event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI); + + BurnToClaimDrop721Logic public drop; + uint256 internal startId; + uint256 internal amount; + uint256[] internal batchIds; + address internal caller; + bytes internal data; + bytes internal encryptedUri; + bytes32 internal provenanceHash; + + bytes private emptyEncodedBytes = abi.encode("", ""); + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address dropImpl = address(new BurnToClaimDropERC721(extensions)); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + drop = BurnToClaimDrop721Logic( + payable( + address( + new TWProxy( + dropImpl, + abi.encodeCall( + BurnToClaimDropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ) + ); + + erc20.mint(deployer, 1_000 ether); + vm.deal(deployer, 1_000 ether); + + startId = 0; + // mint 5 batches + vm.startPrank(deployer); + for (uint256 i = 0; i < 5; i++) { + uint256 _amount = (i + 1) * 10; + uint256 batchId = startId + _amount; + batchIds.push(batchId); + + string memory baseURI = Strings.toString(batchId); + startId = drop.lazyMint(_amount, baseURI, ""); + } + vm.stopPrank(); + + encryptedUri = bytes("ipfs://encryptedURI"); + provenanceHash = keccak256("provenanceHash"); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](2); + + // Extension: Permissions + address permissions = address(new PermissionsEnumerableImpl()); + + Extension memory extension_permissions; + extension_permissions.metadata = ExtensionMetadata({ + name: "Permissions", + metadataURI: "ipfs://Permissions", + implementation: permissions + }); + + extension_permissions.functions = new ExtensionFunction[](1); + extension_permissions.functions[0] = ExtensionFunction( + Permissions.hasRole.selector, + "hasRole(bytes32,address)" + ); + + extensions[0] = extension_permissions; + + address dropLogic = address(new BurnToClaimDrop721Logic()); + + Extension memory extension_drop; + extension_drop.metadata = ExtensionMetadata({ + name: "BurnToClaimDrop721Logic", + metadataURI: "ipfs://BurnToClaimDrop721Logic", + implementation: dropLogic + }); + + extension_drop.functions = new ExtensionFunction[](6); + extension_drop.functions[0] = ExtensionFunction(BurnToClaimDrop721Logic.tokenURI.selector, "tokenURI(uint256)"); + extension_drop.functions[1] = ExtensionFunction( + BurnToClaimDrop721Logic.lazyMint.selector, + "lazyMint(uint256,string,bytes)" + ); + extension_drop.functions[2] = ExtensionFunction( + BatchMintMetadata.getBaseURICount.selector, + "getBaseURICount()" + ); + extension_drop.functions[3] = ExtensionFunction( + BurnToClaimDrop721Logic.nextTokenIdToMint.selector, + "nextTokenIdToMint()" + ); + extension_drop.functions[4] = ExtensionFunction( + DelayedReveal.encryptDecrypt.selector, + "encryptDecrypt(bytes,bytes)" + ); + extension_drop.functions[5] = ExtensionFunction( + DelayedReveal.isEncryptedBatch.selector, + "isEncryptedBatch(uint256)" + ); + + extensions[1] = extension_drop; + } + + // ================== + // ======= Test branch: when `data` empty + // ================== + + function test_lazyMint_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert("Not authorized"); + drop.lazyMint(amount, "", ""); + } + + modifier whenCallerAuthorized() { + caller = deployer; + _; + } + + function test_lazyMint_zeroAmount() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectRevert("0 amt"); + drop.lazyMint(amount, "", ""); + } + + modifier whenAmountNotZero() { + amount = 50; + _; + } + + function test_lazyMint() public whenCallerAuthorized whenAmountNotZero { + // check previous state + uint256 _nextTokenIdToLazyMintOld = drop.nextTokenIdToMint(); + assertEq(_nextTokenIdToLazyMintOld, batchIds[4]); + + string memory baseURI = "ipfs://baseURI"; + + // lazy mint next batch + vm.prank(address(caller)); + uint256 _batchId = drop.lazyMint(amount, baseURI, ""); + + // check new state + assertEq(_batchId, _nextTokenIdToLazyMintOld + amount); + for (uint256 i = _nextTokenIdToLazyMintOld; i < _batchId; i++) { + assertEq(drop.tokenURI(i), string(abi.encodePacked(baseURI, i.toString()))); + } + assertEq(drop.nextTokenIdToMint(), _nextTokenIdToLazyMintOld + amount); + assertEq(drop.getBaseURICount(), batchIds.length + 1); + } + + function test_lazyMint_event() public whenCallerAuthorized whenAmountNotZero { + string memory baseURI = "ipfs://baseURI"; + uint256 _nextTokenIdToLazyMintOld = drop.nextTokenIdToMint(); + + // lazy mint next batch + vm.prank(address(caller)); + vm.expectEmit(true, false, false, true); + emit TokensLazyMinted(_nextTokenIdToLazyMintOld, _nextTokenIdToLazyMintOld + amount - 1, baseURI, ""); + drop.lazyMint(amount, baseURI, ""); + } + + // ================== + // ======= Test branch: when `data` not empty + // ================== + + function test_lazyMint_withData_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert("Not authorized"); + drop.lazyMint(amount, "", data); + } + + function test_lazyMint_withData_zeroAmount() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectRevert("0 amt"); + drop.lazyMint(amount, "", data); + } + + function test_lazyMint_withData_incorrectData() public whenCallerAuthorized whenAmountNotZero { + data = bytes("random data"); // not bytes+bytes32 encoded as expected + vm.prank(address(caller)); + vm.expectRevert(); + drop.lazyMint(amount, "", data); + } + + modifier whenCorrectEncodingOfData() { + data = abi.encode(encryptedUri, provenanceHash); + _; + } + + function test_lazyMint_withData() public whenCallerAuthorized whenAmountNotZero whenCorrectEncodingOfData { + // check previous state + uint256 _nextTokenIdToLazyMintOld = drop.nextTokenIdToMint(); + assertEq(_nextTokenIdToLazyMintOld, batchIds[4]); + + string memory placeholderURI = "ipfs://placeholderURI"; + + // lazy mint next batch + vm.prank(address(caller)); + uint256 _batchId = drop.lazyMint(amount, placeholderURI, data); + + // check new state + assertTrue(drop.isEncryptedBatch(_batchId)); // encrypted batch + assertEq(_batchId, _nextTokenIdToLazyMintOld + amount); + for (uint256 i = _nextTokenIdToLazyMintOld; i < _batchId; i++) { + assertEq(drop.tokenURI(i), string(abi.encodePacked(placeholderURI, "0"))); // encrypted batch, hence token-id 0 + } + assertEq(drop.nextTokenIdToMint(), _nextTokenIdToLazyMintOld + amount); + assertEq(drop.getBaseURICount(), batchIds.length + 1); + } + + function test_lazyMint_withData_event() public whenCallerAuthorized whenAmountNotZero whenCorrectEncodingOfData { + string memory placeholderURI = "ipfs://placeholderURI"; + uint256 _nextTokenIdToLazyMintOld = drop.nextTokenIdToMint(); + + // lazy mint next batch + vm.prank(address(caller)); + vm.expectEmit(true, false, false, true); + emit TokensLazyMinted(_nextTokenIdToLazyMintOld, _nextTokenIdToLazyMintOld + amount - 1, placeholderURI, data); + drop.lazyMint(amount, placeholderURI, data); + } +} diff --git a/src/test/burn-to-claim-drop-BTT/logic/lazy-mint/lazyMint.tree b/src/test/burn-to-claim-drop-BTT/logic/lazy-mint/lazyMint.tree new file mode 100644 index 000000000..39b512286 --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/logic/lazy-mint/lazyMint.tree @@ -0,0 +1,38 @@ +lazyMint( + uint256 _amount, + string calldata _baseURIForTokens, + bytes calldata _data +) +// Assume `_data` empty +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── when amount to lazy mint is 0 + │ └── it should revert ✅ + └── when amount to lazy mint is not 0 + └── it should save the batch of tokens starting at `nextTokenIdToLazyMint` ✅ + └── it should store batch id equal to the sum of `nextTokenIdToLazyMint` and `_amount` ✅ + └── it should map the new batch id to `_baseURIForTokens` ✅ + └── it should increase `nextTokenIdToLazyMint` by `_amount` ✅ + └── it should return the new `batchId` ✅ + └── it should emit TokensLazyMinted event ✅ + +// Assume `_data` not empty +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── when amount to lazy mint is 0 + │ └── it should revert ✅ + └── when amount to lazy mint is not 0 + └── when data can't be decoded + │ └── it should revert ✅ + └── when data can be decoded successfully + └── when decoded encryptedURI and provenanceHash are non-empty + └── it should set encrypted data for the new batch equal to _data ✅ + └── it should save the batch of tokens starting at `nextTokenIdToLazyMint` ✅ + └── it should store batch id equal to the sum of `nextTokenIdToLazyMint` and `_amount` ✅ + └── it should map the new batch id to `_baseURIForTokens` ✅ + └── it should increase `nextTokenIdToLazyMint` by `_amount` ✅ + └── it should return the new `batchId` ✅ + └── it should emit TokensLazyMinted event ✅ + diff --git a/src/test/burn-to-claim-drop-BTT/logic/other-functions/other.t.sol b/src/test/burn-to-claim-drop-BTT/logic/other-functions/other.t.sol new file mode 100644 index 000000000..0f99059b6 --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/logic/other-functions/other.t.sol @@ -0,0 +1,395 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../../utils/BaseTest.sol"; +import { BurnToClaimDropERC721 } from "contracts/prebuilts/unaudited/burn-to-claim-drop/BurnToClaimDropERC721.sol"; +import { BurnToClaimDrop721Logic, IERC2981 } from "contracts/prebuilts/unaudited/burn-to-claim-drop/extension/BurnToClaimDrop721Logic.sol"; +import { IDrop } from "contracts/extension/interface/IDrop.sol"; +import { IStaking721 } from "contracts/extension/interface/IStaking721.sol"; +import { PermissionsEnumerableImpl } from "contracts/extension/upgradeable/impl/PermissionsEnumerableImpl.sol"; + +import { ERC721AStorage } from "contracts/extension/upgradeable/init/ERC721AInit.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { Permissions } from "contracts/extension/Permissions.sol"; + +contract MyBurnToClaimDrop721Logic is BurnToClaimDrop721Logic { + function canSetPlatformFeeInfo() external view returns (bool) { + return _canSetPlatformFeeInfo(); + } + + function canSetPrimarySaleRecipient() external view returns (bool) { + return _canSetPrimarySaleRecipient(); + } + + function canSetOwner() external view returns (bool) { + return _canSetOwner(); + } + + function canSetRoyaltyInfo() external view returns (bool) { + return _canSetRoyaltyInfo(); + } + + function canSetContractURI() external view returns (bool) { + return _canSetContractURI(); + } + + function canSetClaimConditions() external view returns (bool) { + return _canSetClaimConditions(); + } + + function canLazyMint() external view returns (bool) { + return _canLazyMint(); + } + + function canSetBurnToClaim() external view returns (bool) { + return _canSetBurnToClaim(); + } + + function beforeTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity) external { + _beforeTokenTransfers(from, to, startTokenId, quantity); + } + + function transferTokensOnClaim(address _to, uint256 _quantityBeingClaimed) external returns (uint256 startTokenId) { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + startTokenId = data._currentIndex; + _safeMint(_to, _quantityBeingClaimed); + } + + function beforeClaim(uint256 _quantity, AllowlistProof calldata proof) external { + _beforeClaim(address(0), _quantity, address(0), 0, proof, ""); + } + + function mintTo(address _recipient) external { + _safeMint(_recipient, 1); + } +} + +contract BurnToClaimDrop721Logic_OtherFunctions is BaseTest, IExtension { + MyBurnToClaimDrop721Logic public drop; + address internal caller; + address internal recipient; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address dropImpl = address(new BurnToClaimDropERC721(extensions)); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + drop = MyBurnToClaimDrop721Logic( + payable( + address( + new TWProxy( + dropImpl, + abi.encodeCall( + BurnToClaimDropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ) + ); + + caller = getActor(5); + recipient = getActor(6); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](2); + + // Extension: Permissions + address permissions = address(new PermissionsEnumerableImpl()); + + Extension memory extension_permissions; + extension_permissions.metadata = ExtensionMetadata({ + name: "Permissions", + metadataURI: "ipfs://Permissions", + implementation: permissions + }); + + extension_permissions.functions = new ExtensionFunction[](3); + extension_permissions.functions[0] = ExtensionFunction( + Permissions.hasRole.selector, + "hasRole(bytes32,address)" + ); + extension_permissions.functions[1] = ExtensionFunction( + Permissions.grantRole.selector, + "grantRole(bytes32,address)" + ); + extension_permissions.functions[2] = ExtensionFunction( + Permissions.revokeRole.selector, + "revokeRole(bytes32,address)" + ); + + extensions[0] = extension_permissions; + + address dropLogic = address(new MyBurnToClaimDrop721Logic()); + + Extension memory extension_drop; + extension_drop.metadata = ExtensionMetadata({ + name: "MyBurnToClaimDrop721Logic", + metadataURI: "ipfs://MyBurnToClaimDrop721Logic", + implementation: dropLogic + }); + + extension_drop.functions = new ExtensionFunction[](18); + extension_drop.functions[0] = ExtensionFunction( + MyBurnToClaimDrop721Logic.canSetPlatformFeeInfo.selector, + "canSetPlatformFeeInfo()" + ); + extension_drop.functions[1] = ExtensionFunction( + MyBurnToClaimDrop721Logic.canSetPrimarySaleRecipient.selector, + "canSetPrimarySaleRecipient()" + ); + extension_drop.functions[2] = ExtensionFunction( + MyBurnToClaimDrop721Logic.canSetOwner.selector, + "canSetOwner()" + ); + extension_drop.functions[3] = ExtensionFunction( + MyBurnToClaimDrop721Logic.canSetRoyaltyInfo.selector, + "canSetRoyaltyInfo()" + ); + extension_drop.functions[4] = ExtensionFunction( + MyBurnToClaimDrop721Logic.canSetClaimConditions.selector, + "canSetClaimConditions()" + ); + extension_drop.functions[5] = ExtensionFunction( + MyBurnToClaimDrop721Logic.canSetContractURI.selector, + "canSetContractURI()" + ); + extension_drop.functions[6] = ExtensionFunction( + MyBurnToClaimDrop721Logic.canLazyMint.selector, + "canLazyMint()" + ); + extension_drop.functions[7] = ExtensionFunction( + MyBurnToClaimDrop721Logic.canSetBurnToClaim.selector, + "canSetBurnToClaim()" + ); + extension_drop.functions[8] = ExtensionFunction( + MyBurnToClaimDrop721Logic.beforeTokenTransfers.selector, + "beforeTokenTransfers(address,address,uint256,uint256)" + ); + extension_drop.functions[9] = ExtensionFunction(BurnToClaimDrop721Logic.totalMinted.selector, "totalMinted()"); + extension_drop.functions[10] = ExtensionFunction( + MyBurnToClaimDrop721Logic.transferTokensOnClaim.selector, + "transferTokensOnClaim(address,uint256)" + ); + extension_drop.functions[11] = ExtensionFunction( + BurnToClaimDrop721Logic.supportsInterface.selector, + "supportsInterface(bytes4)" + ); + extension_drop.functions[12] = ExtensionFunction( + MyBurnToClaimDrop721Logic.beforeClaim.selector, + "beforeClaim(uint256,(bytes32[],uint256,uint256,address))" + ); + extension_drop.functions[13] = ExtensionFunction( + BurnToClaimDrop721Logic.lazyMint.selector, + "lazyMint(uint256,string,bytes)" + ); + extension_drop.functions[14] = ExtensionFunction( + BurnToClaimDrop721Logic.setMaxTotalMinted.selector, + "setMaxTotalMinted(uint256)" + ); + extension_drop.functions[15] = ExtensionFunction(BurnToClaimDrop721Logic.burn.selector, "burn(uint256)"); + extension_drop.functions[16] = ExtensionFunction(MyBurnToClaimDrop721Logic.mintTo.selector, "mintTo(address)"); + extension_drop.functions[17] = ExtensionFunction( + IERC721.setApprovalForAll.selector, + "setApprovalForAll(address,bool)" + ); + + extensions[1] = extension_drop; + } + + modifier whenCallerAuthorized() { + caller = deployer; + _; + } + + function test_canSetPlatformFeeInfo_notAuthorized() public { + vm.prank(caller); + drop.canSetPlatformFeeInfo(); + } + + function test_canSetPlatformFeeInfo() public whenCallerAuthorized { + vm.prank(caller); + assertTrue(drop.canSetPlatformFeeInfo()); + } + + function test_canSetPrimarySaleRecipient_notAuthorized() public { + vm.prank(caller); + drop.canSetPrimarySaleRecipient(); + } + + function test_canSetPrimarySaleRecipient() public whenCallerAuthorized { + vm.prank(caller); + assertTrue(drop.canSetPrimarySaleRecipient()); + } + + function test_canSetOwner_notAuthorized() public { + vm.prank(caller); + drop.canSetOwner(); + } + + function test_canSetOwner() public whenCallerAuthorized { + vm.prank(caller); + assertTrue(drop.canSetOwner()); + } + + function test_canSetRoyaltyInfo_notAuthorized() public { + vm.prank(caller); + drop.canSetRoyaltyInfo(); + } + + function test_canSetRoyaltyInfo() public whenCallerAuthorized { + vm.prank(caller); + assertTrue(drop.canSetRoyaltyInfo()); + } + + function test_canSetContractURI_notAuthorized() public { + vm.prank(caller); + drop.canSetContractURI(); + } + + function test_canSetContractURI() public whenCallerAuthorized { + vm.prank(caller); + assertTrue(drop.canSetContractURI()); + } + + function test_canSetClaimConditions_notAuthorized() public { + vm.prank(caller); + drop.canSetClaimConditions(); + } + + function test_canSetClaimConditions() public whenCallerAuthorized { + vm.prank(caller); + assertTrue(drop.canSetClaimConditions()); + } + + function test_canLazyMint_notAuthorized() public { + vm.prank(caller); + drop.canLazyMint(); + } + + function test_canLazyMint() public whenCallerAuthorized { + vm.prank(caller); + assertTrue(drop.canLazyMint()); + } + + function test_canSetBurnToClaim_notAuthorized() public { + vm.prank(caller); + drop.canSetBurnToClaim(); + } + + function test_canSetBurnToClaim() public whenCallerAuthorized { + vm.prank(caller); + assertTrue(drop.canSetBurnToClaim()); + } + + function test_beforeTokenTransfers_restricted_notTransferRole() public { + vm.prank(deployer); + Permissions(address(drop)).revokeRole(keccak256("TRANSFER_ROLE"), address(0)); + vm.expectRevert("!Transfer-Role"); + drop.beforeTokenTransfers(caller, address(0x123), 0, 1); + } + + modifier whenTransferRole() { + vm.prank(deployer); + Permissions(address(drop)).grantRole(keccak256("TRANSFER_ROLE"), caller); + _; + } + + function test_beforeTokenTransfers_restricted() public whenTransferRole { + drop.beforeTokenTransfers(caller, address(0x123), 0, 1); + } + + function test_totalMinted() public { + uint256 totalMinted = drop.totalMinted(); + assertEq(totalMinted, 0); + + // mint tokens + drop.transferTokensOnClaim(caller, 10); + totalMinted = drop.totalMinted(); + assertEq(totalMinted, 10); + } + + function test_supportsInterface() public { + assertTrue(drop.supportsInterface(type(IERC2981).interfaceId)); + assertFalse(drop.supportsInterface(type(IStaking721).interfaceId)); + } + + function test_beforeClaim() public { + bytes32[] memory emptyBytes32Array = new bytes32[](0); + IDrop.AllowlistProof memory proof = IDrop.AllowlistProof(emptyBytes32Array, 0, 0, address(0)); + drop.beforeClaim(0, proof); + + vm.expectRevert("!Tokens"); + drop.beforeClaim(1, proof); + + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", ""); + + vm.prank(deployer); + drop.setMaxTotalMinted(1); + + vm.expectRevert("exceed max total mint cap."); + drop.beforeClaim(10, proof); + + vm.prank(deployer); + drop.setMaxTotalMinted(0); + + drop.beforeClaim(10, proof); // no revert if max total mint cap is set to 0 + } + + //=========== burn tests ========= + + function test_burn_whenNotOwnerNorApproved() public { + // mint + drop.mintTo(recipient); + + // burn + vm.expectRevert(); + drop.burn(0); + } + + function test_burn_whenOwner() public { + // mint + drop.mintTo(recipient); + + // burn + vm.prank(recipient); + drop.burn(0); + + vm.expectRevert(); // checking non-existent token, because burned + drop.ownerOf(0); + } + + function test_burn_whenApproved() public { + drop.mintTo(recipient); + + vm.prank(recipient); + drop.setApprovalForAll(caller, true); + + // burn + vm.prank(caller); + drop.burn(0); + + vm.expectRevert(); // checking non-existent token, because burned + drop.ownerOf(0); + } +} diff --git a/src/test/burn-to-claim-drop-BTT/logic/other-functions/other.tree b/src/test/burn-to-claim-drop-BTT/logic/other-functions/other.tree new file mode 100644 index 000000000..a7ed4a957 --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/logic/other-functions/other.tree @@ -0,0 +1,86 @@ +_canSetPlatformFeeInfo() +├── when the caller doesn't have DEFAULT_ADMIN_ROLE +│ └── it should revert ✅ +└── when the caller has DEFAULT_ADMIN_ROLE + └── it should return true ✅ + +_canSetPrimarySaleRecipient() +├── when the caller doesn't have DEFAULT_ADMIN_ROLE +│ └── it should revert ✅ +└── when the caller has DEFAULT_ADMIN_ROLE + └── it should return true ✅ + +_canSetOwner() +├── when the caller doesn't have DEFAULT_ADMIN_ROLE +│ └── it should revert ✅ +└── when the caller has DEFAULT_ADMIN_ROLE + └── it should return true ✅ + +_canSetRoyaltyInfo() +├── when the caller doesn't have DEFAULT_ADMIN_ROLE +│ └── it should revert ✅ +└── when the caller has DEFAULT_ADMIN_ROLE + └── it should return true ✅ + +_canSetContractURI() +├── when the caller doesn't have DEFAULT_ADMIN_ROLE +│ └── it should revert ✅ +└── when the caller has DEFAULT_ADMIN_ROLE + └── it should return true ✅ + +_canSetClaimConditions() +├── when the caller doesn't have DEFAULT_ADMIN_ROLE +│ └── it should revert ✅ +└── when the caller has DEFAULT_ADMIN_ROLE + └── it should return true ✅ + +_canLazyMint() +├── when the caller doesn't have MINTER_ROLE +│ └── it should revert ✅ +└── when the caller has MINTER_ROLE + └── it should return true ✅ + +_canSetBurnToClaim() +├── when the caller doesn't have DEFAULT_ADMIN_ROLE +│ └── it should revert ✅ +└── when the caller has DEFAULT_ADMIN_ROLE + └── it should return true ✅ + +burn(uint256 tokenId) +├── when the caller isn't the owner of `tokenId` or token not approved to caller +│ └── it should revert ✅ +└── when the caller owns `tokenId` +│ └── it should burn the token ✅ +└── when the `tokenId` is approved to caller + └── it should burn the token ✅ + +_beforeTokenTransfers( + address from, + address to, + uint256 startTokenId, + uint256 quantity +) +├── when transfers are restricted (i.e. address(0) doesn't have transfer role, or from-to addresses are not address(0) +│ └── when from and to don't have transfer role +│ └── it should revert ✅ + +totalMinted() +├── should return the quantity of tokens minted (i.e. claimed) so far ✅ + +supportsInterface(bytes4 interfaceId) +├── it should return true for supported interface ✅ +├── it should return false for not supported interface ✅ + +_beforeClaim( + address, + uint256 _quantity, + address, + uint256, + AllowlistProof calldata, + bytes memory +) +├── when `_quantity` exceeds lazy minted quantity +│ └── it should revert ✅ +├── when `_quantity` exceeds max total mint cap (if not zero) +│ └── it should revert ✅ + diff --git a/src/test/burn-to-claim-drop-BTT/logic/reveal/reveal.t.sol b/src/test/burn-to-claim-drop-BTT/logic/reveal/reveal.t.sol new file mode 100644 index 000000000..f60d11351 --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/logic/reveal/reveal.t.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../../utils/BaseTest.sol"; +import { BurnToClaimDropERC721 } from "contracts/prebuilts/unaudited/burn-to-claim-drop/BurnToClaimDropERC721.sol"; +import { BurnToClaimDrop721Logic, ERC721AUpgradeable, DelayedReveal, LazyMint, Drop, BurnToClaim, PrimarySale, PlatformFee } from "contracts/prebuilts/unaudited/burn-to-claim-drop/extension/BurnToClaimDrop721Logic.sol"; +import { PermissionsEnumerableImpl } from "contracts/extension/upgradeable/impl/PermissionsEnumerableImpl.sol"; +import { Royalty } from "contracts/extension/upgradeable/Royalty.sol"; +import { BatchMintMetadata } from "contracts/extension/upgradeable/BatchMintMetadata.sol"; +import { IBurnToClaim } from "contracts/extension/interface/IBurnToClaim.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "erc721a-upgradeable/contracts/IERC721AUpgradeable.sol"; + +contract BurnToClaimDropERC721Logic_Reveal is BaseTest, IExtension { + using Strings for uint256; + using Strings for address; + + event TokenURIRevealed(uint256 indexed index, string revealedURI); + + BurnToClaimDrop721Logic public drop; + uint256 internal startId; + uint256 internal amount; + uint256[] internal batchIds; + address internal caller; + bytes internal data; + string internal placeholderURI; + bytes internal originalURI; + uint256 internal _index; + bytes internal _key; + + bytes private emptyEncodedBytes = abi.encode("", ""); + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address dropImpl = address(new BurnToClaimDropERC721(extensions)); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + drop = BurnToClaimDrop721Logic( + payable( + address( + new TWProxy( + dropImpl, + abi.encodeCall( + BurnToClaimDropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ) + ); + + erc20.mint(deployer, 1_000 ether); + vm.deal(deployer, 1_000 ether); + + startId = 0; + originalURI = bytes("ipfs://originalURI"); + placeholderURI = "ipfs://placeholderURI"; + _key = "key123"; + // mint 3 batches + vm.startPrank(deployer); + for (uint256 i = 0; i < 3; i++) { + uint256 _amount = (i + 1) * 10; + uint256 batchId = startId + _amount; + batchIds.push(batchId); + + // set encrypted uri for one of the batches + if (i == 1) { + bytes memory _encryptedURI = drop.encryptDecrypt(originalURI, _key); + bytes32 _provenanceHash = keccak256(abi.encodePacked(originalURI, _key, block.chainid)); + + startId = drop.lazyMint(_amount, placeholderURI, abi.encode(_encryptedURI, _provenanceHash)); + } else { + startId = drop.lazyMint(_amount, string(originalURI), ""); + } + } + vm.stopPrank(); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](2); + + // Extension: Permissions + address permissions = address(new PermissionsEnumerableImpl()); + + Extension memory extension_permissions; + extension_permissions.metadata = ExtensionMetadata({ + name: "Permissions", + metadataURI: "ipfs://Permissions", + implementation: permissions + }); + + extension_permissions.functions = new ExtensionFunction[](1); + extension_permissions.functions[0] = ExtensionFunction( + Permissions.hasRole.selector, + "hasRole(bytes32,address)" + ); + + extensions[0] = extension_permissions; + + address dropLogic = address(new BurnToClaimDrop721Logic()); + + Extension memory extension_drop; + extension_drop.metadata = ExtensionMetadata({ + name: "BurnToClaimDrop721Logic", + metadataURI: "ipfs://BurnToClaimDrop721Logic", + implementation: dropLogic + }); + + extension_drop.functions = new ExtensionFunction[](6); + extension_drop.functions[0] = ExtensionFunction(BurnToClaimDrop721Logic.tokenURI.selector, "tokenURI(uint256)"); + extension_drop.functions[1] = ExtensionFunction( + BurnToClaimDrop721Logic.lazyMint.selector, + "lazyMint(uint256,string,bytes)" + ); + extension_drop.functions[2] = ExtensionFunction( + BurnToClaimDrop721Logic.reveal.selector, + "reveal(uint256,bytes)" + ); + extension_drop.functions[3] = ExtensionFunction( + DelayedReveal.encryptDecrypt.selector, + "encryptDecrypt(bytes,bytes)" + ); + extension_drop.functions[4] = ExtensionFunction( + DelayedReveal.isEncryptedBatch.selector, + "isEncryptedBatch(uint256)" + ); + extension_drop.functions[5] = ExtensionFunction( + DelayedReveal.getRevealURI.selector, + "getRevealURI(uint256,bytes)" + ); + + extensions[1] = extension_drop; + } + + function test_reveal_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert("Not authorized"); + drop.lazyMint(amount, "", ""); + } + + modifier whenCallerAuthorized() { + caller = deployer; + _; + } + + function test_reveal_invalidIndex() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectRevert("Invalid index"); + drop.reveal(4, "key"); + } + + modifier whenValidIndex() { + _; + } + + function test_reveal_noEncryptedURI() public whenCallerAuthorized whenValidIndex { + _index = 2; + vm.prank(address(caller)); + vm.expectRevert("Nothing to reveal"); + drop.reveal(_index, "key"); + } + + modifier whenEncryptedURI() { + _index = 1; + _; + } + + function test_reveal_incorrectKey() public whenCallerAuthorized whenValidIndex whenEncryptedURI { + vm.prank(address(caller)); + vm.expectRevert("Incorrect key"); + drop.reveal(_index, "incorrect key"); + } + + modifier whenCorrectKey() { + _; + } + + function test_reveal() public whenCallerAuthorized whenValidIndex whenEncryptedURI { + //state before + for (uint256 i = 0; i < 3; i++) { + uint256 _startId = i > 0 ? batchIds[i - 1] : 0; + + for (uint256 j = _startId; j < batchIds[i]; j += 1) { + string memory uri = drop.tokenURI(j); + if (i == 1) { + assertEq(uri, string(abi.encodePacked(placeholderURI, "0"))); // <-- placeholder URI for encrypted batch + } else { + assertEq(uri, string(abi.encodePacked(string(originalURI), j.toString()))); + } + } + } + + // reveal + vm.prank(address(caller)); + string memory revealedURI = drop.reveal(_index, _key); + + // check state after + vm.expectRevert("Nothing to reveal"); + drop.getRevealURI(_index, _key); + + assertEq(revealedURI, string(originalURI)); + + for (uint256 i = 0; i < 3; i++) { + uint256 _startId = i > 0 ? batchIds[i - 1] : 0; + + for (uint256 j = _startId; j < batchIds[i]; j += 1) { + string memory uri = drop.tokenURI(j); + assertEq(uri, string(abi.encodePacked(string(originalURI), j.toString()))); + } + } + } + + function test_reveal_event() public whenCallerAuthorized whenValidIndex whenEncryptedURI { + vm.prank(address(caller)); + vm.expectEmit(true, false, false, true); + emit TokenURIRevealed(1, string(originalURI)); + drop.reveal(_index, _key); + } +} diff --git a/src/test/burn-to-claim-drop-BTT/logic/reveal/reveal.tree b/src/test/burn-to-claim-drop-BTT/logic/reveal/reveal.tree new file mode 100644 index 000000000..febcdb258 --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/logic/reveal/reveal.tree @@ -0,0 +1,16 @@ +reveal(uint256 index, bytes calldata key) +├── when caller doesn't have minter_role +│ └── it should revert ✅ +└── when caller has minter role + ├── when index is more than number of batches + │ └── it should revert ✅ + └── when index is within total number of batches + ├── when there is no encrypted uri associated with the batch index + │ └── it should revert ✅ + └── when there is an encrypted uri present + ├── when the provenance hash generated is incorrect for the given key + │ └── it should revert ✅ + └── when provenance hash is correct + └── it should set the encrypted data for this batch to "" ✅ + └── it should set base URI for this batch to correct revealed URI ✅ + └── it should emit TokenURIRevealed event ✅ \ No newline at end of file diff --git a/src/test/burn-to-claim-drop-BTT/router/initialize/initialize.t.sol b/src/test/burn-to-claim-drop-BTT/router/initialize/initialize.t.sol new file mode 100644 index 000000000..cb1da0318 --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/router/initialize/initialize.t.sol @@ -0,0 +1,455 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../../utils/BaseTest.sol"; +import { BurnToClaimDropERC721 } from "contracts/prebuilts/unaudited/burn-to-claim-drop/BurnToClaimDropERC721.sol"; +import { BurnToClaimDrop721Logic } from "contracts/prebuilts/unaudited/burn-to-claim-drop/extension/BurnToClaimDrop721Logic.sol"; +import { PermissionsEnumerableImpl } from "contracts/extension/upgradeable/impl/PermissionsEnumerableImpl.sol"; + +import { ERC721AStorage } from "contracts/extension/upgradeable/init/ERC721AInit.sol"; +import { ERC2771ContextStorage } from "contracts/extension/upgradeable/init/ERC2771ContextInit.sol"; +import { ContractMetadataStorage } from "contracts/extension/upgradeable/init/ContractMetadataInit.sol"; +import { OwnableStorage } from "contracts/extension/upgradeable/init/OwnableInit.sol"; +import { PlatformFeeStorage } from "contracts/extension/upgradeable/init/PlatformFeeInit.sol"; +import { RoyaltyStorage } from "contracts/extension/upgradeable/init/RoyaltyInit.sol"; +import { PrimarySaleStorage } from "contracts/extension/upgradeable/init/PrimarySaleInit.sol"; +import { PermissionsStorage } from "contracts/extension/upgradeable/init/PermissionsInit.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract BurnToClaimDropERC721Router is BurnToClaimDropERC721 { + constructor(Extension[] memory _extensions) BurnToClaimDropERC721(_extensions) {} + + function hasRole(bytes32 role, address addr) public view returns (bool) { + return _hasRole(role, addr); + } + + function roleAdmin(bytes32 role) public view returns (bytes32) { + PermissionsStorage.Data storage data = PermissionsStorage.data(); + return data._getRoleAdmin[role]; + } + + function name() public view returns (string memory) { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + return data._name; + } + + function symbol() public view returns (string memory) { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + return data._symbol; + } + + function trustedForwarders(address[] memory _trustedForwarders) public view returns (bool) { + ERC2771ContextStorage.Data storage data = ERC2771ContextStorage.data(); + + for (uint256 i = 0; i < _trustedForwarders.length; i++) { + if (!data.trustedForwarder[_trustedForwarders[i]]) { + return false; + } + } + return true; + } + + function contractURI() public view returns (string memory) { + ContractMetadataStorage.Data storage data = ContractMetadataStorage.data(); + return data.contractURI; + } + + function owner() public view returns (address) { + OwnableStorage.Data storage data = OwnableStorage.data(); + return data._owner; + } + + function platformFeeRecipient() public view returns (address) { + PlatformFeeStorage.Data storage data = PlatformFeeStorage.data(); + return data.platformFeeRecipient; + } + + function platformFeeBps() public view returns (uint16) { + PlatformFeeStorage.Data storage data = PlatformFeeStorage.data(); + return data.platformFeeBps; + } + + function royaltyRecipient() public view returns (address) { + RoyaltyStorage.Data storage data = RoyaltyStorage.data(); + return data.royaltyRecipient; + } + + function royaltyBps() public view returns (uint16) { + RoyaltyStorage.Data storage data = RoyaltyStorage.data(); + return data.royaltyBps; + } + + function primarySaleRecipient() public view returns (address) { + PrimarySaleStorage.Data storage data = PrimarySaleStorage.data(); + return data.recipient; + } +} + +contract BurnToClaimDropERC721_Initialize is BaseTest, IExtension { + address public implementation; + address public proxy; + + event ContractURIUpdated(string prevURI, string newURI); + event OwnerUpdated(address indexed prevOwner, address indexed newOwner); + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole); + event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps); + event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps); + event PrimarySaleRecipientUpdated(address indexed recipient); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); // setup just a couple of extension/functions for testing here + implementation = address(new BurnToClaimDropERC721Router(extensions)); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + BurnToClaimDropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](2); + + // Extension: Permissions + address permissions = address(new PermissionsEnumerableImpl()); + + Extension memory extension_permissions; + extension_permissions.metadata = ExtensionMetadata({ + name: "Permissions", + metadataURI: "ipfs://Permissions", + implementation: permissions + }); + + extension_permissions.functions = new ExtensionFunction[](1); + extension_permissions.functions[0] = ExtensionFunction( + Permissions.hasRole.selector, + "hasRole(bytes32,address)" + ); + + extensions[0] = extension_permissions; + + address dropLogic = address(new BurnToClaimDrop721Logic()); + + Extension memory extension_drop; + extension_drop.metadata = ExtensionMetadata({ + name: "BurnToClaimDrop721Logic", + metadataURI: "ipfs://BurnToClaimDrop721Logic", + implementation: dropLogic + }); + + extension_drop.functions = new ExtensionFunction[](1); + extension_drop.functions[0] = ExtensionFunction(BurnToClaimDrop721Logic.tokenURI.selector, "tokenURI(uint256)"); + extensions[1] = extension_drop; + } + + function test_initialize_initializingImplementation() public { + vm.expectRevert("Initializable: contract is already initialized"); + BurnToClaimDropERC721Router(payable(implementation)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + modifier whenNotImplementation() { + _; + } + + function test_initialize_proxyAlreadyInitialized() public whenNotImplementation { + vm.expectRevert("Initializable: contract is already initialized"); + BurnToClaimDropERC721Router(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + modifier whenProxyNotInitialized() { + proxy = address(new TWProxy(implementation, "")); + _; + } + + function test_initialize() public whenNotImplementation whenProxyNotInitialized { + BurnToClaimDropERC721(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + + // check state + BurnToClaimDropERC721Router router = BurnToClaimDropERC721Router(payable(proxy)); + assertEq(router.name(), NAME); + assertEq(router.symbol(), SYMBOL); + assertTrue(router.trustedForwarders(forwarders())); + assertEq(router.platformFeeRecipient(), platformFeeRecipient); + assertEq(router.platformFeeBps(), platformFeeBps); + assertEq(router.royaltyRecipient(), royaltyRecipient); + assertEq(router.royaltyBps(), royaltyBps); + assertEq(router.primarySaleRecipient(), saleRecipient); + assertTrue(router.hasRole(bytes32(0x00), deployer)); + assertTrue(router.hasRole(keccak256("TRANSFER_ROLE"), deployer)); + assertTrue(router.hasRole(keccak256("TRANSFER_ROLE"), address(0))); + assertTrue(router.hasRole(keccak256("MINTER_ROLE"), deployer)); + assertTrue(router.hasRole(keccak256("EXTENSION_ROLE"), deployer)); + assertEq(router.roleAdmin(keccak256("EXTENSION_ROLE")), keccak256("EXTENSION_ROLE")); + + // check default extensions + Extension[] memory _extensions = router.getAllExtensions(); + assertEq(_extensions.length, 2); + } + + function test_initialize_event_ContractURIUpdated() public whenNotImplementation whenProxyNotInitialized { + vm.expectEmit(false, false, false, true); + emit ContractURIUpdated("", CONTRACT_URI); + BurnToClaimDropERC721(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_OwnerUpdated() public whenNotImplementation whenProxyNotInitialized { + vm.expectEmit(true, true, false, false); + emit OwnerUpdated(address(0), deployer); + BurnToClaimDropERC721(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleGranted_DefaultAdmin() public whenNotImplementation whenProxyNotInitialized { + bytes32 _defaultAdminRole = bytes32(0x00); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_defaultAdminRole, deployer, deployer); + BurnToClaimDropERC721(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleGranted_MinterRole() public whenNotImplementation whenProxyNotInitialized { + bytes32 _minterRole = keccak256("MINTER_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_minterRole, deployer, deployer); + BurnToClaimDropERC721(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleGranted_TransferRole() public whenNotImplementation whenProxyNotInitialized { + bytes32 _transferRole = keccak256("TRANSFER_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_transferRole, deployer, deployer); + BurnToClaimDropERC721(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleGranted_TransferRole_AddressZero() + public + whenNotImplementation + whenProxyNotInitialized + { + bytes32 _transferRole = keccak256("TRANSFER_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_transferRole, address(0), deployer); + BurnToClaimDropERC721(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleGranted_ExtensionRole() public whenNotImplementation whenProxyNotInitialized { + bytes32 _extensionRole = keccak256("EXTENSION_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_extensionRole, deployer, deployer); + BurnToClaimDropERC721(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleAdminChanged_ExtensionRole() + public + whenNotImplementation + whenProxyNotInitialized + { + bytes32 _extensionRole = keccak256("EXTENSION_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleAdminChanged(_extensionRole, bytes32(0x00), _extensionRole); + BurnToClaimDropERC721(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_PlatformFeeInfoUpdated() public whenNotImplementation whenProxyNotInitialized { + vm.prank(deployer); + vm.expectEmit(true, false, false, true); + emit PlatformFeeInfoUpdated(platformFeeRecipient, platformFeeBps); + BurnToClaimDropERC721(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_DefaultRoyalty() public whenNotImplementation whenProxyNotInitialized { + vm.prank(deployer); + vm.expectEmit(true, false, false, true); + emit DefaultRoyalty(royaltyRecipient, royaltyBps); + BurnToClaimDropERC721(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_PrimarySaleRecipientUpdated() public whenNotImplementation whenProxyNotInitialized { + vm.prank(deployer); + vm.expectEmit(true, false, false, false); + emit PrimarySaleRecipientUpdated(saleRecipient); + BurnToClaimDropERC721(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } +} diff --git a/src/test/burn-to-claim-drop-BTT/router/initialize/initialize.tree b/src/test/burn-to-claim-drop-BTT/router/initialize/initialize.tree new file mode 100644 index 000000000..295d65120 --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/router/initialize/initialize.tree @@ -0,0 +1,44 @@ +initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _royaltyRecipient, + uint128 _royaltyBps, + uint128 _platformFeeBps, + address _platformFeeRecipient +) +├── when it is the implementation contract (not proxy) +│ └── it should revert ✅ +└── when it is a proxy to the implementation + └── when it is already initialized + │ └── it should revert ✅ + └── when it is not initialized + └── it should initialize base-router with default extensions if any ✅ + └── it should set trustedForwarder mapping to true for all addresses in `_trustedForwarders` ✅ + └── it should set _name and _symbol to `_name` and `_symbol` param values respectively ✅ + └── it should set contractURI to `_contractURI` param value ✅ + └── it should emit ContractURIUpdated event ✅ + └── it should set _owner to `_defaultAdmin` param value ✅ + └── it should emit OwnerUpdated event ✅ + └── it should grant 0x00 (DEFAULT_ADMIN_ROLE) to `_defaultAdmin` address ✅ + └── it should emit RoleGranted event ✅ + └── it should grant MINTER_ROLE to `_defaultAdmin` address ✅ + └── it should emit RoleGranted event ✅ + └── it should grant TRANSFER_ROLE to `_defaultAdmin` address ✅ + └── it should emit RoleGranted event ✅ + └── it should grant TRANSFER_ROLE to address(0) ✅ + └── it should emit RoleGranted event ✅ + └── it should grant EXTENSION_ROLE to `_defaultAdmin` address ✅ + └── it should emit RoleGranted event ✅ + └── it should set EXTENSION_ROLE as role admin for EXTENSION_ROLE ✅ + └── it should emit RoleAdminChanged event ✅ + └── it should set platformFeeRecipient and platformFeeBps as `_platformFeeRecipient` and `_platformFeeBps` respectively ✅ + └── it should emit PlatformFeeInfoUpdated event ✅ + └── it should set royaltyRecipient and royaltyBps as `_royaltyRecipient` and `_royaltyBps` respectively ✅ + └── it should emit DefaultRoyalty event ✅ + └── it should set primary sale recipient as `_saleRecipient` param value ✅ + └── it should emit PrimarySaleRecipientUpdated event ✅ + diff --git a/src/test/burn-to-claim-drop-BTT/router/other-functions/other.t.sol b/src/test/burn-to-claim-drop-BTT/router/other-functions/other.t.sol new file mode 100644 index 000000000..d9cdb0656 --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/router/other-functions/other.t.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../../utils/BaseTest.sol"; +import { BurnToClaimDropERC721 } from "contracts/prebuilts/unaudited/burn-to-claim-drop/BurnToClaimDropERC721.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract BurnToClaimDropERC721Router is BurnToClaimDropERC721 { + constructor(Extension[] memory _extensions) BurnToClaimDropERC721(_extensions) {} + + function isAuthorizedCallToUpgrade() public view returns (bool) { + return _isAuthorizedCallToUpgrade(); + } +} + +contract BurnToClaimDropERC721_OtherFunctions is BaseTest, IExtension { + address public implementation; + address public proxy; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + Extension[] memory extensions; + implementation = address(new BurnToClaimDropERC721Router(extensions)); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + BurnToClaimDropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + } + + function test_contractType() public { + BurnToClaimDropERC721Router router = BurnToClaimDropERC721Router(payable(proxy)); + assertEq(router.contractType(), bytes32("BurnToClaimDropERC721")); + } + + function test_contractVersion() public { + BurnToClaimDropERC721Router router = BurnToClaimDropERC721Router(payable(proxy)); + assertEq(router.contractVersion(), uint8(5)); + } + + function test_isAuthorizedCallToUpgrade_notExtensionRole() public { + BurnToClaimDropERC721Router router = BurnToClaimDropERC721Router(payable(proxy)); + assertFalse(router.isAuthorizedCallToUpgrade()); + } + + modifier whenExtensionRole() { + _; + } + + function test_isAuthorizedCallToUpgrade() public whenExtensionRole { + BurnToClaimDropERC721Router router = BurnToClaimDropERC721Router(payable(proxy)); + + vm.prank(deployer); + assertTrue(router.isAuthorizedCallToUpgrade()); + } +} diff --git a/src/test/burn-to-claim-drop-BTT/router/other-functions/other.tree b/src/test/burn-to-claim-drop-BTT/router/other-functions/other.tree new file mode 100644 index 000000000..98e8df4fe --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/router/other-functions/other.tree @@ -0,0 +1,12 @@ +contractType() +├── it should return bytes32("BurnToClaimDropERC721") ✅ + +contractVersion() +├── it should return uint8(5) ✅ + +_isAuthorizedCallToUpgrade() +├── when the caller doesn't have EXTENSION_ROLE +│ └── it should revert ✅ +└── when the caller has EXTENSION_ROLE + └── it should return true ✅ + diff --git a/src/test/burn-to-claim-drop/BurnToClaimDropERC721.t.sol b/src/test/burn-to-claim-drop/BurnToClaimDropERC721.t.sol new file mode 100644 index 000000000..b3994d496 --- /dev/null +++ b/src/test/burn-to-claim-drop/BurnToClaimDropERC721.t.sol @@ -0,0 +1,1883 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../utils/BaseTest.sol"; +import { BurnToClaimDropERC721 } from "contracts/prebuilts/unaudited/burn-to-claim-drop/BurnToClaimDropERC721.sol"; +import { BurnToClaimDrop721Logic, ERC721AUpgradeable, DelayedReveal, LazyMint, Drop, BurnToClaim, PrimarySale, PlatformFee } from "contracts/prebuilts/unaudited/burn-to-claim-drop/extension/BurnToClaimDrop721Logic.sol"; +import { PermissionsEnumerableImpl } from "contracts/extension/upgradeable/impl/PermissionsEnumerableImpl.sol"; +import { Royalty } from "contracts/extension/upgradeable/Royalty.sol"; +import { BatchMintMetadata } from "contracts/extension/upgradeable/BatchMintMetadata.sol"; +import { IBurnToClaim } from "contracts/extension/interface/IBurnToClaim.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "erc721a-upgradeable/contracts/IERC721AUpgradeable.sol"; + +contract BurnToClaimDropERC721Test is BaseTest, IExtension { + using Strings for uint256; + using Strings for address; + + event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI); + event TokenURIRevealed(uint256 indexed index, string revealedURI); + + BurnToClaimDrop721Logic public drop; + + bytes private emptyEncodedBytes = abi.encode("", ""); + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address dropImpl = address(new BurnToClaimDropERC721(extensions)); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + drop = BurnToClaimDrop721Logic( + payable( + address( + new TWProxy( + dropImpl, + abi.encodeCall( + BurnToClaimDropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ) + ); + + erc20.mint(deployer, 1_000 ether); + vm.deal(deployer, 1_000 ether); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](2); + + // Extension: Permissions + address permissions = address(new PermissionsEnumerableImpl()); + + Extension memory extension_permissions; + extension_permissions.metadata = ExtensionMetadata({ + name: "Permissions", + metadataURI: "ipfs://Permissions", + implementation: permissions + }); + + extension_permissions.functions = new ExtensionFunction[](7); + extension_permissions.functions[0] = ExtensionFunction( + Permissions.hasRole.selector, + "hasRole(bytes32,address)" + ); + extension_permissions.functions[1] = ExtensionFunction( + Permissions.hasRoleWithSwitch.selector, + "hasRoleWithSwitch(bytes32,address)" + ); + extension_permissions.functions[2] = ExtensionFunction( + Permissions.grantRole.selector, + "grantRole(bytes32,address)" + ); + extension_permissions.functions[3] = ExtensionFunction( + Permissions.renounceRole.selector, + "renounceRole(bytes32,address)" + ); + extension_permissions.functions[4] = ExtensionFunction( + Permissions.revokeRole.selector, + "revokeRole(bytes32,address)" + ); + extension_permissions.functions[5] = ExtensionFunction( + PermissionsEnumerable.getRoleMemberCount.selector, + "getRoleMemberCount(bytes32)" + ); + extension_permissions.functions[6] = ExtensionFunction( + PermissionsEnumerable.getRoleMember.selector, + "getRoleMember(bytes32,uint256)" + ); + + extensions[0] = extension_permissions; + + address dropLogic = address(new BurnToClaimDrop721Logic()); + + Extension memory extension_drop; + extension_drop.metadata = ExtensionMetadata({ + name: "BurnToClaimDrop721Logic", + metadataURI: "ipfs://BurnToClaimDrop721Logic", + implementation: dropLogic + }); + + extension_drop.functions = new ExtensionFunction[](32); + extension_drop.functions[0] = ExtensionFunction(BurnToClaimDrop721Logic.tokenURI.selector, "tokenURI(uint256)"); + extension_drop.functions[1] = ExtensionFunction( + BurnToClaimDrop721Logic.lazyMint.selector, + "lazyMint(uint256,string,bytes)" + ); + extension_drop.functions[2] = ExtensionFunction( + BurnToClaimDrop721Logic.reveal.selector, + "reveal(uint256,bytes)" + ); + extension_drop.functions[3] = ExtensionFunction(Drop.claimCondition.selector, "claimCondition()"); + extension_drop.functions[4] = ExtensionFunction( + BatchMintMetadata.getBaseURICount.selector, + "getBaseURICount()" + ); + extension_drop.functions[5] = ExtensionFunction( + Drop.claim.selector, + "claim(address,uint256,address,uint256,(bytes32[],uint256,uint256,address),bytes)" + ); + extension_drop.functions[6] = ExtensionFunction( + Drop.setClaimConditions.selector, + "setClaimConditions((uint256,uint256,uint256,uint256,bytes32,uint256,address,string)[],bool)" + ); + extension_drop.functions[7] = ExtensionFunction( + Drop.getActiveClaimConditionId.selector, + "getActiveClaimConditionId()" + ); + extension_drop.functions[8] = ExtensionFunction( + Drop.getClaimConditionById.selector, + "getClaimConditionById(uint256)" + ); + extension_drop.functions[9] = ExtensionFunction( + Drop.getSupplyClaimedByWallet.selector, + "getSupplyClaimedByWallet(uint256,address)" + ); + extension_drop.functions[10] = ExtensionFunction(BurnToClaimDrop721Logic.totalMinted.selector, "totalMinted()"); + extension_drop.functions[11] = ExtensionFunction( + BurnToClaimDrop721Logic.nextTokenIdToMint.selector, + "nextTokenIdToMint()" + ); + extension_drop.functions[12] = ExtensionFunction( + IERC721Upgradeable.setApprovalForAll.selector, + "setApprovalForAll(address,bool)" + ); + extension_drop.functions[13] = ExtensionFunction( + IERC721Upgradeable.approve.selector, + "approve(address,uint256)" + ); + extension_drop.functions[14] = ExtensionFunction( + IERC721Upgradeable.transferFrom.selector, + "transferFrom(address,address,uint256)" + ); + extension_drop.functions[15] = ExtensionFunction(ERC721AUpgradeable.balanceOf.selector, "balanceOf(address)"); + extension_drop.functions[16] = ExtensionFunction( + DelayedReveal.encryptDecrypt.selector, + "encryptDecrypt(bytes,bytes)" + ); + extension_drop.functions[17] = ExtensionFunction( + BurnToClaimDrop721Logic.supportsInterface.selector, + "supportsInterface(bytes4)" + ); + extension_drop.functions[18] = ExtensionFunction(Royalty.royaltyInfo.selector, "royaltyInfo(uint256,uint256)"); + extension_drop.functions[19] = ExtensionFunction( + Royalty.getRoyaltyInfoForToken.selector, + "getRoyaltyInfoForToken(uint256)" + ); + extension_drop.functions[20] = ExtensionFunction( + Royalty.getDefaultRoyaltyInfo.selector, + "getDefaultRoyaltyInfo()" + ); + extension_drop.functions[21] = ExtensionFunction( + Royalty.setDefaultRoyaltyInfo.selector, + "setDefaultRoyaltyInfo(address,uint256)" + ); + extension_drop.functions[22] = ExtensionFunction( + Royalty.setRoyaltyInfoForToken.selector, + "setRoyaltyInfoForToken(uint256,address,uint256)" + ); + extension_drop.functions[23] = ExtensionFunction(IERC721.ownerOf.selector, "ownerOf(uint256)"); + extension_drop.functions[24] = ExtensionFunction(IERC1155.balanceOf.selector, "balanceOf(address,uint256)"); + extension_drop.functions[25] = ExtensionFunction( + BurnToClaim.setBurnToClaimInfo.selector, + "setBurnToClaimInfo((address,uint8,uint256,uint256,address))" + ); + extension_drop.functions[26] = ExtensionFunction( + BurnToClaim.getBurnToClaimInfo.selector, + "getBurnToClaimInfo()" + ); + extension_drop.functions[27] = ExtensionFunction( + BurnToClaim.verifyBurnToClaim.selector, + "verifyBurnToClaim(address,uint256,uint256)" + ); + extension_drop.functions[28] = ExtensionFunction( + BurnToClaimDrop721Logic.burnAndClaim.selector, + "burnAndClaim(uint256,uint256)" + ); + extension_drop.functions[29] = ExtensionFunction( + BurnToClaimDrop721Logic.nextTokenIdToClaim.selector, + "nextTokenIdToClaim()" + ); + extension_drop.functions[30] = ExtensionFunction( + PrimarySale.setPrimarySaleRecipient.selector, + "setPrimarySaleRecipient(address)" + ); + extension_drop.functions[31] = ExtensionFunction( + PlatformFee.setPlatformFeeInfo.selector, + "setPlatformFeeInfo(address,uint256)" + ); + + extensions[1] = extension_drop; + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: misc. + //////////////////////////////////////////////////////////////*/ + + /** + * note: Tests whether contract reverts when a non-holder renounces a role. + */ + function test_revert_nonHolder_renounceRole() public { + address caller = address(0x123); + bytes32 role = keccak256("MINTER_ROLE"); + + vm.prank(caller); + vm.expectRevert( + abi.encodePacked( + "Permissions: account ", + Strings.toHexString(uint160(caller), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ); + + Permissions(address(drop)).renounceRole(role, caller); + } + + /** + * note: Tests whether contract reverts when a role admin revokes a role for a non-holder. + */ + function test_revert_revokeRoleForNonHolder() public { + address target = address(0x123); + bytes32 role = keccak256("MINTER_ROLE"); + + vm.prank(deployer); + vm.expectRevert( + abi.encodePacked( + "Permissions: account ", + Strings.toHexString(uint160(target), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ); + + Permissions(address(drop)).revokeRole(role, target); + } + + /** + * @dev Tests whether contract reverts when a role is granted to an existent role holder. + */ + function test_revert_grant_role_to_account_with_role() public { + bytes32 role = keccak256("ABC_ROLE"); + address receiver = getActor(0); + + vm.startPrank(deployer); + + Permissions(address(drop)).grantRole(role, receiver); + + vm.expectRevert("Can only grant to non holders"); + Permissions(address(drop)).grantRole(role, receiver); + + vm.stopPrank(); + } + + /** + * @dev Tests contract state for Transfer role. + */ + function test_state_grant_transferRole() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + + // check if admin and address(0) have transfer role in the beginning + bool checkAddressZero = Permissions(address(drop)).hasRole(role, address(0)); + bool checkAdmin = Permissions(address(drop)).hasRole(role, deployer); + assertTrue(checkAddressZero); + assertTrue(checkAdmin); + + // check if transfer role can be granted to a non-holder + address receiver = getActor(0); + vm.startPrank(deployer); + Permissions(address(drop)).grantRole(role, receiver); + + // expect revert when granting to a holder + vm.expectRevert("Can only grant to non holders"); + Permissions(address(drop)).grantRole(role, receiver); + + // check if receiver has transfer role + bool checkReceiver = Permissions(address(drop)).hasRole(role, receiver); + assertTrue(checkReceiver); + + // check if role is correctly revoked + Permissions(address(drop)).revokeRole(role, receiver); + checkReceiver = Permissions(address(drop)).hasRole(role, receiver); + assertFalse(checkReceiver); + Permissions(address(drop)).revokeRole(role, address(0)); + checkAddressZero = Permissions(address(drop)).hasRole(role, address(0)); + assertFalse(checkAddressZero); + + vm.stopPrank(); + } + + /** + * @dev Tests contract state for Transfer role. + */ + function test_state_getRoleMember_transferRole() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + + uint256 roleMemberCount = PermissionsEnumerable(address(drop)).getRoleMemberCount(role); + assertEq(roleMemberCount, 2); + + address roleMember = PermissionsEnumerable(address(drop)).getRoleMember(role, 1); + assertEq(roleMember, address(0)); + + vm.startPrank(deployer); + Permissions(address(drop)).grantRole(role, address(2)); + Permissions(address(drop)).grantRole(role, address(3)); + Permissions(address(drop)).grantRole(role, address(4)); + + roleMemberCount = PermissionsEnumerable(address(drop)).getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(address(drop)).getRoleMember(role, i)); + } + console.log(""); + + Permissions(address(drop)).revokeRole(role, address(2)); + roleMemberCount = PermissionsEnumerable(address(drop)).getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(address(drop)).getRoleMember(role, i)); + } + console.log(""); + + Permissions(address(drop)).revokeRole(role, address(0)); + roleMemberCount = PermissionsEnumerable(address(drop)).getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(address(drop)).getRoleMember(role, i)); + } + console.log(""); + + Permissions(address(drop)).grantRole(role, address(5)); + roleMemberCount = PermissionsEnumerable(address(drop)).getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(address(drop)).getRoleMember(role, i)); + } + console.log(""); + + Permissions(address(drop)).grantRole(role, address(0)); + roleMemberCount = PermissionsEnumerable(address(drop)).getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(address(drop)).getRoleMember(role, i)); + } + console.log(""); + + Permissions(address(drop)).grantRole(role, address(6)); + roleMemberCount = PermissionsEnumerable(address(drop)).getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(address(drop)).getRoleMember(role, i)); + } + console.log(""); + } + + /** + * note: Testing transfer of tokens when transfer-role is restricted + */ + function test_claim_transferRole() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + drop.claim(receiver, 1, address(0), 0, alp, ""); + + // revoke transfer role from address(0) + vm.prank(deployer); + Permissions(address(drop)).revokeRole(keccak256("TRANSFER_ROLE"), address(0)); + vm.startPrank(receiver); + vm.expectRevert("!Transfer-Role"); + drop.transferFrom(receiver, address(123), 0); + } + + /** + * @dev Tests whether role member count is incremented correctly. + */ + function test_member_count_incremented_properly_when_role_granted() public { + bytes32 role = keccak256("ABC_ROLE"); + address receiver = getActor(0); + + vm.startPrank(deployer); + uint256 roleMemberCount = PermissionsEnumerable(address(drop)).getRoleMemberCount(role); + + assertEq(roleMemberCount, 0); + + Permissions(address(drop)).grantRole(role, receiver); + + assertEq(PermissionsEnumerable(address(drop)).getRoleMemberCount(role), 1); + + vm.stopPrank(); + } + + function test_claimCondition_with_startTimestamp() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].startTimestamp = 100; + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.warp(99); + vm.prank(getActor(5), getActor(5)); + vm.expectRevert("!CONDITION."); + drop.claim(receiver, 1, address(0), 0, alp, ""); + + vm.warp(100); + vm.prank(getActor(4), getActor(4)); + drop.claim(receiver, 1, address(0), 0, alp, ""); + } + + /*/////////////////////////////////////////////////////////////// + Primary sale and Platform fee tests + //////////////////////////////////////////////////////////////*/ + + /// note: Test whether transaction reverts when adding address(0) as primary sale recipient at deploy time + function test_revert_deploy_emptyPrimarySaleRecipient() public { + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address dropImpl = address(new BurnToClaimDropERC721(extensions)); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + vm.expectRevert("Invalid recipient"); + drop = BurnToClaimDrop721Logic( + payable( + address( + new TWProxy( + dropImpl, + abi.encodeCall( + BurnToClaimDropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + address(0), + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ) + ); + } + + /// note: Test whether transaction reverts when adding address(0) as primary sale recipient + function test_revert_emptyPrimarySaleRecipient() public { + vm.prank(deployer); + vm.expectRevert("Invalid recipient"); + drop.setPrimarySaleRecipient(address(0)); + } + + /// note: Test whether transaction reverts when adding address(0) as platform fee recipient at deploy time + function test_revert_deploy_emptyPlatformFeeRecipient() public { + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address dropImpl = address(new BurnToClaimDropERC721(extensions)); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + vm.expectRevert("Invalid recipient"); + drop = BurnToClaimDrop721Logic( + payable( + address( + new TWProxy( + dropImpl, + abi.encodeCall( + BurnToClaimDropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + address(0) + ) + ) + ) + ) + ) + ); + } + + /// note: Test whether transaction reverts when adding address(0) as platform fee recipient + function test_revert_emptyPlatformFeeRecipient() public { + vm.prank(deployer); + vm.expectRevert("Invalid recipient"); + drop.setPlatformFeeInfo(address(0), 100); + } + + /*/////////////////////////////////////////////////////////////// + Lazy Mint Tests + //////////////////////////////////////////////////////////////*/ + + /* + * note: Testing state changes; lazy mint a batch of tokens with no encrypted base URI. + */ + function test_state_lazyMint_noEncryptedURI() public { + uint256 amountToLazyMint = 100; + string memory baseURI = "ipfs://"; + + uint256 nextTokenIdToMintBefore = drop.nextTokenIdToMint(); + + vm.startPrank(deployer); + uint256 batchId = drop.lazyMint(amountToLazyMint, baseURI, emptyEncodedBytes); + + assertEq(nextTokenIdToMintBefore + amountToLazyMint, drop.nextTokenIdToMint()); + assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); + + for (uint256 i = 0; i < amountToLazyMint; i += 1) { + string memory uri = drop.tokenURI(i); + console.log(uri); + assertEq(uri, string(abi.encodePacked(baseURI, i.toString()))); + } + + vm.stopPrank(); + } + + /* + * note: Testing state changes; lazy mint a batch of tokens with encrypted base URI. + */ + function test_state_lazyMint_withEncryptedURI() public { + uint256 amountToLazyMint = 100; + string memory baseURI = "ipfs://"; + bytes memory encryptedBaseURI = "encryptedBaseURI://"; + bytes32 provenanceHash = bytes32("whatever"); + + uint256 nextTokenIdToMintBefore = drop.nextTokenIdToMint(); + + vm.startPrank(deployer); + uint256 batchId = drop.lazyMint(amountToLazyMint, baseURI, abi.encode(encryptedBaseURI, provenanceHash)); + + assertEq(nextTokenIdToMintBefore + amountToLazyMint, drop.nextTokenIdToMint()); + assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); + + for (uint256 i = 0; i < amountToLazyMint; i += 1) { + string memory uri = drop.tokenURI(i); + console.log(uri); + assertEq(uri, string(abi.encodePacked(baseURI, "0"))); + } + + vm.stopPrank(); + } + + /** + * note: Testing revert condition; an address without MINTER_ROLE calls lazyMint function. + */ + function test_revert_lazyMint_MINTER_ROLE() public { + vm.expectRevert("Not authorized"); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + } + + /* + * note: Testing revert condition; calling tokenURI for invalid batch id. + */ + function test_revert_lazyMint_URIForNonLazyMintedToken() public { + vm.startPrank(deployer); + + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + vm.expectRevert("Invalid tokenId"); + drop.tokenURI(100); + + vm.stopPrank(); + } + + /** + * note: Testing event emission; tokens lazy minted. + */ + function test_event_lazyMint_TokensLazyMinted() public { + vm.startPrank(deployer); + + vm.expectEmit(true, false, false, true); + emit TokensLazyMinted(0, 99, "ipfs://", emptyEncodedBytes); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + vm.stopPrank(); + } + + /* + * note: Fuzz testing state changes; lazy mint a batch of tokens with no encrypted base URI. + */ + function test_fuzz_lazyMint_noEncryptedURI(uint256 x) public { + vm.assume(x > 0); + + uint256 amountToLazyMint = x; + string memory baseURI = "ipfs://"; + + uint256 nextTokenIdToMintBefore = drop.nextTokenIdToMint(); + + vm.startPrank(deployer); + uint256 batchId = drop.lazyMint(amountToLazyMint, baseURI, emptyEncodedBytes); + + assertEq(nextTokenIdToMintBefore + amountToLazyMint, drop.nextTokenIdToMint()); + assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); + + string memory uri = drop.tokenURI(0); + assertEq(uri, string(abi.encodePacked(baseURI, uint256(0).toString()))); + + uri = drop.tokenURI(x - 1); + assertEq(uri, string(abi.encodePacked(baseURI, uint256(x - 1).toString()))); + + /** + * note: this loop takes too long to run with fuzz tests. + */ + // for(uint256 i = 0; i < amountToLazyMint; i += 1) { + // string memory uri = drop.tokenURI(i); + // console.log(uri); + // assertEq(uri, string(abi.encodePacked(baseURI, i.toString()))); + // } + + vm.stopPrank(); + } + + /* + * note: Fuzz testing state changes; lazy mint a batch of tokens with encrypted base URI. + */ + function test_fuzz_lazyMint_withEncryptedURI(uint256 x) public { + vm.assume(x > 0); + + uint256 amountToLazyMint = x; + string memory baseURI = "ipfs://"; + bytes memory encryptedBaseURI = "encryptedBaseURI://"; + bytes32 provenanceHash = bytes32("whatever"); + + uint256 nextTokenIdToMintBefore = drop.nextTokenIdToMint(); + + vm.startPrank(deployer); + uint256 batchId = drop.lazyMint(amountToLazyMint, baseURI, abi.encode(encryptedBaseURI, provenanceHash)); + + assertEq(nextTokenIdToMintBefore + amountToLazyMint, drop.nextTokenIdToMint()); + assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); + + string memory uri = drop.tokenURI(0); + assertEq(uri, string(abi.encodePacked(baseURI, "0"))); + + uri = drop.tokenURI(x - 1); + assertEq(uri, string(abi.encodePacked(baseURI, "0"))); + + /** + * note: this loop takes too long to run with fuzz tests. + */ + // for(uint256 i = 0; i < amountToLazyMint; i += 1) { + // string memory uri = drop.tokenURI(1); + // assertEq(uri, string(abi.encodePacked(baseURI, "0"))); + // } + + vm.stopPrank(); + } + + /* + * note: Fuzz testing; a batch of tokens, and nextTokenIdToMint + */ + function test_fuzz_lazyMint_batchMintAndNextTokenIdToMint(uint256 x) public { + vm.assume(x > 0); + vm.startPrank(deployer); + + if (x == 0) { + vm.expectRevert("Zero amount"); + } + drop.lazyMint(x, "ipfs://", emptyEncodedBytes); + + uint256 slot = stdstore.target(address(drop)).sig("nextTokenIdToMint()").find(); + bytes32 loc = bytes32(slot); + uint256 nextTokenIdToMint = uint256(vm.load(address(drop), loc)); + + assertEq(nextTokenIdToMint, x); + vm.stopPrank(); + } + + /*/////////////////////////////////////////////////////////////// + Delayed Reveal Tests + //////////////////////////////////////////////////////////////*/ + + /* + * note: Testing state changes; URI revealed for a batch of tokens. + */ + function test_state_reveal() public { + vm.startPrank(deployer); + + bytes memory key = "key"; + uint256 amountToLazyMint = 100; + bytes memory secretURI = "ipfs://"; + string memory placeholderURI = "abcd://"; + bytes memory encryptedURI = drop.encryptDecrypt(secretURI, key); + bytes32 provenanceHash = keccak256(abi.encodePacked(secretURI, key, block.chainid)); + + drop.lazyMint(amountToLazyMint, placeholderURI, abi.encode(encryptedURI, provenanceHash)); + + for (uint256 i = 0; i < amountToLazyMint; i += 1) { + string memory uri = drop.tokenURI(i); + assertEq(uri, string(abi.encodePacked(placeholderURI, "0"))); + } + + string memory revealedURI = drop.reveal(0, key); + assertEq(revealedURI, string(secretURI)); + + for (uint256 i = 0; i < amountToLazyMint; i += 1) { + string memory uri = drop.tokenURI(i); + assertEq(uri, string(abi.encodePacked(secretURI, i.toString()))); + } + + vm.stopPrank(); + } + + /** + * note: Testing revert condition; an address without MINTER_ROLE calls reveal function. + */ + function test_revert_reveal_MINTER_ROLE() public { + bytes memory key = "key"; + bytes memory encryptedURI = drop.encryptDecrypt("ipfs://", key); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); + vm.prank(deployer); + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + + vm.prank(deployer); + drop.reveal(0, "key"); + + vm.expectRevert("not minter."); + drop.reveal(0, "key"); + } + + /* + * note: Testing revert condition; trying to reveal URI for non-existent batch. + */ + function test_revert_reveal_revealingNonExistentBatch() public { + vm.startPrank(deployer); + + bytes memory key = "key"; + bytes memory encryptedURI = drop.encryptDecrypt("ipfs://", key); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + drop.reveal(0, "key"); + + console.log(drop.getBaseURICount()); + + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + vm.expectRevert("Invalid index"); + drop.reveal(2, "key"); + + vm.stopPrank(); + } + + /* + * note: Testing revert condition; already revealed URI. + */ + function test_revert_delayedReveal_alreadyRevealed() public { + vm.startPrank(deployer); + + bytes memory key = "key"; + bytes memory encryptedURI = drop.encryptDecrypt("ipfs://", key); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + drop.reveal(0, "key"); + + vm.expectRevert("Nothing to reveal"); + drop.reveal(0, "key"); + + vm.stopPrank(); + } + + /* + * note: Testing state changes; revealing URI with an incorrect key. + */ + function test_revert_reveal_incorrectKey() public { + vm.startPrank(deployer); + + bytes memory key = "key"; + bytes memory encryptedURI = drop.encryptDecrypt("ipfs://", key); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + + vm.expectRevert(); + string memory revealedURI = drop.reveal(0, "keyy"); + + vm.stopPrank(); + } + + /** + * note: Testing event emission; TokenURIRevealed. + */ + function test_event_reveal_TokenURIRevealed() public { + vm.startPrank(deployer); + + bytes memory key = "key"; + bytes memory encryptedURI = drop.encryptDecrypt("ipfs://", key); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + + vm.expectEmit(true, false, false, true); + emit TokenURIRevealed(0, "ipfs://"); + drop.reveal(0, "key"); + + vm.stopPrank(); + } + + /*/////////////////////////////////////////////////////////////// + Claim Tests + //////////////////////////////////////////////////////////////*/ + + /** + * note: Testing revert condition; not enough minted tokens. + */ + function test_revert_claimCondition_notEnoughMintedTokens() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 200; + + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.expectRevert("!Tokens"); + vm.prank(getActor(6), getActor(6)); + drop.claim(receiver, 101, address(0), 0, alp, ""); + } + + /** + * note: Testing revert condition; exceed max claimable supply. + */ + function test_revert_claimCondition_exceedMaxClaimableSupply() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 200; + + vm.prank(deployer); + drop.lazyMint(200, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + drop.claim(receiver, 100, address(0), 0, alp, ""); + + vm.expectRevert("!MaxSupply"); + vm.prank(getActor(6), getActor(6)); + drop.claim(receiver, 1, address(0), 0, alp, ""); + } + + /** + * note: Testing quantity limit restriction when no allowlist present. + */ + function test_fuzz_claim_noAllowlist(uint256 x) public { + vm.assume(x != 0); + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = x; + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + bytes memory errorQty = "!Qty"; + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(errorQty); + drop.claim(receiver, 0, address(0), 0, alp, ""); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(errorQty); + drop.claim(receiver, 101, address(0), 0, alp, ""); + + vm.prank(deployer); + drop.setClaimConditions(conditions, true); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(errorQty); + drop.claim(receiver, 101, address(0), 0, alp, ""); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price set to 0 + */ + function test_state_claim_allowlisted_SetQuantityZeroPrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "0"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 0; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, 100, address(erc20), 0, alp, ""); // claims for free, because allowlist price is 0 + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), 100); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price set to non-zero value + */ + function test_state_claim_allowlisted_SetQuantityPrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "5"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 5; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.prank(receiver, receiver); + vm.expectRevert("!PriceOrCurrency"); + drop.claim(receiver, 100, address(erc20), 0, alp, ""); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(drop), 10000); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, 100, address(erc20), 5, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), 100); + assertEq(erc20.balanceOf(receiver), 10000 - 500); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price not set; should default to general price and currency + */ + function test_state_claim_allowlisted_SetQuantityDefaultPrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = Strings.toString(type(uint256).max); // this implies that general price is applicable + inputs[4] = "0x0000000000000000000000000000000000000000"; + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = type(uint256).max; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(drop), 10000); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, 100, address(erc20), 10, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), 100); + assertEq(erc20.balanceOf(receiver), 10000 - 1000); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to 0 => should default to general limit + * - allowlist price set to some value different than general price + */ + function test_state_claim_allowlisted_DefaultQuantitySomePrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "0"; // this implies that general limit is applicable + inputs[3] = "5"; + inputs[4] = "0x0000000000000000000000000000000000000000"; // general currency will be applicable + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 0; + alp.pricePerToken = 5; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(drop), 10000); + + bytes memory errorQty = "!Qty"; + vm.prank(receiver, receiver); + vm.expectRevert(errorQty); + drop.claim(receiver, 100, address(erc20), 5, alp, ""); // trying to claim more than general limit + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, 10, address(erc20), 5, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), 10); + assertEq(erc20.balanceOf(receiver), 10000 - 50); + } + + function test_fuzz_claim_merkleProof(uint256 x) public { + vm.assume(x > 10 && x < 500); + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = Strings.toString(x); + inputs[3] = "0"; + inputs[4] = "0x0000000000000000000000000000000000000000"; + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = x; + alp.pricePerToken = 0; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + + // bytes32[] memory proofs = new bytes32[](0); + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = x; + conditions[0].quantityLimitPerWallet = 1; + conditions[0].merkleRoot = root; + + vm.prank(deployer); + drop.lazyMint(2 * x, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, x - 5, address(0), 0, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), x - 5); + + bytes memory errorQty = "!Qty"; + + vm.prank(receiver, receiver); + vm.expectRevert(errorQty); + drop.claim(receiver, 6, address(0), 0, alp, ""); + + vm.prank(receiver, receiver); + drop.claim(receiver, 5, address(0), 0, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), x); + + vm.prank(receiver, receiver); + vm.expectRevert(errorQty); + drop.claim(receiver, 5, address(0), 0, alp, ""); // quantity limit already claimed + } + + /** + * note: Testing state changes; reset eligibility of claim conditions and claiming again for same condition id. + */ + function test_state_claimCondition_resetEligibility() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + drop.claim(receiver, 100, address(0), 0, alp, ""); + + bytes memory errorQty = "!Qty"; + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(errorQty); + drop.claim(receiver, 100, address(0), 0, alp, ""); + + vm.prank(deployer); + drop.setClaimConditions(conditions, true); + + vm.prank(getActor(5), getActor(5)); + drop.claim(receiver, 100, address(0), 0, alp, ""); + } + + /*/////////////////////////////////////////////////////////////// + setClaimConditions + //////////////////////////////////////////////////////////////*/ + + function test_claimCondition_startIdAndCount() public { + vm.startPrank(deployer); + + uint256 currentStartId = 0; + uint256 count = 0; + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](2); + conditions[0].startTimestamp = 0; + conditions[0].maxClaimableSupply = 10; + conditions[1].startTimestamp = 1; + conditions[1].maxClaimableSupply = 10; + + drop.setClaimConditions(conditions, false); + (currentStartId, count) = drop.claimCondition(); + assertEq(currentStartId, 0); + assertEq(count, 2); + + drop.setClaimConditions(conditions, false); + (currentStartId, count) = drop.claimCondition(); + assertEq(currentStartId, 0); + assertEq(count, 2); + + drop.setClaimConditions(conditions, true); + (currentStartId, count) = drop.claimCondition(); + assertEq(currentStartId, 2); + assertEq(count, 2); + + drop.setClaimConditions(conditions, true); + (currentStartId, count) = drop.claimCondition(); + assertEq(currentStartId, 4); + assertEq(count, 2); + } + + function test_claimCondition_startPhase() public { + vm.startPrank(deployer); + + uint256 activeConditionId = 0; + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](3); + conditions[0].startTimestamp = 10; + conditions[0].maxClaimableSupply = 11; + conditions[0].quantityLimitPerWallet = 12; + conditions[1].startTimestamp = 20; + conditions[1].maxClaimableSupply = 21; + conditions[1].quantityLimitPerWallet = 22; + conditions[2].startTimestamp = 30; + conditions[2].maxClaimableSupply = 31; + conditions[2].quantityLimitPerWallet = 32; + drop.setClaimConditions(conditions, false); + + vm.expectRevert("!CONDITION."); + drop.getActiveClaimConditionId(); + + vm.warp(10); + activeConditionId = drop.getActiveClaimConditionId(); + assertEq(activeConditionId, 0); + assertEq(drop.getClaimConditionById(activeConditionId).startTimestamp, 10); + assertEq(drop.getClaimConditionById(activeConditionId).maxClaimableSupply, 11); + assertEq(drop.getClaimConditionById(activeConditionId).quantityLimitPerWallet, 12); + + vm.warp(20); + activeConditionId = drop.getActiveClaimConditionId(); + assertEq(activeConditionId, 1); + assertEq(drop.getClaimConditionById(activeConditionId).startTimestamp, 20); + assertEq(drop.getClaimConditionById(activeConditionId).maxClaimableSupply, 21); + assertEq(drop.getClaimConditionById(activeConditionId).quantityLimitPerWallet, 22); + + vm.warp(30); + activeConditionId = drop.getActiveClaimConditionId(); + assertEq(activeConditionId, 2); + assertEq(drop.getClaimConditionById(activeConditionId).startTimestamp, 30); + assertEq(drop.getClaimConditionById(activeConditionId).maxClaimableSupply, 31); + assertEq(drop.getClaimConditionById(activeConditionId).quantityLimitPerWallet, 32); + + vm.warp(40); + assertEq(drop.getActiveClaimConditionId(), 2); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function test_delayedReveal_withNewLazyMintedEmptyBatch() public { + vm.startPrank(deployer); + + bytes memory encryptedURI = drop.encryptDecrypt("ipfs://", "key"); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", "key", block.chainid)); + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + drop.reveal(0, "key"); + + string memory uri = drop.tokenURI(1); + assertEq(uri, string(abi.encodePacked("ipfs://", "1"))); + + bytes memory newEncryptedURI = drop.encryptDecrypt("ipfs://secret", "key"); + vm.expectRevert("0 amt"); + drop.lazyMint(0, "", abi.encode(newEncryptedURI, provenanceHash)); + + vm.stopPrank(); + } + + /*/////////////////////////////////////////////////////////////// + Burn To Claim + //////////////////////////////////////////////////////////////*/ + + function test_state_burnAndClaim_1155Origin_zeroMintPrice() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc1155); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC1155; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 0; + burnToClaimInfo.currency = address(erc20); + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // check details correctly saved + BurnToClaimDrop721Logic.BurnToClaimInfo memory savedInfo = drop.getBurnToClaimInfo(); + assertEq(savedInfo.originContractAddress, burnToClaimInfo.originContractAddress); + assertTrue(savedInfo.tokenType == burnToClaimInfo.tokenType); + assertEq(savedInfo.tokenId, burnToClaimInfo.tokenId); + assertEq(savedInfo.mintPriceForNewToken, burnToClaimInfo.mintPriceForNewToken); + assertEq(savedInfo.currency, burnToClaimInfo.currency); + + // mint some erc1155 to a claimer + address claimer = getActor(0); + erc1155.mint(claimer, 0, 10); + assertEq(erc1155.balanceOf(claimer, 0), 10); + vm.prank(claimer); + erc1155.setApprovalForAll(address(drop), true); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // burn and claim + vm.prank(claimer); + drop.burnAndClaim(0, 10); + + // check state + assertEq(erc1155.balanceOf(claimer, 0), 0); + assertEq(drop.balanceOf(claimer), 10); + assertEq(drop.nextTokenIdToClaim(), 10); + } + + function test_state_burnAndClaim_1155Origin_nonZeroMintPrice() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc1155); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC1155; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 1; + burnToClaimInfo.currency = address(erc20); + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // check details correctly saved + BurnToClaimDrop721Logic.BurnToClaimInfo memory savedInfo = drop.getBurnToClaimInfo(); + assertEq(savedInfo.originContractAddress, burnToClaimInfo.originContractAddress); + assertTrue(savedInfo.tokenType == burnToClaimInfo.tokenType); + assertEq(savedInfo.tokenId, burnToClaimInfo.tokenId); + assertEq(savedInfo.mintPriceForNewToken, burnToClaimInfo.mintPriceForNewToken); + assertEq(savedInfo.currency, burnToClaimInfo.currency); + + // mint some erc1155 to a claimer + address claimer = getActor(0); + erc1155.mint(claimer, 0, 10); + assertEq(erc1155.balanceOf(claimer, 0), 10); + vm.prank(claimer); + erc1155.setApprovalForAll(address(drop), true); + + // mint erc20 to claimer, to pay claim price + erc20.mint(claimer, 100); + vm.prank(claimer); + erc20.approve(address(drop), type(uint256).max); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // burn and claim + vm.prank(claimer); + drop.burnAndClaim(0, 10); + + // check state + assertEq(erc1155.balanceOf(claimer, 0), 0); + assertEq(erc20.balanceOf(claimer), 90); + assertEq(erc20.balanceOf(saleRecipient), 10); + assertEq(drop.balanceOf(claimer), 10); + assertEq(drop.nextTokenIdToClaim(), 10); + } + + function test_state_burnAndClaim_1155Origin_nonZeroMintPrice_nativeToken() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc1155); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC1155; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 1; + burnToClaimInfo.currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // check details correctly saved + BurnToClaimDrop721Logic.BurnToClaimInfo memory savedInfo = drop.getBurnToClaimInfo(); + assertEq(savedInfo.originContractAddress, burnToClaimInfo.originContractAddress); + assertTrue(savedInfo.tokenType == burnToClaimInfo.tokenType); + assertEq(savedInfo.tokenId, burnToClaimInfo.tokenId); + assertEq(savedInfo.mintPriceForNewToken, burnToClaimInfo.mintPriceForNewToken); + assertEq(savedInfo.currency, burnToClaimInfo.currency); + + // mint some erc1155 to a claimer + address claimer = getActor(0); + erc1155.mint(claimer, 0, 10); + assertEq(erc1155.balanceOf(claimer, 0), 10); + vm.prank(claimer); + erc1155.setApprovalForAll(address(drop), true); + + // deal ether to claimer, to pay claim price + vm.deal(claimer, 100); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // burn and claim + vm.prank(claimer); + drop.burnAndClaim{ value: 10 }(0, 10); + + // check state + assertEq(erc1155.balanceOf(claimer, 0), 0); + assertEq(claimer.balance, 90); + assertEq(saleRecipient.balance, 10); + assertEq(drop.balanceOf(claimer), 10); + assertEq(drop.nextTokenIdToClaim(), 10); + } + + function test_state_burnAndClaim_721Origin_zeroMintPrice() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc721); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC721; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 0; + burnToClaimInfo.currency = address(erc20); + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // check details correctly saved + BurnToClaimDrop721Logic.BurnToClaimInfo memory savedInfo = drop.getBurnToClaimInfo(); + assertEq(savedInfo.originContractAddress, burnToClaimInfo.originContractAddress); + assertTrue(savedInfo.tokenType == burnToClaimInfo.tokenType); + assertEq(savedInfo.tokenId, burnToClaimInfo.tokenId); + assertEq(savedInfo.mintPriceForNewToken, burnToClaimInfo.mintPriceForNewToken); + assertEq(savedInfo.currency, burnToClaimInfo.currency); + + // mint some erc721 to a claimer + address claimer = getActor(0); + erc721.mint(claimer, 10); + assertEq(erc721.balanceOf(claimer), 10); + vm.prank(claimer); + erc721.setApprovalForAll(address(drop), true); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // burn and claim + vm.prank(claimer); + drop.burnAndClaim(0, 1); + + // check state + assertEq(erc721.balanceOf(claimer), 9); + assertEq(drop.balanceOf(claimer), 1); + assertEq(drop.nextTokenIdToClaim(), 1); + + vm.expectRevert("ERC721: invalid token ID"); // because the token doesn't exist anymore + erc721.ownerOf(0); + } + + function test_state_burnAndClaim_721Origin_nonZeroMintPrice() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc721); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC721; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 1; + burnToClaimInfo.currency = address(erc20); + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // check details correctly saved + BurnToClaimDrop721Logic.BurnToClaimInfo memory savedInfo = drop.getBurnToClaimInfo(); + assertEq(savedInfo.originContractAddress, burnToClaimInfo.originContractAddress); + assertTrue(savedInfo.tokenType == burnToClaimInfo.tokenType); + assertEq(savedInfo.tokenId, burnToClaimInfo.tokenId); + assertEq(savedInfo.mintPriceForNewToken, burnToClaimInfo.mintPriceForNewToken); + assertEq(savedInfo.currency, burnToClaimInfo.currency); + + // mint some erc721 to a claimer + address claimer = getActor(0); + erc721.mint(claimer, 10); + assertEq(erc721.balanceOf(claimer), 10); + vm.prank(claimer); + erc721.setApprovalForAll(address(drop), true); + + // mint erc20 to claimer, to pay claim price + erc20.mint(claimer, 100); + vm.prank(claimer); + erc20.approve(address(drop), type(uint256).max); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // burn and claim + vm.prank(claimer); + drop.burnAndClaim(0, 1); + + // check state + assertEq(erc721.balanceOf(claimer), 9); + assertEq(drop.balanceOf(claimer), 1); + assertEq(drop.nextTokenIdToClaim(), 1); + assertEq(erc20.balanceOf(claimer), 99); + assertEq(erc20.balanceOf(saleRecipient), 1); + + vm.expectRevert("ERC721: invalid token ID"); // because the token doesn't exist anymore + erc721.ownerOf(0); + } + + function test_state_burnAndClaim_721Origin_nonZeroMintPrice_nativeToken() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc721); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC721; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 1; + burnToClaimInfo.currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // check details correctly saved + BurnToClaimDrop721Logic.BurnToClaimInfo memory savedInfo = drop.getBurnToClaimInfo(); + assertEq(savedInfo.originContractAddress, burnToClaimInfo.originContractAddress); + assertTrue(savedInfo.tokenType == burnToClaimInfo.tokenType); + assertEq(savedInfo.tokenId, burnToClaimInfo.tokenId); + assertEq(savedInfo.mintPriceForNewToken, burnToClaimInfo.mintPriceForNewToken); + assertEq(savedInfo.currency, burnToClaimInfo.currency); + + // mint some erc721 to a claimer + address claimer = getActor(0); + erc721.mint(claimer, 10); + assertEq(erc721.balanceOf(claimer), 10); + vm.prank(claimer); + erc721.setApprovalForAll(address(drop), true); + + // deal ether to claimer, to pay claim price + vm.deal(claimer, 100); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // burn and claim + vm.prank(claimer); + drop.burnAndClaim{ value: 1 }(0, 1); + + // check state + assertEq(erc721.balanceOf(claimer), 9); + assertEq(drop.balanceOf(claimer), 1); + assertEq(drop.nextTokenIdToClaim(), 1); + assertEq(claimer.balance, 99); + assertEq(saleRecipient.balance, 1); + + vm.expectRevert("ERC721: invalid token ID"); // because the token doesn't exist anymore + erc721.ownerOf(0); + } + + function test_revert_burnAndClaim_originNotSet() public { + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // burn and claim + vm.expectRevert(); + drop.burnAndClaim(0, 1); + } + + function test_revert_burnAndClaim_noLazyMintedTokens() public { + // burn and claim + vm.expectRevert("!Tokens"); + drop.burnAndClaim(0, 1); + } + + function test_revert_burnAndClaim_invalidTokenId() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc1155); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC1155; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 0; + burnToClaimInfo.currency = address(erc20); + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // mint some erc1155 to a claimer + address claimer = getActor(0); + erc1155.mint(claimer, 0, 10); + assertEq(erc1155.balanceOf(claimer, 0), 10); + vm.prank(claimer); + erc1155.setApprovalForAll(address(drop), true); + + // burn and claim + vm.prank(claimer); + vm.expectRevert("Invalid token Id"); + drop.burnAndClaim(1, 1); + } + + function test_revert_burnAndClaim_notEnoughBalance() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc1155); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC1155; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 0; + burnToClaimInfo.currency = address(erc20); + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // mint some erc1155 to a claimer + address claimer = getActor(0); + erc1155.mint(claimer, 0, 10); + assertEq(erc1155.balanceOf(claimer, 0), 10); + vm.prank(claimer); + erc1155.setApprovalForAll(address(drop), true); + + // burn and claim + vm.prank(claimer); + vm.expectRevert("!Balance"); + drop.burnAndClaim(0, 11); + } + + function test_revert_burnAndClaim_notOwnerOfToken() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc721); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC721; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 1; + burnToClaimInfo.currency = address(erc20); + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // mint some erc721 to a claimer + address claimer = getActor(0); + erc721.mint(claimer, 10); + assertEq(erc721.balanceOf(claimer), 10); + vm.prank(claimer); + erc721.setApprovalForAll(address(drop), true); + + // mint erc721 to another address + erc721.mint(address(0x567), 5); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // burn and claim + vm.prank(claimer); + vm.expectRevert("!Owner"); + drop.burnAndClaim(11, 1); + } + + /*/////////////////////////////////////////////////////////////// + Extension Role and Upgradeability + //////////////////////////////////////////////////////////////*/ + + // function test_addExtension() public { + // address permissionsNew = address(new PermissionsEnumerableImpl()); + + // Extension memory extension_permissions_new; + // extension_permissions_new.metadata = ExtensionMetadata({ + // name: "PermissionsNew", + // metadataURI: "ipfs://PermissionsNew", + // implementation: permissionsNew + // }); + + // extension_permissions_new.functions = new ExtensionFunction[](4); + // extension_permissions_new.functions[0] = ExtensionFunction( + // Permissions.hasRole.selector, + // "hasRole(bytes32,address)" + // ); + // extension_permissions_new.functions[1] = ExtensionFunction( + // Permissions.hasRoleWithSwitch.selector, + // "hasRoleWithSwitch(bytes32,address)" + // ); + // extension_permissions_new.functions[2] = ExtensionFunction( + // Permissions.grantRole.selector, + // "grantRole(bytes32,address)" + // ); + // extension_permissions_new.functions[3] = ExtensionFunction( + // PermissionsEnumerable.getRoleMemberCount.selector, + // "getRoleMemberCount(bytes32)" + // ); + + // // cast drop to router type + // BurnToClaimDropERC721 dropRouter = BurnToClaimDropERC721(payable(address(drop))); + + // vm.prank(deployer); + // dropRouter.addExtension(extension_permissions_new); + + // // assertEq( + // // dropRouter.getExtensionForFunction(PermissionsEnumerable.getRoleMemberCount.selector).name, + // // "PermissionsNew" + // // ); + + // // assertEq( + // // dropRouter.getExtensionForFunction(PermissionsEnumerable.getRoleMemberCount.selector).implementation, + // // permissionsNew + // // ); + // } + + function test_revert_addExtension_NotAuthorized() public { + Extension memory extension_permissions_new; + + // cast drop to router type + BurnToClaimDropERC721 dropRouter = BurnToClaimDropERC721(payable(address(drop))); + + vm.prank(address(0x123)); + vm.expectRevert("ExtensionManager: unauthorized."); + dropRouter.addExtension(extension_permissions_new); + } + + function test_revert_addExtension_deployerRenounceExtensionRole() public { + Extension memory extension_permissions_new; + + // cast drop to router type + BurnToClaimDropERC721 dropRouter = BurnToClaimDropERC721(payable(address(drop))); + + vm.prank(deployer); + Permissions(address(drop)).renounceRole(keccak256("EXTENSION_ROLE"), deployer); + + vm.prank(deployer); + vm.expectRevert("ExtensionManager: unauthorized."); + dropRouter.addExtension(extension_permissions_new); + + vm.startPrank(deployer); + vm.expectRevert( + abi.encodePacked( + "Permissions: account ", + Strings.toHexString(uint160(deployer), 20), + " is missing role ", + Strings.toHexString(uint256(keccak256("EXTENSION_ROLE")), 32) + ) + ); + Permissions(address(drop)).grantRole(keccak256("EXTENSION_ROLE"), address(0x12345)); + vm.stopPrank(); + } +} diff --git a/src/test/drop/DropERC1155.t.sol b/src/test/drop/DropERC1155.t.sol index 66d5e1664..840f78cf6 100644 --- a/src/test/drop/DropERC1155.t.sol +++ b/src/test/drop/DropERC1155.t.sol @@ -1,49 +1,956 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; -import { DropERC1155 } from "contracts/drop/DropERC1155.sol"; +import { DropERC1155, BatchMintMetadata, Drop1155, LazyMint, IPermissions, ILazyMint } from "contracts/prebuilts/drop/DropERC1155.sol"; // Test imports + import "../utils/BaseTest.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; contract DropERC1155Test is BaseTest { + using Strings for uint256; + using Strings for address; + + event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI); + event TokenURIRevealed(uint256 indexed index, string revealedURI); + event MaxTotalSupplyUpdated(uint256 tokenId, uint256 maxTotalSupply); + DropERC1155 public drop; + bytes private emptyEncodedBytes = abi.encode("", ""); + + using stdStorage for StdStorage; + function setUp() public override { super.setUp(); drop = DropERC1155(getContract("DropERC1155")); + + erc20.mint(deployer, 1_000 ether); + vm.deal(deployer, 1_000 ether); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: misc. + //////////////////////////////////////////////////////////////*/ + + /** + * note: Tests whether contract reverts when a non-holder renounces a role. + */ + function test_revert_nonHolder_renounceRole() public { + address caller = address(0x123); + bytes32 role = keccak256("MINTER_ROLE"); + + vm.prank(caller); + vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsUnauthorizedAccount.selector, caller, role)); + + drop.renounceRole(role, caller); + } + + /** + * note: Tests whether contract reverts when a role admin revokes a role for a non-holder. + */ + function test_revert_revokeRoleForNonHolder() public { + address target = address(0x123); + bytes32 role = keccak256("MINTER_ROLE"); + + vm.prank(deployer); + vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsUnauthorizedAccount.selector, target, role)); + + drop.revokeRole(role, target); + } + + /** + * @dev Tests whether contract reverts when a role is granted to an existent role holder. + */ + function test_revert_grant_role_to_account_with_role() public { + bytes32 role = keccak256("ABC_ROLE"); + address receiver = getActor(0); + + vm.startPrank(deployer); + + drop.grantRole(role, receiver); + + vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsAlreadyGranted.selector, receiver, role)); + drop.grantRole(role, receiver); + + vm.stopPrank(); + } + + /** + * @dev Tests contract state for Transfer role. + */ + function test_state_grant_transferRole() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + + // check if admin and address(0) have transfer role in the beginning + bool checkAddressZero = drop.hasRole(role, address(0)); + bool checkAdmin = drop.hasRole(role, deployer); + assertTrue(checkAddressZero); + assertTrue(checkAdmin); + + // check if transfer role can be granted to a non-holder + address receiver = getActor(0); + vm.startPrank(deployer); + drop.grantRole(role, receiver); + + // expect revert when granting to a holder + vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsAlreadyGranted.selector, receiver, role)); + drop.grantRole(role, receiver); + + // check if receiver has transfer role + bool checkReceiver = drop.hasRole(role, receiver); + assertTrue(checkReceiver); + + // check if role is correctly revoked + drop.revokeRole(role, receiver); + checkReceiver = drop.hasRole(role, receiver); + assertFalse(checkReceiver); + drop.revokeRole(role, address(0)); + checkAddressZero = drop.hasRole(role, address(0)); + assertFalse(checkAddressZero); + + vm.stopPrank(); + } + + /** + * @dev Tests contract state for Transfer role. + */ + function test_state_getRoleMember_transferRole() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + + uint256 roleMemberCount = drop.getRoleMemberCount(role); + assertEq(roleMemberCount, 2); + + address roleMember = drop.getRoleMember(role, 1); + assertEq(roleMember, address(0)); + + vm.startPrank(deployer); + drop.grantRole(role, address(2)); + drop.grantRole(role, address(3)); + drop.grantRole(role, address(4)); + + roleMemberCount = drop.getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(drop.getRoleMember(role, i)); + } + console.log(""); + + drop.revokeRole(role, address(2)); + roleMemberCount = drop.getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(drop.getRoleMember(role, i)); + } + console.log(""); + + drop.revokeRole(role, address(0)); + roleMemberCount = drop.getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(drop.getRoleMember(role, i)); + } + console.log(""); + + drop.grantRole(role, address(5)); + roleMemberCount = drop.getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(drop.getRoleMember(role, i)); + } + console.log(""); + + drop.grantRole(role, address(0)); + roleMemberCount = drop.getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(drop.getRoleMember(role, i)); + } + console.log(""); + + drop.grantRole(role, address(6)); + roleMemberCount = drop.getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(drop.getRoleMember(role, i)); + } + console.log(""); + } + + /** + * note: Testing transfer of tokens when transfer-role is restricted + */ + function test_claim_transferRole() public { + vm.warp(1); + + uint256 _tokenId = 0; + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + DropERC1155.AllowlistProof memory alp; + alp.proof = proofs; + + DropERC1155.ClaimCondition[] memory conditions = new DropERC1155.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(_tokenId, conditions, false); + + vm.prank(getActor(5), getActor(5)); + drop.claim(receiver, _tokenId, 1, address(0), 0, alp, ""); + + // revoke transfer role from address(0) + vm.prank(deployer); + drop.revokeRole(keccak256("TRANSFER_ROLE"), address(0)); + vm.startPrank(receiver); + vm.expectRevert("restricted to TRANSFER_ROLE holders."); + drop.safeTransferFrom(receiver, address(123), 0, 0, ""); + } + + /** + * @dev Tests whether role member count is incremented correctly. + */ + function test_member_count_incremented_properly_when_role_granted() public { + bytes32 role = keccak256("ABC_ROLE"); + address receiver = getActor(0); + + vm.startPrank(deployer); + uint256 roleMemberCount = drop.getRoleMemberCount(role); + + assertEq(roleMemberCount, 0); + + drop.grantRole(role, receiver); + + assertEq(drop.getRoleMemberCount(role), 1); + + vm.stopPrank(); + } + + function test_claimCondition_with_startTimestamp() public { + vm.warp(1); + + uint256 _tokenId = 0; + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + DropERC1155.AllowlistProof memory alp; + alp.proof = proofs; + + DropERC1155.ClaimCondition[] memory conditions = new DropERC1155.ClaimCondition[](1); + conditions[0].startTimestamp = 100; + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + vm.prank(deployer); + drop.setClaimConditions(_tokenId, conditions, false); + + vm.warp(99); + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(abi.encodeWithSelector(Drop1155.DropNoActiveCondition.selector)); + drop.claim(receiver, _tokenId, 1, address(0), 0, alp, ""); + + vm.warp(100); + vm.prank(getActor(4), getActor(4)); + drop.claim(receiver, _tokenId, 1, address(0), 0, alp, ""); } /*/////////////////////////////////////////////////////////////// - Miscellaneous + Lazy Mint Tests //////////////////////////////////////////////////////////////*/ - function test_revert_claim_claimQty() public { + /* + * note: Testing state changes; lazy mint a batch of tokens with no encrypted base URI. + */ + function test_state_lazyMint_noEncryptedURI() public { + uint256 amountToLazyMint = 100; + string memory baseURI = "ipfs://"; + + uint256 nextTokenIdToMintBefore = drop.nextTokenIdToMint(); + + vm.startPrank(deployer); + uint256 batchId = drop.lazyMint(amountToLazyMint, baseURI, emptyEncodedBytes); + + assertEq(nextTokenIdToMintBefore + amountToLazyMint, drop.nextTokenIdToMint()); + assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); + + for (uint256 i = 0; i < amountToLazyMint; i += 1) { + string memory uri = drop.uri(i); + console.log(uri); + assertEq(uri, string(abi.encodePacked(baseURI, i.toString()))); + } + + vm.stopPrank(); + } + + /** + * note: Testing revert condition; an address without MINTER_ROLE calls lazyMint function. + */ + function test_revert_lazyMint_MINTER_ROLE() public { + vm.expectRevert(abi.encodeWithSelector(LazyMint.LazyMintUnauthorized.selector)); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + } + + /* + * note: Testing revert condition; calling tokenURI for invalid batch id. + */ + function test_revert_lazyMint_URIForNonLazyMintedToken() public { + vm.startPrank(deployer); + + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + vm.expectRevert(abi.encodeWithSelector(BatchMintMetadata.BatchMintInvalidTokenId.selector, 100)); + drop.uri(100); + + vm.stopPrank(); + } + + /** + * note: Testing event emission; tokens lazy minted. + */ + function test_event_lazyMint_TokensLazyMinted() public { + vm.startPrank(deployer); + + vm.expectEmit(true, false, false, true); + emit TokensLazyMinted(0, 99, "ipfs://", emptyEncodedBytes); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + vm.stopPrank(); + } + + /* + * note: Fuzz testing state changes; lazy mint a batch of tokens with no encrypted base URI. + */ + function test_fuzz_lazyMint_noEncryptedURI(uint256 x) public { + vm.assume(x > 0); + + uint256 amountToLazyMint = x; + string memory baseURI = "ipfs://"; + + uint256 nextTokenIdToMintBefore = drop.nextTokenIdToMint(); + + vm.startPrank(deployer); + uint256 batchId = drop.lazyMint(amountToLazyMint, baseURI, emptyEncodedBytes); + + assertEq(nextTokenIdToMintBefore + amountToLazyMint, drop.nextTokenIdToMint()); + assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); + + string memory uri = drop.uri(0); + assertEq(uri, string(abi.encodePacked(baseURI, uint256(0).toString()))); + + uri = drop.uri(x - 1); + assertEq(uri, string(abi.encodePacked(baseURI, uint256(x - 1).toString()))); + + /** + * note: this loop takes too long to run with fuzz tests. + */ + // for(uint256 i = 0; i < amountToLazyMint; i += 1) { + // string memory uri = drop.uri(i); + // console.log(uri); + // assertEq(uri, string(abi.encodePacked(baseURI, i.toString()))); + // } + + vm.stopPrank(); + } + + /* + * note: Fuzz testing; a batch of tokens, and nextTokenIdToMint + */ + function test_fuzz_lazyMint_batchMintAndNextTokenIdToMint(uint256 x) public { + vm.assume(x > 0); + vm.startPrank(deployer); + + if (x == 0) { + vm.expectRevert("Zero amount"); + } + drop.lazyMint(x, "ipfs://", emptyEncodedBytes); + + uint256 slot = stdstore.target(address(drop)).sig("nextTokenIdToMint()").find(); + bytes32 loc = bytes32(slot); + uint256 nextTokenIdToMint = uint256(vm.load(address(drop), loc)); + + assertEq(nextTokenIdToMint, x); + vm.stopPrank(); + } + + /*/////////////////////////////////////////////////////////////// + Claim Tests + //////////////////////////////////////////////////////////////*/ + + /** + * note: Testing revert condition; not enough minted tokens. + */ + function test_revert_claimCondition_notEnoughMintedTokens() public { + vm.warp(1); + + uint256 _tokenId = 0; + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + DropERC1155.AllowlistProof memory alp; + alp.proof = proofs; + + DropERC1155.ClaimCondition[] memory conditions = new DropERC1155.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 200; + + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(_tokenId, conditions, false); + + vm.expectRevert(abi.encodeWithSelector(Drop1155.DropNoActiveCondition.selector)); + vm.prank(getActor(6), getActor(6)); + drop.claim(receiver, 100, 101, address(0), 0, alp, ""); + } + + /** + * note: Testing revert condition; exceed max claimable supply. + */ + function test_revert_claimCondition_exceedMaxClaimableSupply() public { vm.warp(1); + uint256 _tokenId = 0; address receiver = getActor(0); bytes32[] memory proofs = new bytes32[](0); + DropERC1155.AllowlistProof memory alp; + alp.proof = proofs; + + DropERC1155.ClaimCondition[] memory conditions = new DropERC1155.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 200; + + vm.prank(deployer); + drop.lazyMint(200, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(_tokenId, conditions, false); + + vm.prank(getActor(5), getActor(5)); + drop.claim(receiver, _tokenId, 100, address(0), 0, alp, ""); + + vm.expectRevert(abi.encodeWithSelector(Drop1155.DropClaimExceedMaxSupply.selector, 100, 101)); + vm.prank(getActor(6), getActor(6)); + drop.claim(receiver, _tokenId, 1, address(0), 0, alp, ""); + } + + /** + * note: Testing quantity limit restriction when no allowlist present. + */ + function test_fuzz_claim_noAllowlist(uint256 x) public { + vm.assume(x != 0); + vm.warp(1); + + uint256 _tokenId = 0; + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + DropERC1155.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = x; + DropERC1155.ClaimCondition[] memory conditions = new DropERC1155.ClaimCondition[](1); conditions[0].maxClaimableSupply = 500; - conditions[0].quantityLimitPerTransaction = 100; - conditions[0].waitTimeInSecondsBetweenClaims = type(uint256).max; + conditions[0].quantityLimitPerWallet = 100; vm.prank(deployer); - drop.lazyMint(500, "ipfs://"); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); vm.prank(deployer); - drop.setClaimConditions(0, conditions, false); + drop.setClaimConditions(_tokenId, conditions, false); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(abi.encodeWithSelector(Drop1155.DropClaimExceedLimit.selector, 100, 0)); + drop.claim(receiver, _tokenId, 0, address(0), 0, alp, ""); vm.prank(getActor(5), getActor(5)); - vm.expectRevert("invalid quantity claimed."); - drop.claim(receiver, 0, 200, address(0), 0, proofs, 2); + vm.expectRevert(abi.encodeWithSelector(Drop1155.DropClaimExceedLimit.selector, 100, 101)); + drop.claim(receiver, _tokenId, 101, address(0), 0, alp, ""); vm.prank(deployer); - drop.setClaimConditions(0, conditions, true); + drop.setClaimConditions(_tokenId, conditions, true); vm.prank(getActor(5), getActor(5)); - vm.expectRevert("invalid quantity claimed."); - drop.claim(receiver, 0, 200, address(0), 0, proofs, 1); + vm.expectRevert(abi.encodeWithSelector(Drop1155.DropClaimExceedLimit.selector, 100, 101)); + drop.claim(receiver, _tokenId, 101, address(0), 0, alp, ""); } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price set to 0 + */ + function test_state_claim_allowlisted_SetQuantityZeroPrice() public { + uint256 _tokenId = 0; + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "0"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + DropERC1155.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 0; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + DropERC1155.ClaimCondition[] memory conditions = new DropERC1155.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(_tokenId, conditions, false); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, _tokenId, 100, address(erc20), 0, alp, ""); // claims for free, because allowlist price is 0 + assertEq(drop.getSupplyClaimedByWallet(_tokenId, drop.getActiveClaimConditionId(_tokenId), receiver), 100); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price set to non-zero value + */ + function test_state_claim_allowlisted_SetQuantityPrice() public { + uint256 _tokenId = 0; + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "5"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + DropERC1155.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 5; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + DropERC1155.ClaimCondition[] memory conditions = new DropERC1155.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(_tokenId, conditions, false); + + vm.prank(receiver, receiver); + vm.expectRevert( + abi.encodeWithSelector(Drop1155.DropClaimInvalidTokenPrice.selector, address(erc20), 0, address(erc20), 5) + ); + drop.claim(receiver, _tokenId, 100, address(erc20), 0, alp, ""); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(drop), 10000); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, _tokenId, 100, address(erc20), 5, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(_tokenId, drop.getActiveClaimConditionId(_tokenId), receiver), 100); + assertEq(erc20.balanceOf(receiver), 10000 - 500); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price not set; should default to general price and currency + */ + function test_state_claim_allowlisted_SetQuantityDefaultPrice() public { + uint256 _tokenId = 0; + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = Strings.toString(type(uint256).max); // this implies that general price is applicable + inputs[4] = "0x0000000000000000000000000000000000000000"; + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + DropERC1155.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = type(uint256).max; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + DropERC1155.ClaimCondition[] memory conditions = new DropERC1155.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(_tokenId, conditions, false); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(drop), 10000); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, _tokenId, 100, address(erc20), 10, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(_tokenId, drop.getActiveClaimConditionId(_tokenId), receiver), 100); + assertEq(erc20.balanceOf(receiver), 10000 - 1000); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to 0 => should default to general limit + * - allowlist price set to some value different than general price + */ + function test_state_claim_allowlisted_DefaultQuantitySomePrice() public { + uint256 _tokenId = 0; + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "0"; // this implies that general limit is applicable + inputs[3] = "5"; + inputs[4] = "0x0000000000000000000000000000000000000000"; // general currency will be applicable + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + DropERC1155.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 0; + alp.pricePerToken = 5; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + DropERC1155.ClaimCondition[] memory conditions = new DropERC1155.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(_tokenId, conditions, false); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(drop), 10000); + + vm.prank(receiver, receiver); + vm.expectRevert(abi.encodeWithSelector(Drop1155.DropClaimExceedLimit.selector, 10, 100)); + drop.claim(receiver, _tokenId, 100, address(erc20), 5, alp, ""); // trying to claim more than general limit + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, _tokenId, 10, address(erc20), 5, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(_tokenId, drop.getActiveClaimConditionId(_tokenId), receiver), 10); + assertEq(erc20.balanceOf(receiver), 10000 - 50); + } + + function test_fuzz_claim_merkleProof(uint256 x) public { + vm.assume(x > 10 && x < 500); + uint256 _tokenId = 0; + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = Strings.toString(x); + inputs[3] = "0"; + inputs[4] = "0x0000000000000000000000000000000000000000"; + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + DropERC1155.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = x; + alp.pricePerToken = 0; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + + // bytes32[] memory proofs = new bytes32[](0); + + DropERC1155.ClaimCondition[] memory conditions = new DropERC1155.ClaimCondition[](1); + conditions[0].maxClaimableSupply = x; + conditions[0].quantityLimitPerWallet = 1; + conditions[0].merkleRoot = root; + + vm.prank(deployer); + drop.lazyMint(2 * x, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(_tokenId, conditions, false); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, _tokenId, x - 5, address(0), 0, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(_tokenId, drop.getActiveClaimConditionId(_tokenId), receiver), x - 5); + + vm.prank(receiver, receiver); + vm.expectRevert(abi.encodeWithSelector(Drop1155.DropClaimExceedLimit.selector, x, x + 1)); + drop.claim(receiver, _tokenId, 6, address(0), 0, alp, ""); + + vm.prank(receiver, receiver); + drop.claim(receiver, _tokenId, 5, address(0), 0, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(_tokenId, drop.getActiveClaimConditionId(_tokenId), receiver), x); + + vm.prank(receiver, receiver); + vm.expectRevert(abi.encodeWithSelector(Drop1155.DropClaimExceedLimit.selector, x, x + 5)); + drop.claim(receiver, _tokenId, 5, address(0), 0, alp, ""); // quantity limit already claimed + } + + /** + * note: Testing state changes; reset eligibility of claim conditions and claiming again for same condition id. + */ + function test_state_claimCondition_resetEligibility() public { + vm.warp(1); + + uint256 _tokenId = 0; + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + DropERC1155.AllowlistProof memory alp; + alp.proof = proofs; + + DropERC1155.ClaimCondition[] memory conditions = new DropERC1155.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + + vm.prank(deployer); + drop.setClaimConditions(_tokenId, conditions, false); + + vm.prank(getActor(5), getActor(5)); + drop.claim(receiver, _tokenId, 100, address(0), 0, alp, ""); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(abi.encodeWithSelector(Drop1155.DropClaimExceedLimit.selector, 100, 200)); + drop.claim(receiver, _tokenId, 100, address(0), 0, alp, ""); + + vm.prank(deployer); + drop.setClaimConditions(_tokenId, conditions, true); + + vm.prank(getActor(5), getActor(5)); + drop.claim(receiver, _tokenId, 100, address(0), 0, alp, ""); + } + + /*/////////////////////////////////////////////////////////////// + setClaimConditions + //////////////////////////////////////////////////////////////*/ + + function test_claimCondition_startIdAndCount() public { + vm.startPrank(deployer); + + uint256 _tokenId = 0; + uint256 currentStartId = 0; + uint256 count = 0; + + DropERC1155.ClaimCondition[] memory conditions = new DropERC1155.ClaimCondition[](2); + conditions[0].startTimestamp = 0; + conditions[0].maxClaimableSupply = 10; + conditions[1].startTimestamp = 1; + conditions[1].maxClaimableSupply = 10; + + drop.setClaimConditions(_tokenId, conditions, false); + (currentStartId, count) = drop.claimCondition(_tokenId); + assertEq(currentStartId, 0); + assertEq(count, 2); + + drop.setClaimConditions(_tokenId, conditions, false); + (currentStartId, count) = drop.claimCondition(_tokenId); + assertEq(currentStartId, 0); + assertEq(count, 2); + + drop.setClaimConditions(_tokenId, conditions, true); + (currentStartId, count) = drop.claimCondition(_tokenId); + assertEq(currentStartId, 2); + assertEq(count, 2); + + drop.setClaimConditions(_tokenId, conditions, true); + (currentStartId, count) = drop.claimCondition(_tokenId); + assertEq(currentStartId, 4); + assertEq(count, 2); + } + + function test_claimCondition_startPhase() public { + vm.startPrank(deployer); + + uint256 _tokenId = 0; + uint256 activeConditionId = 0; + + DropERC1155.ClaimCondition[] memory conditions = new DropERC1155.ClaimCondition[](3); + conditions[0].startTimestamp = 10; + conditions[0].maxClaimableSupply = 11; + conditions[0].quantityLimitPerWallet = 12; + conditions[1].startTimestamp = 20; + conditions[1].maxClaimableSupply = 21; + conditions[1].quantityLimitPerWallet = 22; + conditions[2].startTimestamp = 30; + conditions[2].maxClaimableSupply = 31; + conditions[2].quantityLimitPerWallet = 32; + drop.setClaimConditions(_tokenId, conditions, false); + + vm.expectRevert(abi.encodeWithSelector(Drop1155.DropNoActiveCondition.selector)); + drop.getActiveClaimConditionId(_tokenId); + + vm.warp(10); + activeConditionId = drop.getActiveClaimConditionId(_tokenId); + assertEq(activeConditionId, 0); + assertEq(drop.getClaimConditionById(_tokenId, activeConditionId).startTimestamp, 10); + assertEq(drop.getClaimConditionById(_tokenId, activeConditionId).maxClaimableSupply, 11); + assertEq(drop.getClaimConditionById(_tokenId, activeConditionId).quantityLimitPerWallet, 12); + + vm.warp(20); + activeConditionId = drop.getActiveClaimConditionId(_tokenId); + assertEq(activeConditionId, 1); + assertEq(drop.getClaimConditionById(_tokenId, activeConditionId).startTimestamp, 20); + assertEq(drop.getClaimConditionById(_tokenId, activeConditionId).maxClaimableSupply, 21); + assertEq(drop.getClaimConditionById(_tokenId, activeConditionId).quantityLimitPerWallet, 22); + + vm.warp(30); + activeConditionId = drop.getActiveClaimConditionId(_tokenId); + assertEq(activeConditionId, 2); + assertEq(drop.getClaimConditionById(_tokenId, activeConditionId).startTimestamp, 30); + assertEq(drop.getClaimConditionById(_tokenId, activeConditionId).maxClaimableSupply, 31); + assertEq(drop.getClaimConditionById(_tokenId, activeConditionId).quantityLimitPerWallet, 32); + + vm.warp(40); + assertEq(drop.getActiveClaimConditionId(_tokenId), 2); + } + + /*/////////////////////////////////////////////////////////////// + Unit Test: updateBatchBaseURI + //////////////////////////////////////////////////////////////*/ + + function test_state_updateBatchBaseURI() public { + string memory initURI = "ipfs://init"; + string memory newURI = "ipfs://new"; + + vm.startPrank(deployer); + drop.lazyMint(100, initURI, ""); + + string memory initTokenURI = drop.uri(0); + + assertEq(initTokenURI, string(abi.encodePacked(initURI, "0"))); + + drop.updateBatchBaseURI(0, newURI); + + string memory newTokenURI = drop.uri(0); + + assertEq(newTokenURI, string(abi.encodePacked(newURI, "0"))); + } + + /*/////////////////////////////////////////////////////////////// + Unit Test: freezeBatchBaseURI + //////////////////////////////////////////////////////////////*/ + + function test_state_freezeBatchBaseURI() public { + string memory initURI = "ipfs://init"; + + vm.startPrank(deployer); + drop.lazyMint(100, initURI, ""); + + string memory initTokenURI = drop.uri(0); + + assertEq(initTokenURI, string(abi.encodePacked(initURI, "0"))); + + drop.freezeBatchBaseURI(0); + + assertEq(drop.batchFrozen(100), true); + } + + /*/////////////////////////////////////////////////////////////// + Unit Test: setMaxTotalSupply + //////////////////////////////////////////////////////////////*/ + + function test_state_setMaxTotalSupply() public { + vm.startPrank(deployer); + drop.setMaxTotalSupply(1, 100); + + assertEq(drop.maxTotalSupply(1), 100); + } + + function test_event_setMaxTotalSupply_MaxTotalSupplyUpdated() public { + vm.startPrank(deployer); + + vm.expectEmit(true, false, false, true); + emit MaxTotalSupplyUpdated(1, 100); + drop.setMaxTotalSupply(1, 100); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ } diff --git a/src/test/drop/DropERC20.t.sol b/src/test/drop/DropERC20.t.sol index e2c76f1e0..f0958a5fb 100644 --- a/src/test/drop/DropERC20.t.sol +++ b/src/test/drop/DropERC20.t.sol @@ -1,49 +1,716 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; -import { DropERC20 } from "contracts/drop/DropERC20.sol"; +import { DropERC20, Permissions, Drop } from "contracts/prebuilts/drop/DropERC20.sol"; // Test imports + import "../utils/BaseTest.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; contract DropERC20Test is BaseTest { + using Strings for uint256; + using Strings for address; + DropERC20 public drop; + using stdStorage for StdStorage; + function setUp() public override { super.setUp(); drop = DropERC20(getContract("DropERC20")); + + erc20.mint(deployer, 1_000 ether); + vm.deal(deployer, 1_000 ether); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: misc. + //////////////////////////////////////////////////////////////*/ + + /** + * note: Tests whether contract reverts when a non-holder renounces a role. + */ + function test_revert_nonHolder_renounceRole() public { + address caller = address(0x123); + bytes32 role = keccak256("TRANSFER_ROLE"); + + vm.prank(caller); + vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsUnauthorizedAccount.selector, caller, role)); + drop.renounceRole(role, caller); + } + + /** + * note: Tests whether contract reverts when a role admin revokes a role for a non-holder. + */ + function test_revert_revokeRoleForNonHolder() public { + address target = address(0x123); + bytes32 role = keccak256("TRANSFER_ROLE"); + + vm.prank(deployer); + vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsUnauthorizedAccount.selector, target, role)); + drop.revokeRole(role, target); + } + + /** + * @dev Tests whether contract reverts when a role is granted to an existent role holder. + */ + function test_revert_grant_role_to_account_with_role() public { + bytes32 role = keccak256("ABC_ROLE"); + address receiver = getActor(0); + + vm.startPrank(deployer); + + drop.grantRole(role, receiver); + + vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsAlreadyGranted.selector, receiver, role)); + drop.grantRole(role, receiver); + + vm.stopPrank(); + } + + /** + * @dev Tests contract state for Transfer role. + */ + function test_state_grant_transferRole() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + + // check if admin and address(0) have transfer role in the beginning + bool checkAddressZero = drop.hasRole(role, address(0)); + bool checkAdmin = drop.hasRole(role, deployer); + assertTrue(checkAddressZero); + assertTrue(checkAdmin); + + // check if transfer role can be granted to a non-holder + address receiver = getActor(0); + vm.startPrank(deployer); + drop.grantRole(role, receiver); + + // expect revert when granting to a holder + vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsAlreadyGranted.selector, receiver, role)); + drop.grantRole(role, receiver); + + // check if receiver has transfer role + bool checkReceiver = drop.hasRole(role, receiver); + assertTrue(checkReceiver); + + // check if role is correctly revoked + drop.revokeRole(role, receiver); + checkReceiver = drop.hasRole(role, receiver); + assertFalse(checkReceiver); + drop.revokeRole(role, address(0)); + checkAddressZero = drop.hasRole(role, address(0)); + assertFalse(checkAddressZero); + + vm.stopPrank(); + } + + /** + * @dev Tests contract state for Transfer role. + */ + function test_state_getRoleMember_transferRole() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + + uint256 roleMemberCount = drop.getRoleMemberCount(role); + assertEq(roleMemberCount, 2); + + address roleMember = drop.getRoleMember(role, 1); + assertEq(roleMember, address(0)); + + vm.startPrank(deployer); + drop.grantRole(role, address(2)); + drop.grantRole(role, address(3)); + drop.grantRole(role, address(4)); + + roleMemberCount = drop.getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(drop.getRoleMember(role, i)); + } + console.log(""); + + drop.revokeRole(role, address(2)); + roleMemberCount = drop.getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(drop.getRoleMember(role, i)); + } + console.log(""); + + drop.revokeRole(role, address(0)); + roleMemberCount = drop.getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(drop.getRoleMember(role, i)); + } + console.log(""); + + drop.grantRole(role, address(5)); + roleMemberCount = drop.getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(drop.getRoleMember(role, i)); + } + console.log(""); + + drop.grantRole(role, address(0)); + roleMemberCount = drop.getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(drop.getRoleMember(role, i)); + } + console.log(""); + + drop.grantRole(role, address(6)); + roleMemberCount = drop.getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(drop.getRoleMember(role, i)); + } + console.log(""); + } + + /** + * note: Testing transfer of tokens when transfer-role is restricted + */ + function test_claim_transferRole() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + DropERC20.AllowlistProof memory alp; + alp.proof = proofs; + + DropERC20.ClaimCondition[] memory conditions = new DropERC20.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + drop.claim(receiver, 1, address(0), 0, alp, ""); + + // revoke transfer role from address(0) + vm.prank(deployer); + drop.revokeRole(keccak256("TRANSFER_ROLE"), address(0)); + vm.startPrank(receiver); + vm.expectRevert("transfers restricted."); + drop.transferFrom(receiver, address(123), 0); + } + + /** + * @dev Tests whether role member count is incremented correctly. + */ + function test_member_count_incremented_properly_when_role_granted() public { + bytes32 role = keccak256("ABC_ROLE"); + address receiver = getActor(0); + + vm.startPrank(deployer); + uint256 roleMemberCount = drop.getRoleMemberCount(role); + + assertEq(roleMemberCount, 0); + + drop.grantRole(role, receiver); + + assertEq(drop.getRoleMemberCount(role), 1); + + vm.stopPrank(); + } + + function test_claimCondition_with_startTimestamp() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + DropERC20.AllowlistProof memory alp; + alp.proof = proofs; + + DropERC20.ClaimCondition[] memory conditions = new DropERC20.ClaimCondition[](1); + conditions[0].startTimestamp = 100; + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.warp(99); + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(abi.encodeWithSelector(Drop.DropNoActiveCondition.selector)); + drop.claim(receiver, 1, address(0), 0, alp, ""); + + vm.warp(100); + vm.prank(getActor(4), getActor(4)); + drop.claim(receiver, 1, address(0), 0, alp, ""); } /*/////////////////////////////////////////////////////////////// - Miscellaneous + Claim Tests //////////////////////////////////////////////////////////////*/ - function test_revert_claim_claimQty() public { + /** + * note: Testing revert condition; exceed max claimable supply. + */ + function test_revert_claimCondition_exceedMaxClaimableSupply() public { vm.warp(1); address receiver = getActor(0); bytes32[] memory proofs = new bytes32[](0); - DropERC20.ClaimCondition[] memory conditions = new DropERC1155.ClaimCondition[](1); + DropERC20.AllowlistProof memory alp; + alp.proof = proofs; + + DropERC20.ClaimCondition[] memory conditions = new DropERC20.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 200; + + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + drop.claim(receiver, 100, address(0), 0, alp, ""); + + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedMaxSupply.selector, conditions[0].maxClaimableSupply, 101) + ); + vm.prank(getActor(6), getActor(6)); + drop.claim(receiver, 1, address(0), 0, alp, ""); + } + + /** + * note: Testing quantity limit restriction when no allowlist present. + */ + function test_fuzz_claim_noAllowlist(uint256 x) public { + vm.assume(x != 0); + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + DropERC20.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = x; + + DropERC20.ClaimCondition[] memory conditions = new DropERC20.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, conditions[0].quantityLimitPerWallet, 0) + ); + drop.claim(receiver, 0, address(0), 0, alp, ""); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, conditions[0].quantityLimitPerWallet, 101) + ); + drop.claim(receiver, 101, address(0), 0, alp, ""); + + vm.prank(deployer); + drop.setClaimConditions(conditions, true); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, conditions[0].quantityLimitPerWallet, 101) + ); + drop.claim(receiver, 101, address(0), 0, alp, ""); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price set to 0 + */ + function test_state_claim_allowlisted_SetQuantityZeroPrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "0"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + DropERC20.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 0; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + DropERC20.ClaimCondition[] memory conditions = new DropERC20.ClaimCondition[](1); conditions[0].maxClaimableSupply = 500; - conditions[0].quantityLimitPerTransaction = 100; - conditions[0].waitTimeInSecondsBetweenClaims = type(uint256).max; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, 100, address(erc20), 0, alp, ""); // claims for free, because allowlist price is 0 + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), 100); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price set to non-zero value + */ + function test_state_claim_allowlisted_SetQuantityPrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = Strings.toString(uint256(300 ether)); + inputs[3] = Strings.toString(uint256(1 ether)); + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + DropERC20.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300 ether; + alp.pricePerToken = 1 ether; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + DropERC20.ClaimCondition[] memory conditions = new DropERC20.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500 ether; + conditions[0].quantityLimitPerWallet = 10 ether; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 5 ether; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.prank(receiver, receiver); + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimInvalidTokenPrice.selector, address(erc20), 0, address(erc20), 1 ether) + ); + drop.claim(receiver, 100 ether, address(erc20), 0, alp, ""); + + erc20.mint(receiver, 1000 ether); + vm.prank(receiver); + erc20.approve(address(drop), 1000 ether); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, 100 ether, address(erc20), 1 ether, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), 100 ether); + assertEq(erc20.balanceOf(receiver), 900 ether); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price not set; should default to general price and currency + */ + function test_state_claim_allowlisted_SetQuantityDefaultPrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = Strings.toString(uint256(300 ether)); + inputs[3] = Strings.toString(type(uint256).max); // this implies that general price is applicable + inputs[4] = "0x0000000000000000000000000000000000000000"; + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + DropERC20.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300 ether; + alp.pricePerToken = type(uint256).max; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + DropERC20.ClaimCondition[] memory conditions = new DropERC20.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500 ether; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10 ether; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + erc20.mint(receiver, 10000 ether); + vm.prank(receiver); + erc20.approve(address(drop), 10000 ether); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, 100 ether, address(erc20), 10 ether, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), 100 ether); + assertEq(erc20.balanceOf(receiver), 10000 ether - 1000 ether); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to 0 => should default to general limit + * - allowlist price set to some value different than general price + */ + function test_state_claim_allowlisted_DefaultQuantitySomePrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "0"; // this implies that general limit is applicable + inputs[3] = Strings.toString(uint256(5 ether)); + inputs[4] = "0x0000000000000000000000000000000000000000"; // general currency will be applicable + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + DropERC20.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 0; + alp.pricePerToken = 5 ether; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + DropERC20.ClaimCondition[] memory conditions = new DropERC20.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500 ether; + conditions[0].quantityLimitPerWallet = 10 ether; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10 ether; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + erc20.mint(receiver, 10000 ether); + vm.prank(receiver); + erc20.approve(address(drop), 10000 ether); - // vm.prank(deployer); - // drop.setMaxTotalSupply(10_000); + vm.prank(receiver, receiver); + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, conditions[0].quantityLimitPerWallet, 100 ether) + ); + drop.claim(receiver, 100 ether, address(erc20), 5 ether, alp, ""); // trying to claim more than general limit + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, 10 ether, address(erc20), 5 ether, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), 10 ether); + assertEq(erc20.balanceOf(receiver), 10000 ether - 50 ether); + } + + function test_fuzz_claim_merkleProof(uint256 x) public { + vm.assume(x > 10 && x < 500); + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = Strings.toString(x); + inputs[3] = "0"; + inputs[4] = "0x0000000000000000000000000000000000000000"; + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + DropERC20.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = x; + alp.pricePerToken = 0; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + + // bytes32[] memory proofs = new bytes32[](0); + + DropERC20.ClaimCondition[] memory conditions = new DropERC20.ClaimCondition[](1); + conditions[0].maxClaimableSupply = x; + conditions[0].quantityLimitPerWallet = 1; + conditions[0].merkleRoot = root; + + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, x - 5, address(0), 0, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), x - 5); + + vm.prank(receiver, receiver); + vm.expectRevert(abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, x, x + 1)); + drop.claim(receiver, 6, address(0), 0, alp, ""); + + vm.prank(receiver, receiver); + drop.claim(receiver, 5, address(0), 0, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), x); + + vm.prank(receiver, receiver); + vm.expectRevert(abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, x, x + 5)); + drop.claim(receiver, 5, address(0), 0, alp, ""); // quantity limit already claimed + } + + /** + * note: Testing state changes; reset eligibility of claim conditions and claiming again for same condition id. + */ + function test_state_claimCondition_resetEligibility() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + DropERC20.AllowlistProof memory alp; + alp.proof = proofs; + + DropERC20.ClaimCondition[] memory conditions = new DropERC20.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 100; vm.prank(deployer); drop.setClaimConditions(conditions, false); vm.prank(getActor(5), getActor(5)); - vm.expectRevert("invalid quantity claimed."); - drop.claim(receiver, 200, address(0), 0, proofs, 1); + drop.claim(receiver, 100, address(0), 0, alp, ""); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, conditions[0].quantityLimitPerWallet, 200) + ); + drop.claim(receiver, 100, address(0), 0, alp, ""); vm.prank(deployer); drop.setClaimConditions(conditions, true); vm.prank(getActor(5), getActor(5)); - vm.expectRevert("invalid quantity claimed."); - drop.claim(receiver, 200, address(0), 0, proofs, 1); + drop.claim(receiver, 100, address(0), 0, alp, ""); + } + + /*/////////////////////////////////////////////////////////////// + setClaimConditions + //////////////////////////////////////////////////////////////*/ + + function test_claimCondition_startIdAndCount() public { + vm.startPrank(deployer); + + uint256 currentStartId = 0; + uint256 count = 0; + + DropERC20.ClaimCondition[] memory conditions = new DropERC20.ClaimCondition[](2); + conditions[0].startTimestamp = 0; + conditions[0].maxClaimableSupply = 10; + conditions[1].startTimestamp = 1; + conditions[1].maxClaimableSupply = 10; + + drop.setClaimConditions(conditions, false); + (currentStartId, count) = drop.claimCondition(); + assertEq(currentStartId, 0); + assertEq(count, 2); + + drop.setClaimConditions(conditions, false); + (currentStartId, count) = drop.claimCondition(); + assertEq(currentStartId, 0); + assertEq(count, 2); + + drop.setClaimConditions(conditions, true); + (currentStartId, count) = drop.claimCondition(); + assertEq(currentStartId, 2); + assertEq(count, 2); + + drop.setClaimConditions(conditions, true); + (currentStartId, count) = drop.claimCondition(); + assertEq(currentStartId, 4); + assertEq(count, 2); + } + + function test_claimCondition_startPhase() public { + vm.startPrank(deployer); + + uint256 activeConditionId = 0; + + DropERC20.ClaimCondition[] memory conditions = new DropERC20.ClaimCondition[](3); + conditions[0].startTimestamp = 10; + conditions[0].maxClaimableSupply = 11; + conditions[0].quantityLimitPerWallet = 12; + conditions[1].startTimestamp = 20; + conditions[1].maxClaimableSupply = 21; + conditions[1].quantityLimitPerWallet = 22; + conditions[2].startTimestamp = 30; + conditions[2].maxClaimableSupply = 31; + conditions[2].quantityLimitPerWallet = 32; + drop.setClaimConditions(conditions, false); + + vm.expectRevert(abi.encodeWithSelector(Drop.DropNoActiveCondition.selector)); + drop.getActiveClaimConditionId(); + + vm.warp(10); + activeConditionId = drop.getActiveClaimConditionId(); + assertEq(activeConditionId, 0); + assertEq(drop.getClaimConditionById(activeConditionId).startTimestamp, 10); + assertEq(drop.getClaimConditionById(activeConditionId).maxClaimableSupply, 11); + assertEq(drop.getClaimConditionById(activeConditionId).quantityLimitPerWallet, 12); + + vm.warp(20); + activeConditionId = drop.getActiveClaimConditionId(); + assertEq(activeConditionId, 1); + assertEq(drop.getClaimConditionById(activeConditionId).startTimestamp, 20); + assertEq(drop.getClaimConditionById(activeConditionId).maxClaimableSupply, 21); + assertEq(drop.getClaimConditionById(activeConditionId).quantityLimitPerWallet, 22); + + vm.warp(30); + activeConditionId = drop.getActiveClaimConditionId(); + assertEq(activeConditionId, 2); + assertEq(drop.getClaimConditionById(activeConditionId).startTimestamp, 30); + assertEq(drop.getClaimConditionById(activeConditionId).maxClaimableSupply, 31); + assertEq(drop.getClaimConditionById(activeConditionId).quantityLimitPerWallet, 32); + + vm.warp(40); + assertEq(drop.getActiveClaimConditionId(), 2); } } diff --git a/src/test/drop/DropERC721.t.sol b/src/test/drop/DropERC721.t.sol index 03f7e7eed..14d3fdeec 100644 --- a/src/test/drop/DropERC721.t.sol +++ b/src/test/drop/DropERC721.t.sol @@ -1,275 +1,792 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; -import { DropERC721 } from "contracts/drop/DropERC721.sol"; -import "@openzeppelin/contracts/utils/Strings.sol"; +import { DropERC721, Permissions, LazyMint, BatchMintMetadata, Drop, DelayedReveal, IDelayedReveal, ERC721AUpgradeable, IPermissions, ILazyMint } from "contracts/prebuilts/drop/DropERC721.sol"; // Test imports +import "erc721a-upgradeable/contracts/IERC721AUpgradeable.sol"; + import "../utils/BaseTest.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; -contract SubExploitContract is ERC721Holder, ERC1155Holder { - DropERC721 internal drop; - address payable internal master; +contract DropERC721Test is BaseTest { + using Strings for uint256; + using Strings for address; - // using Strings for uint256; + event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI); + event TokenURIRevealed(uint256 indexed index, string revealedURI); + event MaxTotalSupplyUpdated(uint256 maxTotalSupply); - // using Strings for uint256; + DropERC721 public drop; - constructor(address _drop) { - drop = DropERC721(_drop); - master = payable(msg.sender); - } + bytes private emptyEncodedBytes = abi.encode("", ""); - /// @dev Lets an account claim NFTs. - function claimDrop( - address _receiver, - uint256 _quantity, - address _currency, - uint256 _pricePerToken, - bytes32[] calldata _proofs, - uint256 _proofMaxQuantityPerTransaction - ) external { - drop.claim(_receiver, _quantity, _currency, _pricePerToken, _proofs, _proofMaxQuantityPerTransaction); + using stdStorage for StdStorage; - selfdestruct(master); - } -} + function setUp() public override { + super.setUp(); + drop = DropERC721(getContract("DropERC721")); -contract MasterExploitContract is ERC721Holder, ERC1155Holder { - address internal drop; - - constructor(address _drop) { - drop = _drop; - } - - /// @dev Lets an account claim NFTs. - function performExploit( - address _receiver, - uint256 _quantity, - address _currency, - uint256 _pricePerToken, - bytes32[] calldata _proofs, - uint256 _proofMaxQuantityPerTransaction - ) external { - for (uint256 i = 0; i < 100; i++) { - SubExploitContract sub = new SubExploitContract(address(drop)); - sub.claimDrop(_receiver, _quantity, _currency, _pricePerToken, _proofs, _proofMaxQuantityPerTransaction); - } + erc20.mint(deployer, 1_000 ether); + vm.deal(deployer, 1_000 ether); } -} -contract DropERC721Test is BaseTest { - DropERC721 public drop; + /*/////////////////////////////////////////////////////////////// + Unit tests: misc. + //////////////////////////////////////////////////////////////*/ - function setUp() public override { - super.setUp(); - drop = DropERC721(getContract("DropERC721")); + /** + * note: Tests whether contract reverts when a non-holder renounces a role. + */ + function test_revert_nonHolder_renounceRole() public { + address caller = address(0x123); + bytes32 role = keccak256("MINTER_ROLE"); + + vm.prank(caller); + vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsUnauthorizedAccount.selector, caller, role)); + drop.renounceRole(role, caller); } - function test_claimCondition_startIdAndCount() public { - vm.startPrank(deployer); + /** + * note: Tests whether contract reverts when a role admin revokes a role for a non-holder. + */ + function test_revert_revokeRoleForNonHolder() public { + address target = address(0x123); + bytes32 role = keccak256("MINTER_ROLE"); - uint256 currentStartId = 0; - uint256 count = 0; + vm.prank(deployer); + vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsUnauthorizedAccount.selector, target, role)); + drop.revokeRole(role, target); + } - DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](2); - conditions[0].startTimestamp = 0; - conditions[0].maxClaimableSupply = 10; - conditions[1].startTimestamp = 1; - conditions[1].maxClaimableSupply = 10; + /** + * @dev Tests whether contract reverts when a role is granted to an existent role holder. + */ + function test_revert_grant_role_to_account_with_role() public { + bytes32 role = keccak256("ABC_ROLE"); + address receiver = getActor(0); - drop.setClaimConditions(conditions, false); - (currentStartId, count) = drop.claimCondition(); - assertEq(currentStartId, 0); - assertEq(count, 2); + vm.startPrank(deployer); - drop.setClaimConditions(conditions, false); - (currentStartId, count) = drop.claimCondition(); - assertEq(currentStartId, 0); - assertEq(count, 2); + drop.grantRole(role, receiver); - drop.setClaimConditions(conditions, true); - (currentStartId, count) = drop.claimCondition(); - assertEq(currentStartId, 2); - assertEq(count, 2); + vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsAlreadyGranted.selector, receiver, role)); + drop.grantRole(role, receiver); - drop.setClaimConditions(conditions, true); - (currentStartId, count) = drop.claimCondition(); - assertEq(currentStartId, 4); - assertEq(count, 2); + vm.stopPrank(); } - function test_claimCondition_startPhase() public { + /** + * @dev Tests contract state for Transfer role. + */ + function test_state_grant_transferRole() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + + // check if admin and address(0) have transfer role in the beginning + bool checkAddressZero = drop.hasRole(role, address(0)); + bool checkAdmin = drop.hasRole(role, deployer); + assertTrue(checkAddressZero); + assertTrue(checkAdmin); + + // check if transfer role can be granted to a non-holder + address receiver = getActor(0); vm.startPrank(deployer); + drop.grantRole(role, receiver); - uint256 activeConditionId = 0; + // expect revert when granting to a holder + vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsAlreadyGranted.selector, receiver, role)); + drop.grantRole(role, receiver); - DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](3); - conditions[0].startTimestamp = 10; - conditions[0].maxClaimableSupply = 11; - conditions[0].quantityLimitPerTransaction = 12; - conditions[0].waitTimeInSecondsBetweenClaims = 13; - conditions[1].startTimestamp = 20; - conditions[1].maxClaimableSupply = 21; - conditions[1].quantityLimitPerTransaction = 22; - conditions[1].waitTimeInSecondsBetweenClaims = 23; - conditions[2].startTimestamp = 30; - conditions[2].maxClaimableSupply = 31; - conditions[2].quantityLimitPerTransaction = 32; - conditions[2].waitTimeInSecondsBetweenClaims = 33; - drop.setClaimConditions(conditions, false); + // check if receiver has transfer role + bool checkReceiver = drop.hasRole(role, receiver); + assertTrue(checkReceiver); - vm.expectRevert("!CONDITION."); - drop.getActiveClaimConditionId(); + // check if role is correctly revoked + drop.revokeRole(role, receiver); + checkReceiver = drop.hasRole(role, receiver); + assertFalse(checkReceiver); + drop.revokeRole(role, address(0)); + checkAddressZero = drop.hasRole(role, address(0)); + assertFalse(checkAddressZero); - vm.warp(10); - activeConditionId = drop.getActiveClaimConditionId(); - assertEq(activeConditionId, 0); - assertEq(drop.getClaimConditionById(activeConditionId).startTimestamp, 10); - assertEq(drop.getClaimConditionById(activeConditionId).maxClaimableSupply, 11); - assertEq(drop.getClaimConditionById(activeConditionId).quantityLimitPerTransaction, 12); - assertEq(drop.getClaimConditionById(activeConditionId).waitTimeInSecondsBetweenClaims, 13); + vm.stopPrank(); + } - vm.warp(20); - activeConditionId = drop.getActiveClaimConditionId(); - assertEq(activeConditionId, 1); - assertEq(drop.getClaimConditionById(activeConditionId).startTimestamp, 20); - assertEq(drop.getClaimConditionById(activeConditionId).maxClaimableSupply, 21); - assertEq(drop.getClaimConditionById(activeConditionId).quantityLimitPerTransaction, 22); - assertEq(drop.getClaimConditionById(activeConditionId).waitTimeInSecondsBetweenClaims, 23); + /** + * @dev Tests contract state for Transfer role. + */ + function test_state_getRoleMember_transferRole() public { + bytes32 role = keccak256("TRANSFER_ROLE"); - vm.warp(30); - activeConditionId = drop.getActiveClaimConditionId(); - assertEq(activeConditionId, 2); - assertEq(drop.getClaimConditionById(activeConditionId).startTimestamp, 30); - assertEq(drop.getClaimConditionById(activeConditionId).maxClaimableSupply, 31); - assertEq(drop.getClaimConditionById(activeConditionId).quantityLimitPerTransaction, 32); - assertEq(drop.getClaimConditionById(activeConditionId).waitTimeInSecondsBetweenClaims, 33); + uint256 roleMemberCount = drop.getRoleMemberCount(role); + assertEq(roleMemberCount, 2); - vm.warp(40); - assertEq(drop.getActiveClaimConditionId(), 2); + address roleMember = drop.getRoleMember(role, 1); + assertEq(roleMember, address(0)); + + vm.startPrank(deployer); + drop.grantRole(role, address(2)); + drop.grantRole(role, address(3)); + drop.grantRole(role, address(4)); + + roleMemberCount = drop.getRoleMemberCount(role); + assertEq(roleMemberCount, 5); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(drop.getRoleMember(role, i)); + } + console.log(""); + + drop.revokeRole(role, address(2)); + roleMemberCount = drop.getRoleMemberCount(role); + assertEq(roleMemberCount, 4); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(drop.getRoleMember(role, i)); + } + console.log(""); + + drop.revokeRole(role, address(0)); + roleMemberCount = drop.getRoleMemberCount(role); + assertEq(roleMemberCount, 3); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(drop.getRoleMember(role, i)); + } + console.log(""); + + drop.grantRole(role, address(5)); + roleMemberCount = drop.getRoleMemberCount(role); + assertEq(roleMemberCount, 4); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(drop.getRoleMember(role, i)); + } + console.log(""); + + drop.grantRole(role, address(0)); + roleMemberCount = drop.getRoleMemberCount(role); + assertEq(roleMemberCount, 5); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(drop.getRoleMember(role, i)); + } + console.log(""); + + drop.grantRole(role, address(6)); + roleMemberCount = drop.getRoleMemberCount(role); + assertEq(roleMemberCount, 6); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(drop.getRoleMember(role, i)); + } + console.log(""); + + drop.revokeRole(role, address(0)); + roleMemberCount = drop.getRoleMemberCount(role); + assertEq(roleMemberCount, 5); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(drop.getRoleMember(role, i)); + } + console.log(""); + + drop.revokeRole(role, address(4)); + roleMemberCount = drop.getRoleMemberCount(role); + assertEq(roleMemberCount, 4); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(drop.getRoleMember(role, i)); + } + console.log(""); } - function test_claimCondition_waitTimeInSecondsBetweenClaims() public { + /** + * note: Testing transfer of tokens when transfer-role is restricted + */ + function test_claim_transferRole() public { vm.warp(1); address receiver = getActor(0); bytes32[] memory proofs = new bytes32[](0); + DropERC721.AllowlistProof memory alp; + alp.proof = proofs; + DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](1); conditions[0].maxClaimableSupply = 100; - conditions[0].quantityLimitPerTransaction = 100; - conditions[0].waitTimeInSecondsBetweenClaims = type(uint256).max; + conditions[0].quantityLimitPerWallet = 100; vm.prank(deployer); - drop.lazyMint(100, "ipfs://", bytes("")); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); vm.prank(deployer); drop.setClaimConditions(conditions, false); vm.prank(getActor(5), getActor(5)); - drop.claim(receiver, 1, address(0), 0, proofs, 0); + drop.claim(receiver, 1, address(0), 0, alp, ""); - vm.expectRevert("cannot claim."); - vm.prank(getActor(5), getActor(5)); - drop.claim(receiver, 1, address(0), 0, proofs, 0); + // revoke transfer role from address(0) + vm.prank(deployer); + drop.revokeRole(keccak256("TRANSFER_ROLE"), address(0)); + vm.startPrank(receiver); + vm.expectRevert("!Transfer-Role"); + drop.transferFrom(receiver, address(123), 0); } - function test_claimCondition_resetEligibility_waitTimeInSecondsBetweenClaims() public { + /** + * @dev Tests whether role member count is incremented correctly. + */ + function test_member_count_incremented_properly_when_role_granted() public { + bytes32 role = keccak256("ABC_ROLE"); + address receiver = getActor(0); + + vm.startPrank(deployer); + uint256 roleMemberCount = drop.getRoleMemberCount(role); + + assertEq(roleMemberCount, 0); + + drop.grantRole(role, receiver); + + assertEq(drop.getRoleMemberCount(role), 1); + + vm.stopPrank(); + } + + function test_claimCondition_with_startTimestamp() public { vm.warp(1); address receiver = getActor(0); bytes32[] memory proofs = new bytes32[](0); + DropERC721.AllowlistProof memory alp; + alp.proof = proofs; + DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](1); + conditions[0].startTimestamp = 100; conditions[0].maxClaimableSupply = 100; - conditions[0].quantityLimitPerTransaction = 100; - conditions[0].waitTimeInSecondsBetweenClaims = type(uint256).max; + conditions[0].quantityLimitPerWallet = 100; vm.prank(deployer); - drop.lazyMint(100, "ipfs://", bytes("")); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); vm.prank(deployer); drop.setClaimConditions(conditions, false); + vm.warp(99); vm.prank(getActor(5), getActor(5)); - drop.claim(receiver, 1, address(0), 0, proofs, 0); + vm.expectRevert(abi.encodeWithSelector(Drop.DropNoActiveCondition.selector)); + drop.claim(receiver, 1, address(0), 0, alp, ""); + + vm.warp(100); + vm.prank(getActor(4), getActor(4)); + drop.claim(receiver, 1, address(0), 0, alp, ""); + } + + /*/////////////////////////////////////////////////////////////// + Lazy Mint Tests + //////////////////////////////////////////////////////////////*/ + + /* + * note: Testing state changes; lazy mint a batch of tokens with no encrypted base URI. + */ + function test_state_lazyMint_noEncryptedURI() public { + uint256 amountToLazyMint = 100; + string memory baseURI = "ipfs://"; + + uint256 nextTokenIdToMintBefore = drop.nextTokenIdToMint(); + + vm.startPrank(deployer); + uint256 batchId = drop.lazyMint(amountToLazyMint, baseURI, emptyEncodedBytes); + + assertEq(nextTokenIdToMintBefore + amountToLazyMint, drop.nextTokenIdToMint()); + assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); + + for (uint256 i = 0; i < amountToLazyMint; i += 1) { + string memory uri = drop.tokenURI(i); + console.log(uri); + assertEq(uri, string(abi.encodePacked(baseURI, i.toString()))); + } + + vm.stopPrank(); + } + + /* + * note: Testing state changes; lazy mint a batch of tokens with encrypted base URI. + */ + function test_state_lazyMint_withEncryptedURI() public { + uint256 amountToLazyMint = 100; + string memory baseURI = "ipfs://"; + bytes memory encryptedBaseURI = "encryptedBaseURI://"; + bytes32 provenanceHash = bytes32("whatever"); + + uint256 nextTokenIdToMintBefore = drop.nextTokenIdToMint(); + + vm.startPrank(deployer); + uint256 batchId = drop.lazyMint(amountToLazyMint, baseURI, abi.encode(encryptedBaseURI, provenanceHash)); + + assertEq(nextTokenIdToMintBefore + amountToLazyMint, drop.nextTokenIdToMint()); + assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); + + for (uint256 i = 0; i < amountToLazyMint; i += 1) { + string memory uri = drop.tokenURI(i); + console.log(uri); + assertEq(uri, string(abi.encodePacked(baseURI, "0"))); + } + vm.stopPrank(); + } + + /** + * note: Testing revert condition; an address without MINTER_ROLE calls lazyMint function. + */ + function test_revert_lazyMint_MINTER_ROLE() public { + vm.expectRevert(abi.encodeWithSelector(LazyMint.LazyMintUnauthorized.selector)); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + } + + /* + * note: Testing revert condition; calling tokenURI for invalid batch id. + */ + function test_revert_lazyMint_URIForNonLazyMintedToken() public { + vm.startPrank(deployer); + + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + vm.expectRevert(abi.encodeWithSelector(BatchMintMetadata.BatchMintInvalidTokenId.selector, 100)); + drop.tokenURI(100); + + vm.stopPrank(); + } + + /** + * note: Testing event emission; tokens lazy minted. + */ + function test_event_lazyMint_TokensLazyMinted() public { + vm.startPrank(deployer); + + vm.expectEmit(true, false, false, true); + emit TokensLazyMinted(0, 99, "ipfs://", emptyEncodedBytes); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + vm.stopPrank(); + } + + /* + * note: Fuzz testing state changes; lazy mint a batch of tokens with no encrypted base URI. + */ + function test_fuzz_lazyMint_noEncryptedURI(uint256 x) public { + vm.assume(x > 0); + + uint256 amountToLazyMint = x; + string memory baseURI = "ipfs://"; + + uint256 nextTokenIdToMintBefore = drop.nextTokenIdToMint(); + + vm.startPrank(deployer); + uint256 batchId = drop.lazyMint(amountToLazyMint, baseURI, emptyEncodedBytes); + + assertEq(nextTokenIdToMintBefore + amountToLazyMint, drop.nextTokenIdToMint()); + assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); + + string memory uri = drop.tokenURI(0); + assertEq(uri, string(abi.encodePacked(baseURI, uint256(0).toString()))); + + uri = drop.tokenURI(x - 1); + assertEq(uri, string(abi.encodePacked(baseURI, uint256(x - 1).toString()))); + + /** + * note: this loop takes too long to run with fuzz tests. + */ + // for(uint256 i = 0; i < amountToLazyMint; i += 1) { + // string memory uri = drop.tokenURI(i); + // console.log(uri); + // assertEq(uri, string(abi.encodePacked(baseURI, i.toString()))); + // } + + vm.stopPrank(); + } + + /* + * note: Fuzz testing state changes; lazy mint a batch of tokens with encrypted base URI. + */ + function test_fuzz_lazyMint_withEncryptedURI(uint256 x) public { + vm.assume(x > 0); + + uint256 amountToLazyMint = x; + string memory baseURI = "ipfs://"; + bytes memory encryptedBaseURI = "encryptedBaseURI://"; + bytes32 provenanceHash = bytes32("whatever"); + + uint256 nextTokenIdToMintBefore = drop.nextTokenIdToMint(); + + vm.startPrank(deployer); + uint256 batchId = drop.lazyMint(amountToLazyMint, baseURI, abi.encode(encryptedBaseURI, provenanceHash)); + + assertEq(nextTokenIdToMintBefore + amountToLazyMint, drop.nextTokenIdToMint()); + assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); + + string memory uri = drop.tokenURI(0); + assertEq(uri, string(abi.encodePacked(baseURI, "0"))); + + uri = drop.tokenURI(x - 1); + assertEq(uri, string(abi.encodePacked(baseURI, "0"))); + + /** + * note: this loop takes too long to run with fuzz tests. + */ + // for(uint256 i = 0; i < amountToLazyMint; i += 1) { + // string memory uri = drop.tokenURI(1); + // assertEq(uri, string(abi.encodePacked(baseURI, "0"))); + // } + + vm.stopPrank(); + } + + /* + * note: Fuzz testing; a batch of tokens, and nextTokenIdToMint + */ + function test_fuzz_lazyMint_batchMintAndNextTokenIdToMint(uint256 x) public { + vm.assume(x > 0); + vm.startPrank(deployer); + + if (x == 0) { + vm.expectRevert("Zero amount"); + } + drop.lazyMint(x, "ipfs://", emptyEncodedBytes); + + uint256 slot = stdstore.target(address(drop)).sig("nextTokenIdToMint()").find(); + bytes32 loc = bytes32(slot); + uint256 nextTokenIdToMint = uint256(vm.load(address(drop), loc)); + + assertEq(nextTokenIdToMint, x); + vm.stopPrank(); + } + + /*/////////////////////////////////////////////////////////////// + Delayed Reveal Tests + //////////////////////////////////////////////////////////////*/ + + /* + * note: Testing state changes; URI revealed for a batch of tokens. + */ + function test_state_reveal() public { + vm.startPrank(deployer); + + bytes memory key = "key"; + uint256 amountToLazyMint = 100; + bytes memory secretURI = "ipfs://"; + string memory placeholderURI = "abcd://"; + bytes memory encryptedURI = drop.encryptDecrypt(secretURI, key); + bytes32 provenanceHash = keccak256(abi.encodePacked(secretURI, key, block.chainid)); + + drop.lazyMint(amountToLazyMint, placeholderURI, abi.encode(encryptedURI, provenanceHash)); + + for (uint256 i = 0; i < amountToLazyMint; i += 1) { + string memory uri = drop.tokenURI(i); + assertEq(uri, string(abi.encodePacked(placeholderURI, "0"))); + } + + string memory revealedURI = drop.reveal(0, key); + assertEq(revealedURI, string(secretURI)); + + for (uint256 i = 0; i < amountToLazyMint; i += 1) { + string memory uri = drop.tokenURI(i); + assertEq(uri, string(abi.encodePacked(secretURI, i.toString()))); + } + + vm.stopPrank(); + } + + /** + * note: Testing revert condition; an address without METADATA_ROLE calls reveal function. + */ + function test_revert_reveal_METADATA_ROLE() public { + bytes memory key = "key"; + bytes memory encryptedURI = drop.encryptDecrypt("ipfs://", key); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); vm.prank(deployer); - drop.setClaimConditions(conditions, true); + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); - vm.prank(getActor(5), getActor(5)); - drop.claim(receiver, 1, address(0), 0, proofs, 0); + vm.prank(deployer); + drop.reveal(0, "key"); + + vm.expectRevert( + abi.encodeWithSelector( + Permissions.PermissionsUnauthorizedAccount.selector, + address(this), + keccak256("METADATA_ROLE") + ) + ); + drop.reveal(0, "key"); + } + + /* + * note: Testing revert condition; trying to reveal URI for non-existent batch. + */ + function test_revert_reveal_revealingNonExistentBatch() public { + vm.startPrank(deployer); + + bytes memory key = "key"; + bytes memory encryptedURI = drop.encryptDecrypt("ipfs://", key); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + drop.reveal(0, "key"); + + console.log(drop.getBaseURICount()); + + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + vm.expectRevert(abi.encodeWithSelector(BatchMintMetadata.BatchMintInvalidBatchId.selector, 2)); + drop.reveal(2, "key"); + + vm.stopPrank(); + } + + /* + * note: Testing revert condition; already revealed URI. + */ + function test_revert_delayedReveal_alreadyRevealed() public { + vm.startPrank(deployer); + + bytes memory key = "key"; + bytes memory encryptedURI = drop.encryptDecrypt("ipfs://", key); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + drop.reveal(0, "key"); + + vm.expectRevert(abi.encodeWithSelector(DelayedReveal.DelayedRevealNothingToReveal.selector)); + drop.reveal(0, "key"); + + vm.stopPrank(); + } + + /* + * note: Testing state changes; revealing URI with an incorrect key. + */ + function test_revert_reveal_incorrectKey() public { + vm.startPrank(deployer); + + bytes memory key = "key"; + bytes memory encryptedURI = drop.encryptDecrypt("ipfs://", key); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + + vm.expectRevert(); + string memory revealedURI = drop.reveal(0, "keyy"); + + vm.stopPrank(); + } + + /** + * note: Testing event emission; TokenURIRevealed. + */ + function test_event_reveal_TokenURIRevealed() public { + vm.startPrank(deployer); + + bytes memory key = "key"; + bytes memory encryptedURI = drop.encryptDecrypt("ipfs://", key); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + + vm.expectEmit(true, false, false, true); + emit TokenURIRevealed(0, "ipfs://"); + drop.reveal(0, "key"); + + vm.stopPrank(); + } + + /*/////////////////////////////////////////////////////////////// + Unit Test: updateBatchBaseURI + //////////////////////////////////////////////////////////////*/ + + function test_state_updateBatchBaseURI() public { + string memory initURI = "ipfs://init"; + string memory newURI = "ipfs://new"; + + vm.startPrank(deployer); + drop.lazyMint(100, initURI, ""); + + string memory initTokenURI = drop.tokenURI(0); + + assertEq(initTokenURI, string(abi.encodePacked(initURI, "0"))); + + drop.updateBatchBaseURI(0, newURI); + + string memory newTokenURI = drop.tokenURI(0); + + assertEq(newTokenURI, string(abi.encodePacked(newURI, "0"))); + } + + function test_updateBatchBaseURI_revert_encrypted() public { + bytes memory uri = "ipfs://init"; + bytes memory key = "key"; + + vm.startPrank(deployer); + bytes memory encryptedURI = drop.encryptDecrypt(uri, key); + bytes32 provenanceHash = keccak256(abi.encodePacked(uri, key, block.chainid)); + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + + vm.expectRevert("Encrypted batch"); + drop.updateBatchBaseURI(0, "uri"); + } + + /*/////////////////////////////////////////////////////////////// + Unit Test: freezeBatchBaseURI + //////////////////////////////////////////////////////////////*/ + + function test_state_freezeBatchBaseURI() public { + string memory initURI = "ipfs://init"; + + vm.startPrank(deployer); + drop.lazyMint(100, initURI, ""); + + string memory initTokenURI = drop.tokenURI(0); + + assertEq(initTokenURI, string(abi.encodePacked(initURI, "0"))); + + drop.freezeBatchBaseURI(0); + + assertEq(drop.batchFrozen(100), true); + } + + /*/////////////////////////////////////////////////////////////// + Unit Test: setMaxTotalSupply + //////////////////////////////////////////////////////////////*/ + + function test_state_setMaxTotalSupply() public { + vm.startPrank(deployer); + drop.setMaxTotalSupply(100); + + assertEq(drop.maxTotalSupply(), 100); + } + + function test_event_setMaxTotalSupply_MaxTotalSupplyUpdated() public { + vm.startPrank(deployer); + + vm.expectEmit(true, false, false, true); + emit MaxTotalSupplyUpdated(100); + drop.setMaxTotalSupply(100); } - function test_multiple_claim_exploit() public { - MasterExploitContract masterExploit = new MasterExploitContract(address(drop)); + /*/////////////////////////////////////////////////////////////// + Claim Tests + //////////////////////////////////////////////////////////////*/ + + /** + * note: Testing revert condition; not enough minted tokens. + */ + function test_revert_claimCondition_notEnoughMintedTokens() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + DropERC721.AllowlistProof memory alp; + alp.proof = proofs; DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](1); conditions[0].maxClaimableSupply = 100; - conditions[0].quantityLimitPerTransaction = 1; - conditions[0].waitTimeInSecondsBetweenClaims = type(uint256).max; + conditions[0].quantityLimitPerWallet = 200; vm.prank(deployer); - drop.lazyMint(100, "ipfs://", bytes("")); - + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); vm.prank(deployer); drop.setClaimConditions(conditions, false); + vm.expectRevert("!Tokens"); + vm.prank(getActor(6), getActor(6)); + drop.claim(receiver, 101, address(0), 0, alp, ""); + } + + /** + * note: Testing revert condition; exceed max claimable supply. + */ + function test_revert_claimCondition_exceedMaxClaimableSupply() public { + vm.warp(1); + + address receiver = getActor(0); bytes32[] memory proofs = new bytes32[](0); - vm.startPrank(getActor(5)); - vm.expectRevert(bytes("BOT")); - masterExploit.performExploit( - address(masterExploit), - conditions[0].quantityLimitPerTransaction, - conditions[0].currency, - conditions[0].pricePerToken, - proofs, - 0 + DropERC721.AllowlistProof memory alp; + alp.proof = proofs; + + DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 200; + + vm.prank(deployer); + drop.lazyMint(200, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + drop.claim(receiver, 100, address(0), 0, alp, ""); + + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedMaxSupply.selector, conditions[0].maxClaimableSupply, 101) ); + vm.prank(getActor(6), getActor(6)); + drop.claim(receiver, 1, address(0), 0, alp, ""); } - /*/////////////////////////////////////////////////////////////// - Miscellaneous - //////////////////////////////////////////////////////////////*/ - - function test_revert_claim_claimQty(uint256 x) public { + /** + * note: Testing quantity limit restriction when no allowlist present. + */ + function test_fuzz_claim_noAllowlist(uint256 x) public { vm.assume(x != 0); vm.warp(1); address receiver = getActor(0); bytes32[] memory proofs = new bytes32[](0); + DropERC721.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = x; + DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](1); conditions[0].maxClaimableSupply = 500; - conditions[0].quantityLimitPerTransaction = 100; - conditions[0].waitTimeInSecondsBetweenClaims = type(uint256).max; + conditions[0].quantityLimitPerWallet = 100; vm.prank(deployer); - drop.lazyMint(500, "ipfs://", bytes("")); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); vm.prank(deployer); drop.setClaimConditions(conditions, false); vm.prank(getActor(5), getActor(5)); - vm.expectRevert("invalid quantity."); - drop.claim(receiver, 200, address(0), 0, proofs, x); + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, conditions[0].quantityLimitPerWallet, 0) + ); + drop.claim(receiver, 0, address(0), 0, alp, ""); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, conditions[0].quantityLimitPerWallet, 101) + ); + drop.claim(receiver, 101, address(0), 0, alp, ""); vm.prank(deployer); drop.setClaimConditions(conditions, true); vm.prank(getActor(5), getActor(5)); - vm.expectRevert("invalid quantity."); - drop.claim(receiver, 200, address(0), 0, proofs, x); + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, conditions[0].quantityLimitPerWallet, 101) + ); + drop.claim(receiver, 101, address(0), 0, alp, ""); } - function test_claimCondition_merkleProof(uint256 x) public { - vm.assume(x != 0 && x < 500); - string[] memory inputs = new string[](3); + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price set to 0 + */ + function test_state_claim_allowlisted_SetQuantityZeroPrice() public { + string[] memory inputs = new string[](5); inputs[0] = "node"; inputs[1] = "src/test/scripts/generateRoot.ts"; - inputs[2] = Strings.toString(x); + inputs[2] = "300"; + inputs[3] = "0"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 bytes memory result = vm.ffi(inputs); // revert(); @@ -279,40 +796,220 @@ contract DropERC721Test is BaseTest { result = vm.ffi(inputs); bytes32[] memory proofs = abi.decode(result, (bytes32[])); + DropERC721.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 0; + alp.currency = address(erc20); + vm.warp(1); - address receiver = address(0x92Bb439374a091c7507bE100183d8D1Ed2c9dAD3); + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist - // bytes32[] memory proofs = new bytes32[](0); + DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, 100, address(erc20), 0, alp, ""); // claims for free, because allowlist price is 0 + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), 100); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price set to non-zero value + */ + function test_state_claim_allowlisted_SetQuantityPrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "5"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + DropERC721.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 5; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](1); - conditions[0].maxClaimableSupply = x; - conditions[0].quantityLimitPerTransaction = 100; - conditions[0].waitTimeInSecondsBetweenClaims = type(uint256).max; + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); vm.prank(deployer); - drop.lazyMint(x, "ipfs://", ""); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); vm.prank(deployer); drop.setClaimConditions(conditions, false); + vm.prank(receiver, receiver); + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimInvalidTokenPrice.selector, address(erc20), 0, address(erc20), 5) + ); + drop.claim(receiver, 100, address(erc20), 0, alp, ""); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(drop), 10000); + // vm.prank(getActor(5), getActor(5)); vm.prank(receiver, receiver); - drop.claim(receiver, x, address(0), 0, proofs, x); + drop.claim(receiver, 100, address(erc20), 5, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), 100); + assertEq(erc20.balanceOf(receiver), 10000 - 500); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price not set; should default to general price and currency + */ + function test_state_claim_allowlisted_SetQuantityDefaultPrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = Strings.toString(type(uint256).max); // this implies that general price is applicable + inputs[4] = "0x0000000000000000000000000000000000000000"; + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + DropERC721.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = type(uint256).max; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); - vm.prank(address(4), address(4)); - vm.expectRevert("not in whitelist."); - drop.claim(receiver, x, address(0), 0, proofs, x); + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(drop), 10000); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, 100, address(erc20), 10, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), 100); + assertEq(erc20.balanceOf(receiver), 10000 - 1000); } - function testFail_claimCondition_merkleProof(uint256 x, uint256 y) public { - vm.assume(x != 0 && x < 500); - vm.assume(x != y); - string[] memory inputs = new string[](3); + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to 0 => should default to general limit + * - allowlist price set to some value different than general price + */ + function test_state_claim_allowlisted_DefaultQuantitySomePrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "0"; // this implies that general limit is applicable + inputs[3] = "5"; + inputs[4] = "0x0000000000000000000000000000000000000000"; // general currency will be applicable + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + DropERC721.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 0; + alp.pricePerToken = 5; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(drop), 10000); + + vm.prank(receiver, receiver); + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, conditions[0].quantityLimitPerWallet, 100) + ); + drop.claim(receiver, 100, address(erc20), 5, alp, ""); // trying to claim more than general limit + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, 10, address(erc20), 5, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), 10); + assertEq(erc20.balanceOf(receiver), 10000 - 50); + } + + function test_fuzz_claim_merkleProof(uint256 x) public { + vm.assume(x > 10 && x < 500); + string[] memory inputs = new string[](5); inputs[0] = "node"; inputs[1] = "src/test/scripts/generateRoot.ts"; inputs[2] = Strings.toString(x); + inputs[3] = "0"; + inputs[4] = "0x0000000000000000000000000000000000000000"; bytes memory result = vm.ffi(inputs); // revert(); @@ -322,29 +1019,185 @@ contract DropERC721Test is BaseTest { result = vm.ffi(inputs); bytes32[] memory proofs = abi.decode(result, (bytes32[])); + DropERC721.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = x; + alp.pricePerToken = 0; + alp.currency = address(0); + vm.warp(1); - address receiver = address(0x92Bb439374a091c7507bE100183d8D1Ed2c9dAD3); + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // bytes32[] memory proofs = new bytes32[](0); DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](1); conditions[0].maxClaimableSupply = x; - conditions[0].quantityLimitPerTransaction = 100; - conditions[0].waitTimeInSecondsBetweenClaims = type(uint256).max; + conditions[0].quantityLimitPerWallet = 1; conditions[0].merkleRoot = root; vm.prank(deployer); - drop.lazyMint(x, "ipfs://", ""); + drop.lazyMint(2 * x, "ipfs://", emptyEncodedBytes); vm.prank(deployer); drop.setClaimConditions(conditions, false); // vm.prank(getActor(5), getActor(5)); vm.prank(receiver, receiver); - drop.claim(receiver, x, address(0), 0, proofs, y); + drop.claim(receiver, x - 5, address(0), 0, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), x - 5); + + vm.prank(receiver, receiver); + vm.expectRevert(abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, x, x + 1)); + drop.claim(receiver, 6, address(0), 0, alp, ""); + + vm.prank(receiver, receiver); + drop.claim(receiver, 5, address(0), 0, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), x); + + vm.prank(receiver, receiver); + vm.expectRevert(abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, x, x + 5)); + drop.claim(receiver, 5, address(0), 0, alp, ""); // quantity limit already claimed + } + + /** + * note: Testing state changes; reset eligibility of claim conditions and claiming again for same condition id. + */ + function test_state_claimCondition_resetEligibility() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + DropERC721.AllowlistProof memory alp; + alp.proof = proofs; + + DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + drop.claim(receiver, 100, address(0), 0, alp, ""); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, conditions[0].quantityLimitPerWallet, 200) + ); + drop.claim(receiver, 100, address(0), 0, alp, ""); + + vm.prank(deployer); + drop.setClaimConditions(conditions, true); + + vm.prank(getActor(5), getActor(5)); + drop.claim(receiver, 100, address(0), 0, alp, ""); + } + + /*/////////////////////////////////////////////////////////////// + setClaimConditions + //////////////////////////////////////////////////////////////*/ + + function test_claimCondition_startIdAndCount() public { + vm.startPrank(deployer); + + uint256 currentStartId = 0; + uint256 count = 0; + + DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](2); + conditions[0].startTimestamp = 0; + conditions[0].maxClaimableSupply = 10; + conditions[1].startTimestamp = 1; + conditions[1].maxClaimableSupply = 10; + + drop.setClaimConditions(conditions, false); + (currentStartId, count) = drop.claimCondition(); + assertEq(currentStartId, 0); + assertEq(count, 2); + + drop.setClaimConditions(conditions, false); + (currentStartId, count) = drop.claimCondition(); + assertEq(currentStartId, 0); + assertEq(count, 2); + + drop.setClaimConditions(conditions, true); + (currentStartId, count) = drop.claimCondition(); + assertEq(currentStartId, 2); + assertEq(count, 2); + + drop.setClaimConditions(conditions, true); + (currentStartId, count) = drop.claimCondition(); + assertEq(currentStartId, 4); + assertEq(count, 2); + } + + function test_claimCondition_startPhase() public { + vm.startPrank(deployer); + + uint256 activeConditionId = 0; + + DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](3); + conditions[0].startTimestamp = 10; + conditions[0].maxClaimableSupply = 11; + conditions[0].quantityLimitPerWallet = 12; + conditions[1].startTimestamp = 20; + conditions[1].maxClaimableSupply = 21; + conditions[1].quantityLimitPerWallet = 22; + conditions[2].startTimestamp = 30; + conditions[2].maxClaimableSupply = 31; + conditions[2].quantityLimitPerWallet = 32; + drop.setClaimConditions(conditions, false); + + vm.expectRevert(abi.encodeWithSelector(Drop.DropNoActiveCondition.selector)); + drop.getActiveClaimConditionId(); + + vm.warp(10); + activeConditionId = drop.getActiveClaimConditionId(); + assertEq(activeConditionId, 0); + assertEq(drop.getClaimConditionById(activeConditionId).startTimestamp, 10); + assertEq(drop.getClaimConditionById(activeConditionId).maxClaimableSupply, 11); + assertEq(drop.getClaimConditionById(activeConditionId).quantityLimitPerWallet, 12); + + vm.warp(20); + activeConditionId = drop.getActiveClaimConditionId(); + assertEq(activeConditionId, 1); + assertEq(drop.getClaimConditionById(activeConditionId).startTimestamp, 20); + assertEq(drop.getClaimConditionById(activeConditionId).maxClaimableSupply, 21); + assertEq(drop.getClaimConditionById(activeConditionId).quantityLimitPerWallet, 22); + + vm.warp(30); + activeConditionId = drop.getActiveClaimConditionId(); + assertEq(activeConditionId, 2); + assertEq(drop.getClaimConditionById(activeConditionId).startTimestamp, 30); + assertEq(drop.getClaimConditionById(activeConditionId).maxClaimableSupply, 31); + assertEq(drop.getClaimConditionById(activeConditionId).quantityLimitPerWallet, 32); + + vm.warp(40); + assertEq(drop.getActiveClaimConditionId(), 2); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function test_delayedReveal_withNewLazyMintedEmptyBatch() public { + vm.startPrank(deployer); + + bytes memory encryptedURI = drop.encryptDecrypt("ipfs://", "key"); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", "key", block.chainid)); + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + drop.reveal(0, "key"); + + string memory uri = drop.tokenURI(1); + assertEq(uri, string(abi.encodePacked("ipfs://", "1"))); + + bytes memory newEncryptedURI = drop.encryptDecrypt("ipfs://secret", "key"); + vm.expectRevert(abi.encodeWithSelector(LazyMint.LazyMintInvalidAmount.selector)); + drop.lazyMint(0, "", abi.encode(newEncryptedURI, provenanceHash)); - vm.prank(address(4), address(4)); - vm.expectRevert("not in whitelist."); - drop.claim(receiver, x, address(0), 0, proofs, y); + vm.stopPrank(); } } diff --git a/src/test/drop/drop-erc1155/_beforeClaim/_beforeClaim.t.sol b/src/test/drop/drop-erc1155/_beforeClaim/_beforeClaim.t.sol new file mode 100644 index 000000000..8397c3195 --- /dev/null +++ b/src/test/drop/drop-erc1155/_beforeClaim/_beforeClaim.t.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC1155 } from "contracts/prebuilts/drop/DropERC1155.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "../../../utils/BaseTest.sol"; + +contract HarnessDropERC1155 is DropERC1155 { + function beforeClaim( + uint256 _tokenId, + address, + uint256 _quantity, + address, + uint256, + AllowlistProof calldata alp, + bytes memory + ) external view { + _beforeClaim(_tokenId, address(0), _quantity, address(0), 0, alp, bytes("")); + } +} + +contract DropERC1155Test_beforeClaim is BaseTest { + address public dropImp; + HarnessDropERC1155 public proxy; + + function setUp() public override { + super.setUp(); + + bytes memory initializeData = abi.encodeCall( + DropERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ); + + dropImp = address(new HarnessDropERC1155()); + proxy = HarnessDropERC1155(address(new TWProxy(dropImp, initializeData))); + + vm.prank(deployer); + proxy.setMaxTotalSupply(0, 1); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: misc. + //////////////////////////////////////////////////////////////*/ + + /** + * note: Tests whether contract reverts when a non-holder renounces a role. + */ + function test_revert_ExceedMaxSupply() public { + DropERC1155.AllowlistProof memory alp; + vm.expectRevert("exceed max total supply"); + proxy.beforeClaim(0, address(0), 2, address(0), 0, alp, bytes("")); + } + + function test_NoRevert() public view { + DropERC1155.AllowlistProof memory alp; + proxy.beforeClaim(0, address(0), 1, address(0), 0, alp, bytes("")); + } +} diff --git a/src/test/drop/drop-erc1155/_beforeClaim/_beforeClaim.tree b/src/test/drop/drop-erc1155/_beforeClaim/_beforeClaim.tree new file mode 100644 index 000000000..a5a114d07 --- /dev/null +++ b/src/test/drop/drop-erc1155/_beforeClaim/_beforeClaim.tree @@ -0,0 +1,12 @@ +function _beforeClaim( + uint256 _tokenId, + address, + uint256 _quantity, + address, + uint256, + AllowlistProof calldata, + bytes memory +) +└── when maxTotalSupply for _tokenId is not zero + └── when totalSupply of _tokenId + _quantity is greater than or equal to maxTotalSupply for _tokenId + └── it should revert ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc1155/_beforeTokenTransfer/_beforeTokenTransfer.sol b/src/test/drop/drop-erc1155/_beforeTokenTransfer/_beforeTokenTransfer.sol new file mode 100644 index 000000000..b4c32607c --- /dev/null +++ b/src/test/drop/drop-erc1155/_beforeTokenTransfer/_beforeTokenTransfer.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC1155 } from "contracts/prebuilts/drop/DropERC1155.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "../../../utils/BaseTest.sol"; + +contract HarnessDropERC1155 is DropERC1155 { + function beforeTokenTransfer( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) external { + _beforeTokenTransfer(operator, from, to, ids, amounts, data); + } +} + +contract DropERC1155Test_beforeTokenTransfer is BaseTest { + address private beforeTransfer_from = address(0x01); + address private beforeTransfer_to = address(0x01); + uint256[] private beforeTransfer_ids; + uint256[] private beforeTransfer_amounts; + bytes private beforeTransfer_data; + + address public dropImp; + HarnessDropERC1155 public proxy; + + function setUp() public override { + super.setUp(); + + bytes memory initializeData = abi.encodeCall( + DropERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ); + + dropImp = address(new HarnessDropERC1155()); + proxy = HarnessDropERC1155(address(new TWProxy(dropImp, initializeData))); + + beforeTransfer_ids = new uint256[](1); + beforeTransfer_ids[0] = 0; + beforeTransfer_amounts = new uint256[](1); + beforeTransfer_amounts[0] = 1; + beforeTransfer_data = abi.encode("", ""); + } + + modifier fromAddressZero() { + beforeTransfer_from = address(0); + _; + } + + modifier toAddressZero() { + beforeTransfer_to = address(0); + _; + } + + /** + * note: Tests whether contract reverts when a non-holder renounces a role. + */ + function test_state_transferFromZero() public fromAddressZero { + uint256 beforeTokenTotalSupply = proxy.totalSupply(0); + proxy.beforeTokenTransfer( + deployer, + beforeTransfer_from, + beforeTransfer_to, + beforeTransfer_ids, + beforeTransfer_amounts, + beforeTransfer_data + ); + uint256 afterTokenTotalSupply = proxy.totalSupply(0); + assertEq(beforeTokenTotalSupply + beforeTransfer_amounts[0], afterTokenTotalSupply); + } + + function test_state_tranferToZero() public toAddressZero { + proxy.beforeTokenTransfer( + deployer, + beforeTransfer_to, + beforeTransfer_from, + beforeTransfer_ids, + beforeTransfer_amounts, + beforeTransfer_data + ); + uint256 beforeTokenTotalSupply = proxy.totalSupply(0); + proxy.beforeTokenTransfer( + deployer, + beforeTransfer_from, + beforeTransfer_to, + beforeTransfer_ids, + beforeTransfer_amounts, + beforeTransfer_data + ); + uint256 afterTokenTotalSupply = proxy.totalSupply(0); + assertEq(beforeTokenTotalSupply - beforeTransfer_amounts[0], afterTokenTotalSupply); + } +} diff --git a/src/test/drop/drop-erc1155/_beforeTokenTransfer/_beforeTokenTransfer.tree b/src/test/drop/drop-erc1155/_beforeTokenTransfer/_beforeTokenTransfer.tree new file mode 100644 index 000000000..b5e147b76 --- /dev/null +++ b/src/test/drop/drop-erc1155/_beforeTokenTransfer/_beforeTokenTransfer.tree @@ -0,0 +1,12 @@ +function _beforeTokenTransfer( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data +) +├── when from equals to address(0) +│ └── totalSupply for each id is incremented by the corresponding amounts ✅ +└── when to equals address(0) + └── totalSupply for each id is decremented by the corresponding amounts ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc1155/_canSetFunctions/_canSetFunctions.sol b/src/test/drop/drop-erc1155/_canSetFunctions/_canSetFunctions.sol new file mode 100644 index 000000000..2be22888c --- /dev/null +++ b/src/test/drop/drop-erc1155/_canSetFunctions/_canSetFunctions.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC1155 } from "contracts/prebuilts/drop/DropERC1155.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "../../../utils/BaseTest.sol"; + +contract HarnessDropERC1155 is DropERC1155 { + function canSetPlatformFeeInfo() external view returns (bool) { + return _canSetPlatformFeeInfo(); + } + + /// @dev Checks whether primary sale recipient can be set in the given execution context. + function canSetPrimarySaleRecipient() external view returns (bool) { + return _canSetPrimarySaleRecipient(); + } + + /// @dev Checks whether owner can be set in the given execution context. + function canSetOwner() external view returns (bool) { + return _canSetOwner(); + } + + /// @dev Checks whether royalty info can be set in the given execution context. + function canSetRoyaltyInfo() external view returns (bool) { + return _canSetRoyaltyInfo(); + } + + /// @dev Checks whether contract metadata can be set in the given execution context. + function canSetContractURI() external view returns (bool) { + return _canSetContractURI(); + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function canSetClaimConditions() external view returns (bool) { + return _canSetClaimConditions(); + } + + /// @dev Returns whether lazy minting can be done in the given execution context. + function canLazyMint() external view virtual returns (bool) { + return _canLazyMint(); + } +} + +contract DropERC1155Test_canSetFunctions is BaseTest { + address public dropImp; + HarnessDropERC1155 public proxy; + + function setUp() public override { + super.setUp(); + + bytes memory initializeData = abi.encodeCall( + DropERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ); + + dropImp = address(new HarnessDropERC1155()); + proxy = HarnessDropERC1155(address(new TWProxy(dropImp, initializeData))); + } + + modifier HasDefaultAdminRole() { + vm.startPrank(deployer); + _; + } + + modifier DoesNotHaveDefaultAdminRole() { + vm.startPrank(address(0x123)); + _; + } + + modifier HasMinterRole() { + vm.startPrank(deployer); + _; + } + + modifier DoesNotHaveMinterRole() { + vm.startPrank(address(0x123)); + _; + } + + /** + * note: Tests whether contract reverts when a non-holder renounces a role. + */ + function test_canSetPlatformFeeInfo_true() public HasDefaultAdminRole { + assertTrue(proxy.canSetPlatformFeeInfo()); + } + + function test_canSetPlatformFeeInfo_false() public DoesNotHaveDefaultAdminRole { + assertFalse(proxy.canSetPlatformFeeInfo()); + } + + function test_canSetPrimarySaleRecipient_true() public HasDefaultAdminRole { + assertTrue(proxy.canSetPrimarySaleRecipient()); + } + + function test_canSetPrimarySaleRecipient_false() public DoesNotHaveDefaultAdminRole { + assertFalse(proxy.canSetPrimarySaleRecipient()); + } + + function test_canSetOwner_true() public HasDefaultAdminRole { + assertTrue(proxy.canSetOwner()); + } + + function test_canSetOwner_false() public DoesNotHaveDefaultAdminRole { + assertFalse(proxy.canSetOwner()); + } + + function test_canSetRoyaltyInfo_true() public HasDefaultAdminRole { + assertTrue(proxy.canSetRoyaltyInfo()); + } + + function test_canSetRoyaltyInfo_false() public DoesNotHaveDefaultAdminRole { + assertFalse(proxy.canSetRoyaltyInfo()); + } + + function test_canSetContractURI_true() public HasDefaultAdminRole { + assertTrue(proxy.canSetContractURI()); + } + + function test_canSetContractURI_false() public DoesNotHaveDefaultAdminRole { + assertFalse(proxy.canSetContractURI()); + } + + function test_canSetClaimConditions_true() public HasDefaultAdminRole { + assertTrue(proxy.canSetClaimConditions()); + } + + function test_canSetClaimConditions_false() public DoesNotHaveDefaultAdminRole { + assertFalse(proxy.canSetClaimConditions()); + } + + function test_canLazyMint_true() public HasMinterRole { + assertTrue(proxy.canLazyMint()); + } + + function test_canLazyMint_false() public DoesNotHaveMinterRole { + assertFalse(proxy.canLazyMint()); + } +} diff --git a/src/test/drop/drop-erc1155/_canSetFunctions/_canSetFunctions.tree b/src/test/drop/drop-erc1155/_canSetFunctions/_canSetFunctions.tree new file mode 100644 index 000000000..4b214c4e9 --- /dev/null +++ b/src/test/drop/drop-erc1155/_canSetFunctions/_canSetFunctions.tree @@ -0,0 +1,41 @@ +function _canSetPlatformFeeInfo() +├── when caller has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when caller does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + +function _canSetPrimarySaleRecipient() +├── when caller has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when caller does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + +function _canSetOwner() +├── when caller has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when caller does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + +function _canSetRoyaltyInfo() +├── when caller has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when caller does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + +function _canSetContractURI() +├── when caller has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when caller does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + +function _canSetClaimConditions() +├── when caller has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when caller does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + +function _canLazyMint() +├── when caller has minterRole +│ └── it should return true ✅ +└── when caller does not have minterRole + └── it should return false ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc1155/burnBatch/burnBatch.t.sol b/src/test/drop/drop-erc1155/burnBatch/burnBatch.t.sol new file mode 100644 index 000000000..86e365f33 --- /dev/null +++ b/src/test/drop/drop-erc1155/burnBatch/burnBatch.t.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC1155 } from "contracts/prebuilts/drop/DropERC1155.sol"; + +// Test imports +import "../../../utils/BaseTest.sol"; +import "../../../../../lib/openzeppelin-contracts-upgradeable/contracts/interfaces/IERC2981Upgradeable.sol"; + +contract DropERC1155Test_burnBatch is BaseTest { + DropERC1155 public drop; + + address private unauthorized = address(0x999); + address private account; + uint256[] private ids; + uint256[] private values; + + address private receiver; + bytes private emptyEncodedBytes = abi.encode("", ""); + + event TransferBatch( + address indexed operator, + address indexed from, + address indexed to, + uint256[] ids, + uint256[] values + ); + + function setUp() public override { + super.setUp(); + drop = DropERC1155(getContract("DropERC1155")); + + ids = new uint256[](1); + values = new uint256[](1); + ids[0] = 0; + values[0] = 1; + } + + /*/////////////////////////////////////////////////////////////// + Branch Testing + //////////////////////////////////////////////////////////////*/ + + modifier callerNotApproved() { + vm.startPrank(unauthorized); + _; + } + + modifier callerOwner() { + receiver = getActor(0); + vm.startPrank(receiver); + _; + } + + modifier callerApproved() { + receiver = getActor(0); + vm.prank(receiver); + drop.setApprovalForAll(deployer, true); + vm.startPrank(deployer); + _; + } + + modifier IdValueMismatch() { + values = new uint256[](2); + values[0] = 1; + values[1] = 1; + _; + } + + modifier tokenClaimed() { + vm.warp(1); + + uint256 _tokenId = 0; + receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + DropERC1155.AllowlistProof memory alp; + alp.proof = proofs; + + DropERC1155.ClaimCondition[] memory conditions = new DropERC1155.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(_tokenId, conditions, false); + + vm.prank(getActor(5), getActor(5)); + drop.claim(receiver, _tokenId, 1, address(0), 0, alp, ""); + + _; + } + + function test_revert_callerNotApproved() public tokenClaimed callerNotApproved { + vm.expectRevert("ERC1155: caller is not owner nor approved."); + drop.burnBatch(receiver, ids, values); + } + + function test_state_callerApproved() public tokenClaimed callerApproved { + uint256 beforeBalance = drop.balanceOf(receiver, ids[0]); + drop.burnBatch(receiver, ids, values); + uint256 afterBalance = drop.balanceOf(receiver, ids[0]); + assertEq(beforeBalance - values[0], afterBalance); + } + + function test_state_callerOwner() public tokenClaimed callerOwner { + uint256 beforeBalance = drop.balanceOf(receiver, ids[0]); + drop.burnBatch(receiver, ids, values); + uint256 afterBalance = drop.balanceOf(receiver, ids[0]); + assertEq(beforeBalance - values[0], afterBalance); + } + + function test_revert_IdValueMismatch() public tokenClaimed IdValueMismatch callerOwner { + vm.expectRevert("ERC1155: ids and amounts length mismatch"); + drop.burnBatch(receiver, ids, values); + } + + function test_revert_balanceUnderflow() public tokenClaimed callerOwner { + values[0] = 2; + vm.expectRevert(); + drop.burnBatch(receiver, ids, values); + } + + function test_event() public tokenClaimed callerOwner { + vm.expectEmit(true, true, true, true); + emit TransferBatch(receiver, receiver, address(0), ids, values); + drop.burnBatch(receiver, ids, values); + } +} diff --git a/src/test/drop/drop-erc1155/burnBatch/burnBatch.tree b/src/test/drop/drop-erc1155/burnBatch/burnBatch.tree new file mode 100644 index 000000000..e8685085e --- /dev/null +++ b/src/test/drop/drop-erc1155/burnBatch/burnBatch.tree @@ -0,0 +1,17 @@ +function burnBatch( + address account, + uint256[] memory ids, + uint256[] memory values +) +├── when account does not equal _msgSender() and _msgSender() is not an approved operator for account +│ └── it should revert ✅ +└── when account is equal to _msgSender() or _msgSender() is an approved operator for account + ├── when ids and values are not the same length + │ └── it should revert ✅ + └── when ids and values are the same length + ├── when the balance of account for each id is not greater than or equal to the corresponding value + │ └── it should revert ✅ + └── when the balance of account for each id is greater than or equal to the corresponding value + ├── it should reduce the balance of each id for account by the corresponding value ✅ + ├── it should reduce the total supply of each id by the corresponding value ✅ + └── it should emit TransferBatch with the following parameters: _msgSender(), account, address(0), ids, amounts ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc1155/collectPriceOnClaim/collectPriceOnClaim.t.sol b/src/test/drop/drop-erc1155/collectPriceOnClaim/collectPriceOnClaim.t.sol new file mode 100644 index 000000000..fd9095fe3 --- /dev/null +++ b/src/test/drop/drop-erc1155/collectPriceOnClaim/collectPriceOnClaim.t.sol @@ -0,0 +1,304 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC1155 } from "contracts/prebuilts/drop/DropERC1155.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "../../../utils/BaseTest.sol"; + +contract HarnessDropERC1155 is DropERC1155 { + function collectPriceOnClaimHarness( + uint256 _tokenId, + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) public payable { + collectPriceOnClaim(_tokenId, _primarySaleRecipient, _quantityToClaim, _currency, _pricePerToken); + } +} + +contract DropERC1155Test_collectPrice is BaseTest { + address private collectPrice_saleRecipient = address(0x010); + address private collectPrice_royaltyRecipient = address(0x011); + uint128 private collectPrice_royaltyBps = 1000; + uint128 private collectPrice_platformFeeBps = 1000; + address private collectPrice_platformFeeRecipient = address(0x012); + uint256 private collectPrice_quantityToClaim = 1; + uint256 private collectPrice_pricePerToken; + address private collectPrice_currency; + uint256 private collectPrice_msgValue; + address private collectPrice_tokenSaleRecipient = address(0x111); + address private defaultFeeRecipient; + + address public dropImp; + HarnessDropERC1155 public proxy; + + function setUp() public override { + super.setUp(); + + bytes memory initializeData = abi.encodeCall( + DropERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ); + + dropImp = address(new HarnessDropERC1155()); + proxy = HarnessDropERC1155(address(new TWProxy(dropImp, initializeData))); + defaultFeeRecipient = proxy.DEFAULT_FEE_RECIPIENT(); + } + + modifier pricePerTokenZero() { + collectPrice_pricePerToken = 0; + _; + } + + modifier pricePerTokenNotZero() { + collectPrice_pricePerToken = 1 ether; + _; + } + + modifier msgValueNotZero() { + collectPrice_msgValue = 1 ether; + _; + } + + modifier nativeCurrency() { + collectPrice_currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + _; + } + + modifier erc20Currency() { + collectPrice_currency = address(erc20); + erc20.mint(address(this), 1_000 ether); + _; + } + + modifier primarySaleRecipientZeroAddress() { + saleRecipient = address(0); + _; + } + + modifier primarySaleRecipientNotZeroAddress() { + saleRecipient = address(0x112); + _; + } + + modifier saleRecipientSet() { + vm.prank(deployer); + proxy.setSaleRecipientForToken(0, address(0x111)); + _; + } + + /*/////////////////////////////////////////////////////////////// + Branch Testing + //////////////////////////////////////////////////////////////*/ + + function test_revert_msgValueNotZero() public nativeCurrency msgValueNotZero pricePerTokenZero { + vm.expectRevert(); + proxy.collectPriceOnClaimHarness{ value: collectPrice_msgValue }( + 0, + saleRecipient, + collectPrice_quantityToClaim, + collectPrice_currency, + collectPrice_pricePerToken + ); + } + + function test_msgValueZero_return() public nativeCurrency pricePerTokenZero { + proxy.collectPriceOnClaimHarness{ value: collectPrice_msgValue }( + 0, + saleRecipient, + collectPrice_quantityToClaim, + collectPrice_currency, + collectPrice_pricePerToken + ); + } + + function test_revert_priceValueMismatchNativeCurrency() public nativeCurrency pricePerTokenNotZero { + vm.expectRevert(); + proxy.collectPriceOnClaimHarness{ value: collectPrice_msgValue }( + 0, + saleRecipient, + collectPrice_quantityToClaim, + collectPrice_currency, + collectPrice_pricePerToken + ); + } + + function test_transferNativeCurrencyToSaleRecipient() public nativeCurrency pricePerTokenNotZero msgValueNotZero { + uint256 balanceSaleRecipientBefore = address(saleRecipient).balance; + uint256 defaultFeeRecipientBefore = address(defaultFeeRecipient).balance; + uint256 platformFeeRecipientBefore = address(platformFeeRecipient).balance; + proxy.collectPriceOnClaimHarness{ value: collectPrice_msgValue }( + 0, + saleRecipient, + collectPrice_quantityToClaim, + collectPrice_currency, + collectPrice_pricePerToken + ); + + uint256 balanceSaleRecipientAfter = address(saleRecipient).balance; + uint256 defaultFeeRecipientAfter = address(defaultFeeRecipient).balance; + uint256 platformFeeRecipientAfter = address(platformFeeRecipient).balance; + uint256 expectedDefaultPlatformFee = (collectPrice_pricePerToken * 100) / MAX_BPS; + uint256 expectedPlatformFee = (collectPrice_pricePerToken * platformFeeBps) / MAX_BPS; + uint256 expectedSaleRecipientProceed = collectPrice_msgValue - expectedPlatformFee - expectedDefaultPlatformFee; + + assertEq(balanceSaleRecipientAfter - balanceSaleRecipientBefore, expectedSaleRecipientProceed); + assertEq(platformFeeRecipientAfter - platformFeeRecipientBefore, expectedPlatformFee); + assertEq(defaultFeeRecipientAfter - defaultFeeRecipientBefore, expectedDefaultPlatformFee); + } + + function test_transferERC20ToSaleRecipient() public erc20Currency pricePerTokenNotZero { + uint256 balanceSaleRecipientBefore = erc20.balanceOf(saleRecipient); + uint256 defaultFeeRecipientBefore = erc20.balanceOf(defaultFeeRecipient); + uint256 platformFeeRecipientBefore = erc20.balanceOf(platformFeeRecipient); + erc20.approve(address(proxy), collectPrice_pricePerToken); + proxy.collectPriceOnClaimHarness( + 0, + saleRecipient, + collectPrice_quantityToClaim, + collectPrice_currency, + collectPrice_pricePerToken + ); + + uint256 balanceSaleRecipientAfter = erc20.balanceOf(saleRecipient); + uint256 defaultFeeRecipientAfter = erc20.balanceOf(defaultFeeRecipient); + uint256 platformFeeRecipientAfter = erc20.balanceOf(platformFeeRecipient); + uint256 expectedDefaultPlatformFee = (collectPrice_pricePerToken * 100) / MAX_BPS; + uint256 expectedPlatformFee = (collectPrice_pricePerToken * platformFeeBps) / MAX_BPS; + uint256 expectedSaleRecipientProceed = collectPrice_pricePerToken - + expectedPlatformFee - + expectedDefaultPlatformFee; + + assertEq(balanceSaleRecipientAfter - balanceSaleRecipientBefore, expectedSaleRecipientProceed); + assertEq(platformFeeRecipientAfter - platformFeeRecipientBefore, expectedPlatformFee); + assertEq(defaultFeeRecipientAfter - defaultFeeRecipientBefore, expectedDefaultPlatformFee); + } + + function test_transferNativeCurrencyToTokenIdSaleRecipient() + public + nativeCurrency + pricePerTokenNotZero + msgValueNotZero + saleRecipientSet + primarySaleRecipientZeroAddress + { + uint256 balanceSaleRecipientBefore = address(collectPrice_tokenSaleRecipient).balance; + uint256 defaultFeeRecipientBefore = address(defaultFeeRecipient).balance; + uint256 platformFeeRecipientBefore = address(platformFeeRecipient).balance; + proxy.collectPriceOnClaimHarness{ value: collectPrice_msgValue }( + 0, + address(0), + collectPrice_quantityToClaim, + collectPrice_currency, + collectPrice_pricePerToken + ); + + uint256 balanceSaleRecipientAfter = address(collectPrice_tokenSaleRecipient).balance; + uint256 defaultFeeRecipientAfter = address(defaultFeeRecipient).balance; + uint256 platformFeeRecipientAfter = address(platformFeeRecipient).balance; + uint256 expectedPlatformFee = (collectPrice_pricePerToken * platformFeeBps) / MAX_BPS; + uint256 expectedDefaultPlatformFee = (collectPrice_pricePerToken * 100) / MAX_BPS; + uint256 expectedSaleRecipientProceed = collectPrice_msgValue - expectedPlatformFee - expectedDefaultPlatformFee; + + assertEq(balanceSaleRecipientAfter - balanceSaleRecipientBefore, expectedSaleRecipientProceed); + assertEq(platformFeeRecipientAfter - platformFeeRecipientBefore, expectedPlatformFee); + assertEq(defaultFeeRecipientAfter - defaultFeeRecipientBefore, expectedDefaultPlatformFee); + } + + function test_transferERC20ToTokenIdSaleRecipient() public erc20Currency pricePerTokenNotZero saleRecipientSet { + uint256 balanceSaleRecipientBefore = erc20.balanceOf(collectPrice_tokenSaleRecipient); + uint256 defaultFeeRecipientBefore = erc20.balanceOf(defaultFeeRecipient); + uint256 platformFeeRecipientBefore = erc20.balanceOf(platformFeeRecipient); + erc20.approve(address(proxy), collectPrice_pricePerToken); + proxy.collectPriceOnClaimHarness( + 0, + address(0), + collectPrice_quantityToClaim, + collectPrice_currency, + collectPrice_pricePerToken + ); + + uint256 balanceSaleRecipientAfter = erc20.balanceOf(collectPrice_tokenSaleRecipient); + uint256 defaultFeeRecipientAfter = erc20.balanceOf(defaultFeeRecipient); + uint256 platformFeeRecipientAfter = erc20.balanceOf(platformFeeRecipient); + uint256 expectedPlatformFee = (collectPrice_pricePerToken * platformFeeBps) / MAX_BPS; + uint256 expectedDefaultPlatformFee = (collectPrice_pricePerToken * 100) / MAX_BPS; + uint256 expectedSaleRecipientProceed = collectPrice_pricePerToken - + expectedPlatformFee - + expectedDefaultPlatformFee; + + assertEq(balanceSaleRecipientAfter - balanceSaleRecipientBefore, expectedSaleRecipientProceed); + assertEq(platformFeeRecipientAfter - platformFeeRecipientBefore, expectedPlatformFee); + assertEq(defaultFeeRecipientAfter - defaultFeeRecipientBefore, expectedDefaultPlatformFee); + } + + function test_transferNativeCurrencyToPrimarySaleRecipient() + public + nativeCurrency + pricePerTokenNotZero + msgValueNotZero + { + uint256 balanceSaleRecipientBefore = address(saleRecipient).balance; + uint256 balanceDefaultFeeRecipientBefore = address(defaultFeeRecipient).balance; + uint256 platformFeeRecipientBefore = address(platformFeeRecipient).balance; + proxy.collectPriceOnClaimHarness{ value: collectPrice_msgValue }( + 0, + address(0), + collectPrice_quantityToClaim, + collectPrice_currency, + collectPrice_pricePerToken + ); + + uint256 balanceSaleRecipientAfter = address(saleRecipient).balance; + uint256 balanceDefaultFeeRecipientAfter = address(defaultFeeRecipient).balance; + uint256 platformFeeRecipientAfter = address(platformFeeRecipient).balance; + uint256 expectedDefaultPlatformFee = (collectPrice_pricePerToken * 100) / MAX_BPS; + uint256 expectedPlatformFee = (collectPrice_pricePerToken * platformFeeBps) / MAX_BPS; + uint256 expectedSaleRecipientProceed = collectPrice_msgValue - expectedPlatformFee - expectedDefaultPlatformFee; + + assertEq(balanceSaleRecipientAfter - balanceSaleRecipientBefore, expectedSaleRecipientProceed); + assertEq(platformFeeRecipientAfter - platformFeeRecipientBefore, expectedPlatformFee); + assertEq(balanceDefaultFeeRecipientAfter - balanceDefaultFeeRecipientBefore, expectedDefaultPlatformFee); + } + + function test_transferERC20ToPrimarySaleRecipient() public erc20Currency pricePerTokenNotZero { + uint256 balanceSaleRecipientBefore = erc20.balanceOf(saleRecipient); + uint256 defaultFeeRecipientBefore = erc20.balanceOf(defaultFeeRecipient); + uint256 platformFeeRecipientBefore = erc20.balanceOf(platformFeeRecipient); + erc20.approve(address(proxy), collectPrice_pricePerToken); + proxy.collectPriceOnClaimHarness( + 0, + address(0), + collectPrice_quantityToClaim, + collectPrice_currency, + collectPrice_pricePerToken + ); + + uint256 balanceSaleRecipientAfter = erc20.balanceOf(saleRecipient); + uint256 defaultFeeRecipientAfter = erc20.balanceOf(defaultFeeRecipient); + uint256 platformFeeRecipientAfter = erc20.balanceOf(platformFeeRecipient); + uint256 expectedDefaultPlatformFee = (collectPrice_pricePerToken * 100) / MAX_BPS; + uint256 expectedPlatformFee = (collectPrice_pricePerToken * platformFeeBps) / MAX_BPS; + uint256 expectedSaleRecipientProceed = collectPrice_pricePerToken - + expectedPlatformFee - + expectedDefaultPlatformFee; + + assertEq(balanceSaleRecipientAfter - balanceSaleRecipientBefore, expectedSaleRecipientProceed); + assertEq(defaultFeeRecipientAfter - defaultFeeRecipientBefore, expectedDefaultPlatformFee); + assertEq(platformFeeRecipientAfter - platformFeeRecipientBefore, expectedPlatformFee); + } +} diff --git a/src/test/drop/drop-erc1155/collectPriceOnClaim/collectPriceOnClaim.tree b/src/test/drop/drop-erc1155/collectPriceOnClaim/collectPriceOnClaim.tree new file mode 100644 index 000000000..6cb5cd180 --- /dev/null +++ b/src/test/drop/drop-erc1155/collectPriceOnClaim/collectPriceOnClaim.tree @@ -0,0 +1,44 @@ +function collectPriceOnClaim( + uint256 _tokenId, + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) +├── when _pricePerToken is equal to zero +│ ├── when msg.value does not equal to zero +│ │ └── it should revert ✅ +│ └── when msg.value is equal to zero +│ └── it should return ✅ +└── when _pricePerToken is not equal to zero + ├── when _primarySaleRecipient is equal to address(0) + │ ├── when saleRecipient for _tokenId is equal to address(0) + │ │ ├── when currency is native token + │ │ │ ├── when msg.value does not equal totalPrice + │ │ │ │ └── it should revert ✅ + │ │ │ └── when msg.value does equal totalPrice + │ │ │ ├── it should transfer platformFees to platformFeeRecipient in native token ✅ + │ │ │ └── it should transfer totalPrice - platformFees to primarySaleRecipient in native token ✅ + │ │ └── when currency is not native token + │ │ ├── it should transfer platformFees to platformFeeRecipient in _currency token ✅ + │ │ └── it should transfer totalPrice - platformFees to primarySaleRecipient in _currency token ✅ + │ └── when salerecipient for _tokenId is not equal to address(0) + │ ├── when currency is native token + │ │ ├── when msg.value does not equal totalPrice + │ │ │ └── it should revert ✅ + │ │ └── when msg.value does equal totalPrice + │ │ ├── it should transfer platformFees to platformFeeRecipient in native token ✅ + │ │ └── it should transfer totalPrice - platformFees to saleRecipient for _tokenId in native token ✅ + │ └── when currency is not native token + │ ├── it should transfer platformFees to platformFeeRecipient in _currency token ✅ + │ └── it should transfer totalPrice - platformFees to saleRecipient for _tokenId in _currency token ✅ + └── when _primarySaleRecipient is not equal to address(0) + ├── when currency is native token + │ ├── when msg.value does not equal totalPrice + │ │ └── it should revert ✅ + │ └── when msg.value does equal totalPrice + │ ├── it should transfer platformFees to platformFeeRecipient in native token ✅ + │ └── it should transfer totalPrice - platformFees to _primarySaleRecipient in native token ✅ + └── when currency is not native token + ├── it should transfer platformFees to platformFeeRecipient in _currency token ✅ + └── it should transfer totalPrice - platformFees to _primarySaleRecipient in _currency token ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc1155/freezeBatchBaseURI/freezeBatchBaseURI.t.sol b/src/test/drop/drop-erc1155/freezeBatchBaseURI/freezeBatchBaseURI.t.sol new file mode 100644 index 000000000..774669b51 --- /dev/null +++ b/src/test/drop/drop-erc1155/freezeBatchBaseURI/freezeBatchBaseURI.t.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC1155, BatchMintMetadata } from "contracts/prebuilts/drop/DropERC1155.sol"; + +// Test imports + +import "../../../utils/BaseTest.sol"; +import "../../../../../lib/openzeppelin-contracts-upgradeable/contracts/interfaces/IERC2981Upgradeable.sol"; + +contract DropERC1155Test_freezeBatchBaseURI is BaseTest { + event MetadataFrozen(); + + DropERC1155 public drop; + + address private unauthorized = address(0x123); + + bytes private emptyEncodedBytes = abi.encode("", ""); + + function setUp() public override { + super.setUp(); + drop = DropERC1155(getContract("DropERC1155")); + } + + /*/////////////////////////////////////////////////////////////// + Branch Testing + //////////////////////////////////////////////////////////////*/ + + modifier callerWithoutMetadataRole() { + vm.startPrank(unauthorized); + _; + } + + modifier callerWithMetadataRole() { + vm.startPrank(deployer); + _; + } + + modifier lazyMint() { + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + _; + } + + modifier lazyMintEmptyUri() { + vm.prank(deployer); + drop.lazyMint(100, "", emptyEncodedBytes); + _; + } + + function test_revert_NoMetadataRole() public lazyMint callerWithoutMetadataRole { + bytes32 role = keccak256("METADATA_ROLE"); + vm.expectRevert( + abi.encodeWithSelector(Permissions.PermissionsUnauthorizedAccount.selector, unauthorized, role) + ); + drop.freezeBatchBaseURI(0); + } + + function test_revert_IndexTooHigh() public lazyMint callerWithMetadataRole { + vm.expectRevert(abi.encodeWithSelector(BatchMintMetadata.BatchMintInvalidBatchId.selector, 1)); + drop.freezeBatchBaseURI(1); + } + + function test_revert_EmptyBaseURI() public lazyMintEmptyUri callerWithMetadataRole { + vm.expectRevert( + abi.encodeWithSelector(BatchMintMetadata.BatchMintInvalidBatchId.selector, drop.getBatchIdAtIndex(0)) + ); + drop.freezeBatchBaseURI(0); + } + + function test_state() public lazyMint callerWithMetadataRole { + uint256 batchId = drop.getBatchIdAtIndex(0); + drop.freezeBatchBaseURI(0); + assertEq(drop.batchFrozen(batchId), true); + } + + function test_event() public lazyMint callerWithMetadataRole { + vm.expectEmit(false, false, false, false); + emit MetadataFrozen(); + drop.freezeBatchBaseURI(0); + } +} diff --git a/src/test/drop/drop-erc1155/freezeBatchBaseURI/freezeBatchBaseURI.tree b/src/test/drop/drop-erc1155/freezeBatchBaseURI/freezeBatchBaseURI.tree new file mode 100644 index 000000000..7a9a00b7f --- /dev/null +++ b/src/test/drop/drop-erc1155/freezeBatchBaseURI/freezeBatchBaseURI.tree @@ -0,0 +1,12 @@ +function freezeBatchBaseURI(uint256 _index) +├── when the caller does not have metadataRole +│ └── it should revert ✅ +└── when the caller has metadataRole + ├── when _index is greater than the number of current batches + │ └── it should revert ✅ + └── when _index is equal to or less than the number of current batches + ├── when the baseURI for the batch at _index is not set + │ └── it should revert ✅ + └── when the baseURI for the batch at _index is set + ├── it should set batchFrozen[(batchId for _index)] to true ✅ + └── it should emit MetadataFrozen ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc1155/initialize/initialize.t.sol b/src/test/drop/drop-erc1155/initialize/initialize.t.sol new file mode 100644 index 000000000..4f242b9e9 --- /dev/null +++ b/src/test/drop/drop-erc1155/initialize/initialize.t.sol @@ -0,0 +1,407 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC1155, Royalty, PlatformFee } from "contracts/prebuilts/drop/DropERC1155.sol"; + +// Test imports +import "../../../utils/BaseTest.sol"; + +contract DropERC1155Test_initializer is BaseTest { + DropERC1155 public newDropContract; + + event ContractURIUpdated(string prevURI, string newURI); + event OwnerUpdated(address indexed prevOwner, address indexed newOwner); + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole); + event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps); + event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps); + event PrimarySaleRecipientUpdated(address indexed recipient); + + function setUp() public override { + super.setUp(); + } + + modifier royaltyBPSTooHigh() { + uint128 royaltyBps = 10001; + _; + } + + modifier platformFeeBPSTooHigh() { + uint128 platformFeeBps = 10001; + _; + } + + function test_state() public { + deployContractProxy( + "DropERC1155", + abi.encodeCall( + DropERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + + newDropContract = DropERC1155(getContract("DropERC1155")); + (address _platformFeeRecipient, uint128 _platformFeeBps) = newDropContract.getPlatformFeeInfo(); + (address _royaltyRecipient, uint128 _royaltyBps) = newDropContract.getDefaultRoyaltyInfo(); + address _saleRecipient = newDropContract.primarySaleRecipient(); + + for (uint256 i = 0; i < forwarders().length; i++) { + assertEq(newDropContract.isTrustedForwarder(forwarders()[i]), true); + } + + assertEq(newDropContract.name(), NAME); + assertEq(newDropContract.symbol(), SYMBOL); + assertEq(newDropContract.contractURI(), CONTRACT_URI); + assertEq(newDropContract.owner(), deployer); + assertEq(_platformFeeRecipient, platformFeeRecipient); + assertEq(_platformFeeBps, platformFeeBps); + assertEq(_royaltyRecipient, royaltyRecipient); + assertEq(_royaltyBps, royaltyBps); + assertEq(_saleRecipient, saleRecipient); + } + + function test_revert_RoyaltyBPSTooHigh() public royaltyBPSTooHigh { + vm.expectRevert(abi.encodeWithSelector(Royalty.RoyaltyExceededMaxFeeBps.selector, 10_000, 10_001)); + deployContractProxy( + "DropERC1155", + abi.encodeCall( + DropERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + 10001, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + } + + function test_revert_PlatformFeeBPSTooHigh() public platformFeeBPSTooHigh { + vm.expectRevert(abi.encodeWithSelector(PlatformFee.PlatformFeeExceededMaxFeeBps.selector, 10_000, 10_001)); + deployContractProxy( + "DropERC1155", + abi.encodeCall( + DropERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + 10001, + platformFeeRecipient + ) + ) + ); + } + + function test_event_ContractURIUpdated() public { + vm.expectEmit(false, false, false, true); + emit ContractURIUpdated("", CONTRACT_URI); + deployContractProxy( + "DropERC1155", + abi.encodeCall( + DropERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + } + + function test_event_OwnerUpdated() public { + vm.expectEmit(true, true, false, false); + emit OwnerUpdated(address(0), deployer); + deployContractProxy( + "DropERC1155", + abi.encodeCall( + DropERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + } + + function test_event_RoleGrantedDefaultAdminRole() public { + bytes32 role = bytes32(0x00); + vm.expectEmit(true, true, true, false); + emit RoleGranted(role, deployer, factory); + deployContractProxy( + "DropERC1155", + abi.encodeCall( + DropERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + } + + function test_event_RoleGrantedMinterRole() public { + bytes32 role = keccak256("MINTER_ROLE"); + vm.expectEmit(true, true, true, false); + emit RoleGranted(role, deployer, factory); + deployContractProxy( + "DropERC1155", + abi.encodeCall( + DropERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + } + + function test_event_RoleGrantedTransferRole() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + vm.expectEmit(true, true, true, false); + emit RoleGranted(role, deployer, factory); + deployContractProxy( + "DropERC1155", + abi.encodeCall( + DropERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + } + + function test_event_RoleGrantedTransferRoleZeroAddress() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + vm.expectEmit(true, true, true, false); + emit RoleGranted(role, address(0), factory); + deployContractProxy( + "DropERC1155", + abi.encodeCall( + DropERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + } + + function test_event_RoleGrantedMetadataRole() public { + bytes32 role = keccak256("METADATA_ROLE"); + vm.expectEmit(true, true, true, false); + emit RoleGranted(role, deployer, factory); + deployContractProxy( + "DropERC1155", + abi.encodeCall( + DropERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + } + + function test_event_RoleAdminChangedMetadataRole() public { + bytes32 role = keccak256("METADATA_ROLE"); + vm.expectEmit(true, true, true, false); + emit RoleAdminChanged(role, bytes32(0x00), role); + deployContractProxy( + "DropERC1155", + abi.encodeCall( + DropERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + } + + function test_event_PlatformFeeInfoUpdated() public { + vm.expectEmit(true, false, false, true); + emit PlatformFeeInfoUpdated(platformFeeRecipient, platformFeeBps); + deployContractProxy( + "DropERC1155", + abi.encodeCall( + DropERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + } + + function test_event_DefaultRoyalty() public { + vm.expectEmit(true, false, false, true); + emit DefaultRoyalty(royaltyRecipient, royaltyBps); + deployContractProxy( + "DropERC1155", + abi.encodeCall( + DropERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + } + + function test_event_PrimarySaleRecipientUpdated() public { + vm.expectEmit(true, false, false, false); + emit PrimarySaleRecipientUpdated(saleRecipient); + deployContractProxy( + "DropERC1155", + abi.encodeCall( + DropERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + } + + function test_roleCheck() public { + deployContractProxy( + "DropERC1155", + abi.encodeCall( + DropERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + + newDropContract = DropERC1155(getContract("DropERC1155")); + + assertEq(newDropContract.hasRole(bytes32(0x00), deployer), true); + assertEq(newDropContract.hasRole(keccak256("MINTER_ROLE"), deployer), true); + assertEq(newDropContract.hasRole(keccak256("TRANSFER_ROLE"), deployer), true); + assertEq(newDropContract.hasRole(keccak256("TRANSFER_ROLE"), address(0)), true); + assertEq(newDropContract.hasRole(keccak256("METADATA_ROLE"), deployer), true); + + assertEq(newDropContract.getRoleAdmin(keccak256("METADATA_ROLE")), keccak256("METADATA_ROLE")); + } +} diff --git a/src/test/drop/drop-erc1155/initialize/initialize.tree b/src/test/drop/drop-erc1155/initialize/initialize.tree new file mode 100644 index 000000000..76c8d15d0 --- /dev/null +++ b/src/test/drop/drop-erc1155/initialize/initialize.tree @@ -0,0 +1,50 @@ +function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _royaltyRecipient, + uint128 _royaltyBps, + uint128 _platformFeeBps, + address _platformFeeRecipient +) +├── when _trustedForwarders.length > 0 +│ └── it should set _trustedForwarder[_trustedForwarders[i]] as true for each address in _trustedForwarders ✅ +├── it should set _uri to an empty string ✅ +├── it should set contractURI as _contractURI ✅ +├── it should emit ContractURIUpdated with the parameters: prevURI, _uri ✅ +├── it should set _defaultAdmin as the owner of the contract ✅ +├── it should emit OwnerUpdated with the parameters: _prevOwner, _defaultAdmin ✅ +├── it should assign the role DEFAULT_ADMIN_ROLE to _defaultAdmin ✅ +├── it should emit RoleGranted with the parameters: DEFAULT_ADMIN_ROLE, _defaultAdmin, msg.sender ✅ +├── it should assign the role _minterRole to _defaultAdmin ✅ +├── it should emit RoleGranted with the parameters: _minterRole, _defaultAdmin, msg.sender ✅ +├── it should assign the role _transferRole to _defaultAdmin ✅ +├── it should emit RoleGranted with the parameters: _transferRole, _defaultAdmin, msg.sender ✅ +├── it should assign the role _transferRole to address(0) ✅ +├── it should emit RoleGranted with the parameters: _transferRole, address(0), msg.sender ✅ +├── it should assign the role _metadataRole to _defaultAdmin ✅ +├── it should emit RoleGranted with the parameters: _metadataRole, _defaultAdmin, msg.sender ✅ +├── it should set _getAdminRole[_metadataRole] to equal _metadataRole ✅ +├── it should emit RoleAdminChanged with the parameters _metadataRole, previousAdminRole, _metadataRole ✅ +├── when _platformFeeBps is greater than 10_000 +│ └── it should revert ✅ +├── when _platformFeeBps is less than or equal to 10_000 +│ ├── it should set platformFeeBps to uint16(_platformFeeBps); ✅ +│ ├── it should set platformFeeRecipient to _platformFeeRecipient ✅ +│ └── it should emit PlatformFeeInfoUpdated with the following parameters: _platformFeeRecipient, _platformFeeBps ✅ +├── when _royaltyBps is greater than 10_000 +│ └── it should revert ✅ +├── when _royaltyBps is less than or equal to 10_000 +│ ├── it should set royaltyRecipient as _royaltyRecipient ✅ +│ ├── it should set royaltyBps as uint16(_royaltyBps) ✅ +│ └── it should emit DefaultRoyalty with the parameters _royaltyRecipient, _royaltyBps ✅ +├── it should set recipient as _primarySaleRecipient ✅ +├── it should emit PrimarySaleRecipientUpdated with the parameters _primarySaleRecipient ✅ +├── it should set transferRole as keccak256("TRANSFER_ROLE") ✅ +├── it should set minterRole as keccak256("MINTER_ROLE") ✅ +├── it should set metadataRole as keccak256("METADATA_ROLE") ✅ +├── it should set name as _name ✅ +└── it should set symbol as _symbol ✅ diff --git a/src/test/drop/drop-erc1155/miscellaneous/miscellaneous.t.sol b/src/test/drop/drop-erc1155/miscellaneous/miscellaneous.t.sol new file mode 100644 index 000000000..661c0e3db --- /dev/null +++ b/src/test/drop/drop-erc1155/miscellaneous/miscellaneous.t.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC1155 } from "contracts/prebuilts/drop/DropERC1155.sol"; + +// Test imports + +import "../../../utils/BaseTest.sol"; +import "../../../../../lib/openzeppelin-contracts-upgradeable/contracts/interfaces/IERC2981Upgradeable.sol"; +import "../../../../../lib/openzeppelin-contracts-upgradeable/contracts/interfaces/IERC1155Upgradeable.sol"; +import "../../../../../lib/openzeppelin-contracts-upgradeable/contracts/interfaces/IERC1155MetadataURIUpgradeable.sol"; + +contract DropERC1155Test_misc is BaseTest { + DropERC1155 public drop; + + bytes private emptyEncodedBytes = abi.encode("", ""); + + function setUp() public override { + super.setUp(); + drop = DropERC1155(getContract("DropERC1155")); + } + + /*/////////////////////////////////////////////////////////////// + Branch Testing + //////////////////////////////////////////////////////////////*/ + + modifier lazyMint() { + vm.prank(deployer); + drop.lazyMint(10, "ipfs://", emptyEncodedBytes); + _; + } + + function test_nextTokenIdToMint_ZeroLazyMinted() public { + uint256 nextTokenIdToMint = drop.nextTokenIdToMint(); + assertEq(nextTokenIdToMint, 0); + } + + function test_nextTokenIdToMint_TenLazyMinted() public lazyMint { + uint256 nextTokenIdToMint = drop.nextTokenIdToMint(); + assertEq(nextTokenIdToMint, 10); + } + + function test_contractType() public { + assertEq(drop.contractType(), bytes32("DropERC1155")); + } + + function test_contractVersion() public { + assertEq(drop.contractVersion(), uint8(4)); + } + + function test_supportsInterface() public { + assertEq(drop.supportsInterface(type(IERC2981Upgradeable).interfaceId), true); + assertEq(drop.supportsInterface(type(IERC1155Upgradeable).interfaceId), true); + assertEq(drop.supportsInterface(type(IERC1155MetadataURIUpgradeable).interfaceId), true); + } + + function test__msgData() public { + HarnessDropERC1155MsgData msgDataDrop = new HarnessDropERC1155MsgData(); + bytes memory msgData = msgDataDrop.msgData(); + bytes4 expectedData = msgDataDrop.msgData.selector; + assertEq(bytes4(msgData), expectedData); + } +} + +contract HarnessDropERC1155MsgData is DropERC1155 { + function msgData() public view returns (bytes memory) { + return _msgData(); + } +} diff --git a/src/test/drop/drop-erc1155/miscellaneous/miscellaneous.tree b/src/test/drop/drop-erc1155/miscellaneous/miscellaneous.tree new file mode 100644 index 000000000..e74189c71 --- /dev/null +++ b/src/test/drop/drop-erc1155/miscellaneous/miscellaneous.tree @@ -0,0 +1,8 @@ +function nextTokenIdToMint() +└── it should return the next tokenId that is to be lazy minted ✅ + +function contractType() +└── it should return "DropERC1155" in bytes32 format ✅ + +function contractVersion() +└── it should return 4 in uint8 format ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc1155/setMaxTotalSupply/setMaxTotalSupply.t.sol b/src/test/drop/drop-erc1155/setMaxTotalSupply/setMaxTotalSupply.t.sol new file mode 100644 index 000000000..965c4ce5e --- /dev/null +++ b/src/test/drop/drop-erc1155/setMaxTotalSupply/setMaxTotalSupply.t.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC1155 } from "contracts/prebuilts/drop/DropERC1155.sol"; + +// Test imports + +import "../../../utils/BaseTest.sol"; +import "../../../../../lib/openzeppelin-contracts-upgradeable/contracts/interfaces/IERC2981Upgradeable.sol"; + +contract DropERC1155Test_setMaxTotalSupply is BaseTest { + DropERC1155 public drop; + + address private unauthorized = address(0x123); + + uint256 private newMaxSupply = 100; + string private updatedBaseURI = "ipfs://"; + + event MaxTotalSupplyUpdated(uint256 tokenId, uint256 maxTotalSupply); + + function setUp() public override { + super.setUp(); + drop = DropERC1155(getContract("DropERC1155")); + } + + /*/////////////////////////////////////////////////////////////// + Branch Testing + //////////////////////////////////////////////////////////////*/ + + modifier callerWithoutAdminRole() { + vm.startPrank(unauthorized); + _; + } + + modifier callerWithAdminRole() { + vm.startPrank(deployer); + _; + } + + function test_revert_NoAdminRole() public callerWithoutAdminRole { + bytes32 role = bytes32(0x00); + vm.expectRevert( + abi.encodeWithSelector(Permissions.PermissionsUnauthorizedAccount.selector, unauthorized, role) + ); + drop.setMaxTotalSupply(0, newMaxSupply); + } + + function test_state() public callerWithAdminRole { + drop.setMaxTotalSupply(0, newMaxSupply); + uint256 newMaxTotalSupply = drop.maxTotalSupply(0); + assertEq(newMaxSupply, newMaxTotalSupply); + } + + function test_event() public callerWithAdminRole { + vm.expectEmit(false, false, false, true); + emit MaxTotalSupplyUpdated(0, newMaxSupply); + drop.setMaxTotalSupply(0, newMaxSupply); + } +} diff --git a/src/test/drop/drop-erc1155/setMaxTotalSupply/setMaxTotalSupply.tree b/src/test/drop/drop-erc1155/setMaxTotalSupply/setMaxTotalSupply.tree new file mode 100644 index 000000000..8aa6ad0ef --- /dev/null +++ b/src/test/drop/drop-erc1155/setMaxTotalSupply/setMaxTotalSupply.tree @@ -0,0 +1,6 @@ +function setMaxTotalSupply(uint256 _tokenId, uint256 _maxTotalSupply) +├── when the caller does not have DEFAULT_ADMIN_ROLE +│ └── it should revert ✅ +└── when the caller does have DEFAULT_ADMIN_ROLE + ├── it should set maxTotalSupply for _tokenId as _maxTotalSupply ✅ + └── it should emit MaxTotalSupplyUpdated with the parameters _tokenId, _maxTotalSupply ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc1155/setSaleRecipientForToken/setSaleRecipientForToken.t.sol b/src/test/drop/drop-erc1155/setSaleRecipientForToken/setSaleRecipientForToken.t.sol new file mode 100644 index 000000000..a3d194a53 --- /dev/null +++ b/src/test/drop/drop-erc1155/setSaleRecipientForToken/setSaleRecipientForToken.t.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC1155 } from "contracts/prebuilts/drop/DropERC1155.sol"; + +// Test imports + +import "../../../utils/BaseTest.sol"; +import "../../../../../lib/openzeppelin-contracts-upgradeable/contracts/interfaces/IERC2981Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; + +contract DropERC1155Test_setSaleRecipientForToken is BaseTest { + using Strings for uint256; + + DropERC1155 public drop; + + address private unauthorized = address(0x123); + address private recipient = address(0x456); + + event SaleRecipientForTokenUpdated(uint256 indexed tokenId, address saleRecipient); + + function setUp() public override { + super.setUp(); + drop = DropERC1155(getContract("DropERC1155")); + } + + /*/////////////////////////////////////////////////////////////// + Branch Testing + //////////////////////////////////////////////////////////////*/ + + modifier callerWithoutAdminRole() { + vm.startPrank(unauthorized); + _; + } + + modifier callerWithAdminRole() { + vm.startPrank(deployer); + _; + } + + function test_revert_NoAdminRole() public callerWithoutAdminRole { + bytes32 role = bytes32(0x00); + vm.expectRevert( + abi.encodeWithSelector(Permissions.PermissionsUnauthorizedAccount.selector, unauthorized, role) + ); + drop.setSaleRecipientForToken(0, recipient); + } + + function test_state() public callerWithAdminRole { + drop.setSaleRecipientForToken(0, recipient); + address newSaleRecipient = drop.saleRecipient(0); + assertEq(newSaleRecipient, recipient); + } + + function test_event() public callerWithAdminRole { + vm.expectEmit(true, true, false, false); + emit SaleRecipientForTokenUpdated(0, recipient); + drop.setSaleRecipientForToken(0, recipient); + } +} diff --git a/src/test/drop/drop-erc1155/setSaleRecipientForToken/setSaleRecipientForToken.tree b/src/test/drop/drop-erc1155/setSaleRecipientForToken/setSaleRecipientForToken.tree new file mode 100644 index 000000000..72d7c2cad --- /dev/null +++ b/src/test/drop/drop-erc1155/setSaleRecipientForToken/setSaleRecipientForToken.tree @@ -0,0 +1,6 @@ +function setSaleRecipientForToken(uint256 _tokenId, address _saleRecipient) +├── when called by a user without DEFAULT_ADMIN_ROLE +│ └── it should revert ✅ +└── when called by a user with DEFAULT_ADMIN_ROLE + ├── it should set saleRecipient for _tokenId as _saleRecipient ✅ + └── it should emit SaleRecipientForTokenUpdated with the parameters _tokenId, _saleRecipient ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc1155/transferTokensOnClaim/transferTokensOnClaim.t.sol b/src/test/drop/drop-erc1155/transferTokensOnClaim/transferTokensOnClaim.t.sol new file mode 100644 index 000000000..411206cc5 --- /dev/null +++ b/src/test/drop/drop-erc1155/transferTokensOnClaim/transferTokensOnClaim.t.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC1155 } from "contracts/prebuilts/drop/DropERC1155.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports + +import "../../../utils/BaseTest.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; + +contract HarnessDropERC1155 is DropERC1155 { + function transferTokensOnClaimHarness(address to, uint256 _tokenId, uint256 _quantityBeingClaimed) external { + transferTokensOnClaim(to, _tokenId, _quantityBeingClaimed); + } +} + +contract MockERC1155Receiver { + function onERC1155Received(address, address, uint256, uint256, bytes memory) external pure returns (bytes4) { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address, + address, + uint256[] memory, + uint256[] memory, + bytes memory + ) external pure returns (bytes4) { + return this.onERC1155BatchReceived.selector; + } +} + +contract MockERC11555NotReceiver {} + +contract DropERC1155Test_transferTokensOnClaim is BaseTest { + using Strings for uint256; + using Strings for address; + + address private to; + MockERC1155Receiver private receiver; + MockERC11555NotReceiver private notReceiver; + + address public dropImp; + HarnessDropERC1155 public proxy; + + function setUp() public override { + super.setUp(); + + bytes memory initializeData = abi.encodeCall( + DropERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ); + + dropImp = address(new HarnessDropERC1155()); + proxy = HarnessDropERC1155(address(new TWProxy(dropImp, initializeData))); + + receiver = new MockERC1155Receiver(); + notReceiver = new MockERC11555NotReceiver(); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: misc. + //////////////////////////////////////////////////////////////*/ + + modifier toEOA() { + to = address(0x01); + _; + } + + modifier toReceiever() { + to = address(receiver); + _; + } + + modifier toNotReceiever() { + to = address(notReceiver); + _; + } + + /** + * note: Tests whether contract reverts when a non-holder renounces a role. + */ + function test_revert_ContractNotERC155Receiver() public toNotReceiever { + vm.expectRevert("ERC1155: transfer to non-ERC1155Receiver implementer"); + proxy.transferTokensOnClaimHarness(to, 0, 1); + } + + function test_state_ContractERC1155Receiver() public toReceiever { + uint256 beforeBalance = proxy.balanceOf(to, 0); + proxy.transferTokensOnClaimHarness(to, 0, 1); + uint256 afterBalance = proxy.balanceOf(to, 0); + assertEq(beforeBalance + 1, afterBalance); + } + + function test_state_EOAReceiver() public toEOA { + uint256 beforeBalance = proxy.balanceOf(to, 0); + proxy.transferTokensOnClaimHarness(to, 0, 1); + uint256 afterBalance = proxy.balanceOf(to, 0); + assertEq(beforeBalance + 1, afterBalance); + } +} diff --git a/src/test/drop/drop-erc1155/transferTokensOnClaim/transferTokensOnClaim.tree b/src/test/drop/drop-erc1155/transferTokensOnClaim/transferTokensOnClaim.tree new file mode 100644 index 000000000..eb600dbed --- /dev/null +++ b/src/test/drop/drop-erc1155/transferTokensOnClaim/transferTokensOnClaim.tree @@ -0,0 +1,12 @@ +function transferTokensOnClaim( + address _to, + uint256 _tokenId, + uint256 _quantityBeingClaimed +) +├── when {to} is a smart contract +│ ├── when {to} does not implement onERC1155Received +│ │ └── it should revert ✅ +│ └── when {to} does implement onERC1155Received +│ └── it should mint {amount} number of {id} tokens to {to} ✅ +└── when {to} is an EOA + └── it should mint {amount} number of {id} tokens to {to} ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc1155/updateBatchBaseURI/updateBatchBaseURI.t.sol b/src/test/drop/drop-erc1155/updateBatchBaseURI/updateBatchBaseURI.t.sol new file mode 100644 index 000000000..fa980975b --- /dev/null +++ b/src/test/drop/drop-erc1155/updateBatchBaseURI/updateBatchBaseURI.t.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC1155, BatchMintMetadata } from "contracts/prebuilts/drop/DropERC1155.sol"; + +// Test imports + +import "../../../utils/BaseTest.sol"; +import "../../../../../lib/openzeppelin-contracts-upgradeable/contracts/interfaces/IERC2981Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; + +contract DropERC1155Test_updateBatchBaseURI is BaseTest { + using Strings for uint256; + + event MetadataFrozen(); + + DropERC1155 public drop; + + address private unauthorized = address(0x123); + + bytes private emptyEncodedBytes = abi.encode("", ""); + string private updatedBaseURI = "ipfs://"; + + event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); + + function setUp() public override { + super.setUp(); + drop = DropERC1155(getContract("DropERC1155")); + } + + /*/////////////////////////////////////////////////////////////// + Branch Testing + //////////////////////////////////////////////////////////////*/ + + modifier callerWithoutMetadataRole() { + vm.startPrank(unauthorized); + _; + } + + modifier callerWithMetadataRole() { + vm.startPrank(deployer); + _; + } + + modifier lazyMint() { + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + _; + } + + modifier lazyMintEmptyUri() { + vm.prank(deployer); + drop.lazyMint(100, "", emptyEncodedBytes); + _; + } + + modifier batchFrozen() { + vm.prank(deployer); + drop.freezeBatchBaseURI(0); + _; + } + + function test_revert_NoMetadataRole() public lazyMint callerWithoutMetadataRole { + bytes32 role = keccak256("METADATA_ROLE"); + vm.expectRevert( + abi.encodeWithSelector(Permissions.PermissionsUnauthorizedAccount.selector, unauthorized, role) + ); + drop.updateBatchBaseURI(0, updatedBaseURI); + } + + function test_revert_IndexTooHigh() public lazyMint callerWithMetadataRole { + vm.expectRevert(abi.encodeWithSelector(BatchMintMetadata.BatchMintInvalidBatchId.selector, 1)); + drop.updateBatchBaseURI(1, updatedBaseURI); + } + + function test_revert_BatchFrozen() public lazyMint batchFrozen callerWithMetadataRole { + vm.expectRevert( + abi.encodeWithSelector(BatchMintMetadata.BatchMintMetadataFrozen.selector, drop.getBatchIdAtIndex(0)) + ); + drop.updateBatchBaseURI(0, updatedBaseURI); + } + + function test_state() public lazyMint callerWithMetadataRole { + drop.updateBatchBaseURI(0, updatedBaseURI); + string memory newBaseURI = drop.uri(0); + console.log("newBaseURI: %s", newBaseURI); + assertEq(newBaseURI, string(abi.encodePacked(updatedBaseURI, "0"))); + } + + function test_event() public lazyMint callerWithMetadataRole { + vm.expectEmit(false, false, false, false); + emit BatchMetadataUpdate(0, 100); + drop.updateBatchBaseURI(0, updatedBaseURI); + } +} diff --git a/src/test/drop/drop-erc1155/updateBatchBaseURI/updateBatchBaseURI.tree b/src/test/drop/drop-erc1155/updateBatchBaseURI/updateBatchBaseURI.tree new file mode 100644 index 000000000..75ffaadcf --- /dev/null +++ b/src/test/drop/drop-erc1155/updateBatchBaseURI/updateBatchBaseURI.tree @@ -0,0 +1,9 @@ +function updateBatchBaseURI(uint256 _index, string calldata _uri) +├── when the caller does not have metadataRole +│ └── it should revert ✅ +└── when the caller has metadataRole + ├── when batchFrozen[_batchId for _index] is equal to true + │ └── it should revert ✅ + └── when batchFrozen[_batchId for _index] is equal to false + ├── it should set baseURI[_batchId for _index] to _uri ✅ + └── it should emit BatchMetadataUpdate with the parameters startingTokenId, _batchId ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc20/_beforeClaim/_beforeClaim.t.sol b/src/test/drop/drop-erc20/_beforeClaim/_beforeClaim.t.sol new file mode 100644 index 000000000..c6c3e681b --- /dev/null +++ b/src/test/drop/drop-erc20/_beforeClaim/_beforeClaim.t.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC20 } from "contracts/prebuilts/drop/DropERC20.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "../../../utils/BaseTest.sol"; + +contract HarnessDropERC20BeforeClaim is DropERC20 { + bytes private emptyBytes = bytes(""); + + function harness_beforeClaim(uint256 quantity, AllowlistProof calldata _proof) public view { + _beforeClaim(address(0), quantity, address(0), 0, _proof, emptyBytes); + } +} + +contract DropERC20Test_beforeClaim is BaseTest { + address public dropImp; + HarnessDropERC20BeforeClaim public proxy; + + uint256 private mintQty; + + function setUp() public override { + super.setUp(); + + bytes memory initializeData = abi.encodeCall( + DropERC20.initialize, + (deployer, NAME, SYMBOL, CONTRACT_URI, forwarders(), saleRecipient, platformFeeRecipient, platformFeeBps) + ); + + dropImp = address(new HarnessDropERC20BeforeClaim()); + proxy = HarnessDropERC20BeforeClaim(address(new TWProxy(dropImp, initializeData))); + } + + modifier setMaxTotalSupply() { + vm.prank(deployer); + proxy.setMaxTotalSupply(100); + _; + } + + modifier qtyExceedMaxTotalSupply() { + mintQty = 101; + _; + } + + function test_revert_MaxSupplyExceeded() public setMaxTotalSupply qtyExceedMaxTotalSupply { + DropERC20.AllowlistProof memory proof; + vm.expectRevert("exceed max total supply."); + proxy.harness_beforeClaim(mintQty, proof); + } +} diff --git a/src/test/drop/drop-erc20/_beforeClaim/_beforeClaim.tree b/src/test/drop/drop-erc20/_beforeClaim/_beforeClaim.tree new file mode 100644 index 000000000..f1cf867a4 --- /dev/null +++ b/src/test/drop/drop-erc20/_beforeClaim/_beforeClaim.tree @@ -0,0 +1,10 @@ +function _beforeClaim( + address, + uint256 _quantity, + address, + uint256, + AllowlistProof calldata, + bytes memory +) +└── when maxTotalSupply does not equal to 0 and totalSupply() + _quantity is greater than _maxTotalSupply + └── it should revert ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc20/_canSetFunctions/_canSetFunctions.t.sol b/src/test/drop/drop-erc20/_canSetFunctions/_canSetFunctions.t.sol new file mode 100644 index 000000000..2b17cdd08 --- /dev/null +++ b/src/test/drop/drop-erc20/_canSetFunctions/_canSetFunctions.t.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC20 } from "contracts/prebuilts/drop/DropERC20.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "../../../utils/BaseTest.sol"; + +contract HarnessDropERC20CanSet is DropERC20 { + function canSetPlatformFeeInfo() external view returns (bool) { + return _canSetPlatformFeeInfo(); + } + + function canSetPrimarySaleRecipient() external view returns (bool) { + return _canSetPrimarySaleRecipient(); + } + + function canSetContractURI() external view returns (bool) { + return _canSetContractURI(); + } + + function canSetClaimConditions() external view returns (bool) { + return _canSetClaimConditions(); + } +} + +contract DropERC20Test_canSet is BaseTest { + address public dropImp; + + HarnessDropERC20CanSet public proxy; + + function setUp() public override { + super.setUp(); + + bytes memory initializeData = abi.encodeCall( + DropERC20.initialize, + (deployer, NAME, SYMBOL, CONTRACT_URI, forwarders(), saleRecipient, platformFeeRecipient, platformFeeBps) + ); + + dropImp = address(new HarnessDropERC20CanSet()); + proxy = HarnessDropERC20CanSet(address(new TWProxy(dropImp, initializeData))); + } + + modifier callerHasDefaultAdminRole() { + vm.startPrank(deployer); + _; + } + + modifier callerDoesNotHaveDefaultAdminRole() { + _; + } + + function test_canSetPlatformFee_returnTrue() public callerHasDefaultAdminRole { + bool status = proxy.canSetPlatformFeeInfo(); + assertEq(status, true); + } + + function test_canSetPlatformFee_returnFalse() public callerDoesNotHaveDefaultAdminRole { + bool status = proxy.canSetPlatformFeeInfo(); + assertEq(status, false); + } + + function test_canSetPrimarySaleRecipient_returnTrue() public callerHasDefaultAdminRole { + bool status = proxy.canSetPrimarySaleRecipient(); + assertEq(status, true); + } + + function test_canSetPrimarySaleRecipient_returnFalse() public callerDoesNotHaveDefaultAdminRole { + bool status = proxy.canSetPrimarySaleRecipient(); + assertEq(status, false); + } + + function test_canSetContractURI_returnTrue() public callerHasDefaultAdminRole { + bool status = proxy.canSetContractURI(); + assertEq(status, true); + } + + function test_canSetContractURI_returnFalse() public callerDoesNotHaveDefaultAdminRole { + bool status = proxy.canSetContractURI(); + assertEq(status, false); + } + + function test_canSetClaimConditions_returnTrue() public callerHasDefaultAdminRole { + bool status = proxy.canSetClaimConditions(); + assertEq(status, true); + } + + function test_canSetClaimConditions_returnFalse() public callerDoesNotHaveDefaultAdminRole { + bool status = proxy.canSetClaimConditions(); + assertEq(status, false); + } +} diff --git a/src/test/drop/drop-erc20/_canSetFunctions/_canSetFunctions.tree b/src/test/drop/drop-erc20/_canSetFunctions/_canSetFunctions.tree new file mode 100644 index 000000000..2f2da72e4 --- /dev/null +++ b/src/test/drop/drop-erc20/_canSetFunctions/_canSetFunctions.tree @@ -0,0 +1,23 @@ +function _canSetPlatformFeeInfo() +├── when caller has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when caller does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + +function _canSetPrimarySaleRecipient() +├── when caller has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when caller does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + +function _canSetContractURI() +├── when caller has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when caller does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + +function _canSetClaimConditions() +├── when caller has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when caller does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ diff --git a/src/test/drop/drop-erc20/_collectPriceOnClaim/_collectPriceOnClaim.t.sol b/src/test/drop/drop-erc20/_collectPriceOnClaim/_collectPriceOnClaim.t.sol new file mode 100644 index 000000000..35eb4ab78 --- /dev/null +++ b/src/test/drop/drop-erc20/_collectPriceOnClaim/_collectPriceOnClaim.t.sol @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC20 } from "contracts/prebuilts/drop/DropERC20.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "../../../utils/BaseTest.sol"; + +contract HarnessDropERC20CollectPriceOnClaim is DropERC20 { + function harness_collectPrice( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) public payable { + _collectPriceOnClaim(_primarySaleRecipient, _quantityToClaim, _currency, _pricePerToken); + } +} + +contract DropERC20Test_collectPrice is BaseTest { + address public dropImp; + HarnessDropERC20CollectPriceOnClaim public proxy; + + address private currency; + address private primarySaleRecipient; + uint256 private msgValue; + uint256 private pricePerToken; + address private defaultFeeRecipient; + + function setUp() public override { + super.setUp(); + + bytes memory initializeData = abi.encodeCall( + DropERC20.initialize, + (deployer, NAME, SYMBOL, CONTRACT_URI, forwarders(), saleRecipient, platformFeeRecipient, platformFeeBps) + ); + + dropImp = address(new HarnessDropERC20CollectPriceOnClaim()); + proxy = HarnessDropERC20CollectPriceOnClaim(address(new TWProxy(dropImp, initializeData))); + defaultFeeRecipient = proxy.DEFAULT_FEE_RECIPIENT(); + } + + modifier pricePerTokenZero() { + _; + } + + modifier pricePerTokenNotZero() { + pricePerToken = 1 ether; + _; + } + + modifier msgValueZero() { + _; + } + + modifier msgValueNotZero() { + msgValue = 1 ether; + _; + } + + modifier valuePriceMismatch() { + msgValue = 1 ether; + pricePerToken = 2 ether; + _; + } + + modifier primarySaleRecipientZeroAddress() { + primarySaleRecipient = address(0); + _; + } + + modifier primarySaleRecipientNotZeroAddress() { + primarySaleRecipient = address(0x0999); + _; + } + + modifier currencyNativeToken() { + currency = NATIVE_TOKEN; + _; + } + + modifier currencyNotNativeToken() { + currency = address(erc20); + _; + } + + function test_revert_pricePerTokenZeroMsgValueNotZero() public pricePerTokenZero msgValueNotZero { + vm.expectRevert("!Value"); + proxy.harness_collectPrice{ value: msgValue }(primarySaleRecipient, 1 ether, currency, pricePerToken); + } + + function test_revert_nativeCurrencyTotalPriceZero() public pricePerTokenNotZero msgValueZero currencyNativeToken { + vm.expectRevert("quantity too low"); + proxy.harness_collectPrice{ value: msgValue }(primarySaleRecipient, 0, currency, pricePerToken); + } + + function test_revert_nativeCurrencyValuePriceMismatch() public currencyNativeToken valuePriceMismatch { + vm.expectRevert("Invalid msg value"); + proxy.harness_collectPrice{ value: msgValue }(primarySaleRecipient, 1 ether, currency, pricePerToken); + } + + function test_revert_erc20ValuePriceMismatch() public currencyNotNativeToken valuePriceMismatch { + vm.expectRevert("Invalid msg value"); + proxy.harness_collectPrice{ value: msgValue }(primarySaleRecipient, 1 ether, currency, pricePerToken); + } + + function test_state_nativeCurrency() + public + currencyNativeToken + pricePerTokenNotZero + msgValueNotZero + primarySaleRecipientNotZeroAddress + { + (address platformFeeRecipient, uint16 platformFeeBps) = proxy.getPlatformFeeInfo(); + uint256 beforeBalancePrimarySaleRecipient = address(primarySaleRecipient).balance; + uint256 beforeBalancePlatformFeeRecipient = address(platformFeeRecipient).balance; + uint256 defaultFeeRecipientBefore = address(defaultFeeRecipient).balance; + + proxy.harness_collectPrice{ value: msgValue }(primarySaleRecipient, 1 ether, currency, pricePerToken); + + uint256 afterBalancePrimarySaleRecipient = address(primarySaleRecipient).balance; + uint256 afterBalancePlatformFeeRecipient = address(platformFeeRecipient).balance; + uint256 defaultFeeRecipientAfter = address(defaultFeeRecipient).balance; + + uint256 defaultPlatformFeeVal = (pricePerToken * 100) / MAX_BPS; + uint256 platformFeeVal = (msgValue * platformFeeBps) / MAX_BPS; + uint256 primarySaleRecipientVal = msgValue - platformFeeVal - defaultPlatformFeeVal; + + assertEq(beforeBalancePrimarySaleRecipient + primarySaleRecipientVal, afterBalancePrimarySaleRecipient); + assertEq(beforeBalancePlatformFeeRecipient + platformFeeVal, afterBalancePlatformFeeRecipient); + assertEq(defaultFeeRecipientAfter - defaultFeeRecipientBefore, defaultPlatformFeeVal); + } + + function test_revert_erc20_msgValueNotZero() + public + currencyNotNativeToken + msgValueNotZero + primarySaleRecipientNotZeroAddress + { + vm.expectRevert("!Value"); + proxy.harness_collectPrice{ value: msgValue }(primarySaleRecipient, msgValue, currency, pricePerToken); + } + + function test_state_erc20() public currencyNotNativeToken pricePerTokenNotZero primarySaleRecipientNotZeroAddress { + (address platformFeeRecipient, uint16 platformFeeBps) = proxy.getPlatformFeeInfo(); + + erc20.mint(address(this), pricePerToken); + ERC20(erc20).approve(address(proxy), pricePerToken); + uint256 beforeBalancePrimarySaleRecipient = erc20.balanceOf(primarySaleRecipient); + uint256 defaultFeeRecipientBefore = erc20.balanceOf(defaultFeeRecipient); + uint256 beforeBalancePlatformFeeRecipient = erc20.balanceOf(platformFeeRecipient); + + proxy.harness_collectPrice(primarySaleRecipient, pricePerToken, currency, pricePerToken); + + uint256 afterBalancePrimarySaleRecipient = erc20.balanceOf(primarySaleRecipient); + uint256 defaultFeeRecipientAfter = erc20.balanceOf(defaultFeeRecipient); + uint256 afterBalancePlatformFeeRecipient = erc20.balanceOf(platformFeeRecipient); + + uint256 defaultPlatformFeeVal = (pricePerToken * 100) / MAX_BPS; + uint256 platformFeeVal = (pricePerToken * platformFeeBps) / MAX_BPS; + uint256 primarySaleRecipientVal = 1 ether - platformFeeVal - defaultPlatformFeeVal; + + assertEq(beforeBalancePrimarySaleRecipient + primarySaleRecipientVal, afterBalancePrimarySaleRecipient); + assertEq(beforeBalancePlatformFeeRecipient + platformFeeVal, afterBalancePlatformFeeRecipient); + assertEq(defaultFeeRecipientAfter - defaultFeeRecipientBefore, defaultPlatformFeeVal); + } + + function test_state_erc20StoredPrimarySaleRecipient() + public + currencyNotNativeToken + pricePerTokenNotZero + primarySaleRecipientZeroAddress + { + (address platformFeeRecipient, uint16 platformFeeBps) = proxy.getPlatformFeeInfo(); + address storedPrimarySaleRecipient = proxy.primarySaleRecipient(); + + erc20.mint(address(this), pricePerToken); + ERC20(erc20).approve(address(proxy), pricePerToken); + uint256 beforeBalancePrimarySaleRecipient = erc20.balanceOf(storedPrimarySaleRecipient); + uint256 defaultFeeRecipientBefore = erc20.balanceOf(defaultFeeRecipient); + uint256 beforeBalancePlatformFeeRecipient = erc20.balanceOf(platformFeeRecipient); + + proxy.harness_collectPrice(primarySaleRecipient, pricePerToken, currency, pricePerToken); + + uint256 afterBalancePrimarySaleRecipient = erc20.balanceOf(storedPrimarySaleRecipient); + uint256 defaultFeeRecipientAfter = erc20.balanceOf(defaultFeeRecipient); + uint256 afterBalancePlatformFeeRecipient = erc20.balanceOf(platformFeeRecipient); + + uint256 defaultPlatformFeeVal = (pricePerToken * 100) / MAX_BPS; + uint256 platformFeeVal = (pricePerToken * platformFeeBps) / MAX_BPS; + uint256 primarySaleRecipientVal = 1 ether - platformFeeVal - defaultPlatformFeeVal; + + assertEq(beforeBalancePrimarySaleRecipient + primarySaleRecipientVal, afterBalancePrimarySaleRecipient); + assertEq(beforeBalancePlatformFeeRecipient + platformFeeVal, afterBalancePlatformFeeRecipient); + assertEq(defaultFeeRecipientAfter - defaultFeeRecipientBefore, defaultPlatformFeeVal); + } + + function test_state_nativeCurrencyStoredPrimarySaleRecipient() + public + currencyNativeToken + pricePerTokenNotZero + primarySaleRecipientZeroAddress + msgValueNotZero + { + (address platformFeeRecipient, uint16 platformFeeBps) = proxy.getPlatformFeeInfo(); + address storedPrimarySaleRecipient = proxy.primarySaleRecipient(); + + uint256 beforeBalancePrimarySaleRecipient = address(storedPrimarySaleRecipient).balance; + uint256 beforeBalancePlatformFeeRecipient = address(platformFeeRecipient).balance; + uint256 defaultFeeRecipientBefore = address(defaultFeeRecipient).balance; + + proxy.harness_collectPrice{ value: msgValue }(primarySaleRecipient, 1 ether, currency, pricePerToken); + + uint256 afterBalancePrimarySaleRecipient = address(storedPrimarySaleRecipient).balance; + uint256 afterBalancePlatformFeeRecipient = address(platformFeeRecipient).balance; + uint256 defaultFeeRecipientAfter = address(defaultFeeRecipient).balance; + + uint256 defaultPlatformFeeVal = (pricePerToken * 100) / MAX_BPS; + uint256 platformFeeVal = (msgValue * platformFeeBps) / MAX_BPS; + uint256 primarySaleRecipientVal = msgValue - platformFeeVal - defaultPlatformFeeVal; + + assertEq(beforeBalancePrimarySaleRecipient + primarySaleRecipientVal, afterBalancePrimarySaleRecipient); + assertEq(beforeBalancePlatformFeeRecipient + platformFeeVal, afterBalancePlatformFeeRecipient); + assertEq(defaultFeeRecipientAfter - defaultFeeRecipientBefore, defaultPlatformFeeVal); + } +} diff --git a/src/test/drop/drop-erc20/_collectPriceOnClaim/_collectPriceOnClaim.tree b/src/test/drop/drop-erc20/_collectPriceOnClaim/_collectPriceOnClaim.tree new file mode 100644 index 000000000..933ae6877 --- /dev/null +++ b/src/test/drop/drop-erc20/_collectPriceOnClaim/_collectPriceOnClaim.tree @@ -0,0 +1,44 @@ +function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken +) +├── when _pricePerToken is equal to zero +│ ├── when msg.value does not equal to zero +│ │ └── it should revert ✅ +│ └── when msg.value is equal to zero +│ └── it should return ✅ +└── when _pricePerToken is not equal to zero + ├── when _primarySaleRecipient is equal to address(0) + │ ├── when totalPrice is equal to zero + │ │ └── it should revert ✅ + │ └── when total price is not equal to zero + │ ├── when currency is native token + │ │ ├── when msg.value does not equal totalPrice + │ │ │ └── it should revert ✅ + │ │ └── when msg.value does equal totalPrice + │ │ ├── platformFees (totalPrice * platformFeeBps / MAX_BPS) should be transfered to platformFeeRecipient ✅ + │ │ └── totalPrice - platformFees should be transfered to primarySaleRecipient() ✅ + │ └── when currency is not native token + │ ├── when msg.value is not equal to zero + │ │ └── it should revert ✅ + │ └── when msg.value is equal to zero + │ ├── platformFees (totalPrice * platformFeeBps / MAX_BPS) should be transfered to platformFeeRecipient ✅ + │ └── totalPrice - platformFees should be transfered to primarySaleRecipient() ✅ + └── when _primarySaleRecipient is not equal to address(0) + ├── when totalPrice is equal to zero + │ └── it should revert ✅ + └── when total price is not equal to zero + ├── when currency is not native token + │ ├── when msg.value does not equal totalPrice + │ │ └── it should revert ✅ + │ └── when msg.value does equal totalPrice + │ ├── platformFees (totalPrice * platformFeeBps / MAX_BPS) should be transfered to platformFeeRecipient ✅ + │ └── totalPrice - platformFees should be transfered to _primarySaleRecipient ✅ + └── when currency is not native token + ├── when msg.value is not equal to zero + │ └── it should revert ✅ + └── when msg.value is equal to zero + ├── platformFees (totalPrice * platformFeeBps / MAX_BPS) should be transfered to platformFeeRecipient ✅ + └── totalPrice - platformFees should be transfered to _primarySaleRecipient ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc20/initialize/initialize.t.sol b/src/test/drop/drop-erc20/initialize/initialize.t.sol new file mode 100644 index 000000000..61debd129 --- /dev/null +++ b/src/test/drop/drop-erc20/initialize/initialize.t.sol @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC20, PlatformFee } from "contracts/prebuilts/drop/DropERC20.sol"; + +// Test imports +import "../../../utils/BaseTest.sol"; + +contract DropERC20Test_initializer is BaseTest { + DropERC20 public newDropContract; + + event ContractURIUpdated(string prevURI, string newURI); + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps); + event PrimarySaleRecipientUpdated(address indexed recipient); + + function setUp() public override { + super.setUp(); + } + + modifier platformFeeBPSTooHigh() { + platformFeeBps = 10001; + _; + } + + function test_state() public { + deployContractProxy( + "DropERC20", + abi.encodeCall( + DropERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ); + + newDropContract = DropERC20(getContract("DropERC20")); + (address _platformFeeRecipient, uint128 _platformFeeBps) = newDropContract.getPlatformFeeInfo(); + address _saleRecipient = newDropContract.primarySaleRecipient(); + + for (uint256 i = 0; i < forwarders().length; i++) { + assertEq(newDropContract.isTrustedForwarder(forwarders()[i]), true); + } + + assertEq(newDropContract.name(), NAME); + assertEq(newDropContract.symbol(), SYMBOL); + assertEq(newDropContract.contractURI(), CONTRACT_URI); + assertEq(_platformFeeRecipient, platformFeeRecipient); + assertEq(_platformFeeBps, platformFeeBps); + assertEq(_saleRecipient, saleRecipient); + } + + function test_revert_PlatformFeeBPSTooHigh() public platformFeeBPSTooHigh { + vm.expectRevert( + abi.encodeWithSelector(PlatformFee.PlatformFeeExceededMaxFeeBps.selector, 10_000, platformFeeBps) + ); + deployContractProxy( + "DropERC20", + abi.encodeCall( + DropERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ); + } + + function test_event_ContractURIUpdated() public { + vm.expectEmit(false, false, false, true); + emit ContractURIUpdated("", CONTRACT_URI); + deployContractProxy( + "DropERC20", + abi.encodeCall( + DropERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ); + } + + function test_event_RoleGrantedDefaultAdminRole() public { + bytes32 role = bytes32(0x00); + vm.expectEmit(true, true, true, false); + emit RoleGranted(role, deployer, factory); + deployContractProxy( + "DropERC20", + abi.encodeCall( + DropERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ); + } + + function test_event_RoleGrantedTransferRole() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + vm.expectEmit(true, true, true, false); + emit RoleGranted(role, deployer, factory); + deployContractProxy( + "DropERC20", + abi.encodeCall( + DropERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ); + } + + function test_event_RoleGrantedTransferRoleZeroAddress() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + vm.expectEmit(true, true, true, false); + emit RoleGranted(role, address(0), factory); + deployContractProxy( + "DropERC20", + abi.encodeCall( + DropERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ); + } + + function test_event_PlatformFeeInfoUpdated() public { + vm.expectEmit(true, false, false, true); + emit PlatformFeeInfoUpdated(platformFeeRecipient, platformFeeBps); + deployContractProxy( + "DropERC20", + abi.encodeCall( + DropERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ); + } + + function test_event_PrimarySaleRecipientUpdated() public { + vm.expectEmit(true, false, false, false); + emit PrimarySaleRecipientUpdated(saleRecipient); + deployContractProxy( + "DropERC20", + abi.encodeCall( + DropERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ); + } + + function test_roleCheck() public { + deployContractProxy( + "DropERC20", + abi.encodeCall( + DropERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ); + + newDropContract = DropERC20(getContract("DropERC20")); + + assertEq(newDropContract.hasRole(bytes32(0x00), deployer), true); + assertEq(newDropContract.hasRole(keccak256("TRANSFER_ROLE"), deployer), true); + assertEq(newDropContract.hasRole(keccak256("TRANSFER_ROLE"), address(0)), true); + } +} diff --git a/src/test/drop/drop-erc20/initialize/intialize.tree b/src/test/drop/drop-erc20/initialize/intialize.tree new file mode 100644 index 000000000..6731bc81e --- /dev/null +++ b/src/test/drop/drop-erc20/initialize/intialize.tree @@ -0,0 +1,33 @@ +function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _platformFeeRecipient, + uint128 _platformFeeBps +) +├── when _trustedForwarders.length > 0 +│ └── it should set _trustedForwarder[_trustedForwarders[i]] as true for each address in _trustedForwarders ✅ +├── it should set contractURI as _contractURI ✅ +├── it should emit ContractURIUpdated with the parameters: prevURI, _uri ✅ +├── it should set _defaultAdmin as the owner of the contract ✅ +├── it should emit OwnerUpdated with the parameters: _prevOwner, _defaultAdmin ✅ +├── it should assign the role DEFAULT_ADMIN_ROLE to _defaultAdmin ✅ +├── it should emit RoleGranted with the parameters: DEFAULT_ADMIN_ROLE, _defaultAdmin, msg.sender ✅ +├── it should assign the role _transferRole to _defaultAdmin ✅ +├── it should emit RoleGranted with the parameters: _transferRole, _defaultAdmin, msg.sender ✅ +├── it should assign the role _transferRole to address(0) ✅ +├── it should emit RoleGranted with the parameters: _transferRole, address(0), msg.sender ✅ +├── when _platformFeeBps is greater than 10_000 +│ └── it should revert ✅ +├── when _platformFeeBps is less than or equal to 10_000 +│ ├── it should set platformFeeBps to uint16(_platformFeeBps); ✅ +│ ├── it should set platformFeeRecipient to _platformFeeRecipient ✅ +│ └── it should emit PlatformFeeInfoUpdated with the following parameters: _platformFeeRecipient, _platformFeeBps ✅ +├── it should set recipient as _primarySaleRecipient ✅ +├── it should emit PrimarySaleRecipientUpdated with the parameters _primarySaleRecipient ✅ +├── it should set transferRole as keccak256("TRANSFER_ROLE")✅ +├── it should set _name as _name ✅ +└── it should set _symbol as _symbol ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc20/miscellaneous/miscellaneous.t.sol b/src/test/drop/drop-erc20/miscellaneous/miscellaneous.t.sol new file mode 100644 index 000000000..0845465ff --- /dev/null +++ b/src/test/drop/drop-erc20/miscellaneous/miscellaneous.t.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC20 } from "contracts/prebuilts/drop/DropERC20.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "../../../utils/BaseTest.sol"; + +contract HarnessDropERC20Misc is DropERC20 { + bytes32 private transferRole = keccak256("TRANSFER_ROLE"); + + function msgData() public view returns (bytes memory) { + return _msgData(); + } + + function transferTokensOnClaim(address _to, uint256 _quantityBeingClaimed) public returns (uint256) { + return _transferTokensOnClaim(_to, _quantityBeingClaimed); + } + + function beforeTokenTransfer(address from, address to, uint256 amount) public { + _beforeTokenTransfer(from, to, amount); + } + + function mint(address to, uint256 amount) public { + _mint(to, amount); + } + + function burn(address from, uint256 amount) public { + _burn(from, amount); + } + + function hasTransferRole(address _account) public view returns (bool) { + return hasRole(transferRole, _account); + } +} + +contract DropERC20Test_misc is BaseTest { + address public dropImp; + HarnessDropERC20Misc public proxy; + + function setUp() public override { + super.setUp(); + + bytes memory initializeData = abi.encodeCall( + DropERC20.initialize, + (deployer, NAME, SYMBOL, CONTRACT_URI, forwarders(), saleRecipient, platformFeeRecipient, platformFeeBps) + ); + + dropImp = address(new HarnessDropERC20Misc()); + proxy = HarnessDropERC20Misc(address(new TWProxy(dropImp, initializeData))); + } + + function test_contractType_returnValue() public { + assertEq(proxy.contractType(), "DropERC20"); + } + + function test_contractVersion_returnValue() public { + assertEq(proxy.contractVersion(), uint8(4)); + } + + function test_msgData_returnValue() public { + bytes memory msgData = proxy.msgData(); + bytes4 expectedData = proxy.msgData.selector; + assertEq(bytes4(msgData), expectedData); + } + + function test_state_transferTokensOnClaim() public { + uint256 initialBalance = proxy.balanceOf(deployer); + uint256 quantityBeingClaimed = 1; + proxy.transferTokensOnClaim(deployer, quantityBeingClaimed); + assertEq(proxy.balanceOf(deployer), initialBalance + quantityBeingClaimed); + } + + function test_returnValue_transferTokensOnClaim() public { + uint256 quantityBeingClaimed = 1; + uint256 returnValue = proxy.transferTokensOnClaim(deployer, quantityBeingClaimed); + assertEq(returnValue, 0); + } + + function test_beforeTokenTransfer_revert_addressZeroNoTransferRole() public { + vm.prank(deployer); + proxy.revokeRole(keccak256("TRANSFER_ROLE"), address(0)); + vm.expectRevert("transfers restricted."); + proxy.beforeTokenTransfer(address(0x01), address(0x02), 1); + } + + function test_beforeTokenTransfer_doesNotRevert_addressZeroNoTransferRole_burnMint() public { + vm.prank(deployer); + proxy.revokeRole(keccak256("TRANSFER_ROLE"), address(0)); + proxy.beforeTokenTransfer(address(0), address(0x02), 1); + proxy.beforeTokenTransfer(address(0x01), address(0), 1); + } + + function test_state_mint() public { + uint256 initialBalance = proxy.balanceOf(deployer); + uint256 amount = 1; + proxy.mint(deployer, amount); + assertEq(proxy.balanceOf(deployer), initialBalance + amount); + } + + function test_state_burn() public { + proxy.mint(deployer, 1); + uint256 initialBalance = proxy.balanceOf(deployer); + uint256 amount = 1; + proxy.burn(deployer, amount); + assertEq(proxy.balanceOf(deployer), initialBalance - amount); + } + + function test_transfer_drop() public { + //deal erc20 drop to address(0x1) + deal(address(proxy), address(0x1), 1); + vm.prank(address(0x1)); + proxy.transfer(address(0x2), 1); + assertEq(proxy.balanceOf(address(0x2)), 1); + } +} diff --git a/src/test/drop/drop-erc20/miscellaneous/miscellaneous.tree b/src/test/drop/drop-erc20/miscellaneous/miscellaneous.tree new file mode 100644 index 000000000..61c35271e --- /dev/null +++ b/src/test/drop/drop-erc20/miscellaneous/miscellaneous.tree @@ -0,0 +1,30 @@ +function contractType() +└── it should return bytes32("DropERC20") ✅ + +function contractVersion() +└── it should return uint8(4) ✅ + +function _mint(address account, uint256 amount) +└── it should mint amount tokens to account ✅ + +function _burn(address account, uint256 amount) +└── it should burn amount tokens from account ✅ + +function _afterTokenTransfer( + address from, + address to, + uint256 amount +) +└── it should call _afterTokenTransfer logic from ERC20VotesUpgradeable + +function _msgData() +└── it should return msg.data ✅ + +function _beforeTokenTransfer(address from, address to, uint256 amount) +└── when address(0) does not have transferRole and from does not equal address(0) and from does not equal address(0) + └── when from does not have transfer role and to does not have transferRole + └── it should revert ✅ + +function _transferTokensOnClaim(address _to, uint256 _quantityBeingClaimed) +├── it should mint _quantityBeingClaimed tokens to _to ✅ +└── it should return 0 ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc20/setMaxTotalSupply/setMaxTotalSupply.t.sol b/src/test/drop/drop-erc20/setMaxTotalSupply/setMaxTotalSupply.t.sol new file mode 100644 index 000000000..8349f808c --- /dev/null +++ b/src/test/drop/drop-erc20/setMaxTotalSupply/setMaxTotalSupply.t.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC20 } from "contracts/prebuilts/drop/DropERC20.sol"; + +// Test imports +import "../../../utils/BaseTest.sol"; + +contract DropERC20Test_setMaxTotalSupply is BaseTest { + event MaxTotalSupplyUpdated(uint256 maxTotalSupply); + + DropERC20 public drop; + + function setUp() public override { + super.setUp(); + + drop = DropERC20(getContract("DropERC20")); + } + + modifier callerHasDefaultAdminRole() { + vm.startPrank(deployer); + _; + } + + modifier callerDoesNotHaveDefaultAdminRole() { + _; + } + + function test_revert_doesNotHaveAdminRole() public callerDoesNotHaveDefaultAdminRole { + bytes32 role = bytes32(0x00); + + vm.expectRevert( + abi.encodeWithSelector(Permissions.PermissionsUnauthorizedAccount.selector, address(this), role) + ); + drop.setMaxTotalSupply(0); + } + + function test_state_callerHasDefaultAdminRole() public callerHasDefaultAdminRole { + drop.setMaxTotalSupply(100); + assertEq(drop.maxTotalSupply(), 100); + } + + function test_event_callerHasDefaultAdminRole() public callerHasDefaultAdminRole { + vm.expectEmit(false, false, false, true); + emit MaxTotalSupplyUpdated(100); + drop.setMaxTotalSupply(100); + } +} diff --git a/src/test/drop/drop-erc20/setMaxTotalSupply/setMaxTotalSupply.tree b/src/test/drop/drop-erc20/setMaxTotalSupply/setMaxTotalSupply.tree new file mode 100644 index 000000000..a8b23e9ca --- /dev/null +++ b/src/test/drop/drop-erc20/setMaxTotalSupply/setMaxTotalSupply.tree @@ -0,0 +1,6 @@ +function setMaxTotalSupply(uint256 _maxTotalSupply) +├── when the caller does not have DEFAULT_ADMIN_ROLE +│ └── it should revert ✅ +└── when the caller does have DEFAULT_ADMIN_ROLE + ├── it should set maxTotalSupply as _maxTotalSupply ✅ + └── it should emit MaxTotalSupplyUpdated with the parameters _maxTotalSupply ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc721/_beforeClaim/_beforeClaim.t.sol b/src/test/drop/drop-erc721/_beforeClaim/_beforeClaim.t.sol new file mode 100644 index 000000000..f30885a51 --- /dev/null +++ b/src/test/drop/drop-erc721/_beforeClaim/_beforeClaim.t.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC721 } from "contracts/prebuilts/drop/DropERC721.sol"; + +// Test imports +import "../../../utils/BaseTest.sol"; + +contract DropERC721Test_beforeClaim is BaseTest { + event TokenURIRevealed(uint256 indexed index, string revealedURI); + + DropERC721 public drop; + + bytes private beforeClaim_data; + string private beforeClaim_baseURI; + uint256 private beforeClaim_amount; + address private receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + + DropERC721.AllowlistProof private alp; + + function setUp() public override { + super.setUp(); + drop = DropERC721(getContract("DropERC721")); + + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "0"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 0; + alp.currency = address(erc20); + + vm.warp(1); + + DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + } + + /*/////////////////////////////////////////////////////////////// + Branch Testing + //////////////////////////////////////////////////////////////*/ + + modifier lazyMintUnEncrypted() { + beforeClaim_amount = 10; + beforeClaim_baseURI = "ipfs://"; + vm.prank(deployer); + drop.lazyMint(beforeClaim_amount, beforeClaim_baseURI, beforeClaim_data); + _; + } + + modifier setMaxSupply() { + vm.prank(deployer); + drop.setMaxTotalSupply(5); + _; + } + + function test_revert_greaterThanNextTokenIdToLazyMint() public lazyMintUnEncrypted { + vm.prank(receiver, receiver); + vm.expectRevert("!Tokens"); + drop.claim(receiver, 11, address(erc20), 0, alp, ""); + } + + function test_revert_greaterThanMaxTotalSupply() public lazyMintUnEncrypted setMaxSupply { + vm.prank(receiver, receiver); + vm.expectRevert("!Supply"); + drop.claim(receiver, 6, address(erc20), 0, alp, ""); + } +} diff --git a/src/test/drop/drop-erc721/_beforeClaim/_beforeClaim.tree b/src/test/drop/drop-erc721/_beforeClaim/_beforeClaim.tree new file mode 100644 index 000000000..a225a8be4 --- /dev/null +++ b/src/test/drop/drop-erc721/_beforeClaim/_beforeClaim.tree @@ -0,0 +1,12 @@ +function _beforeClaim( + address, + uint256 _quantity, + address, + uint256, + AllowlistProof calldata, + bytes memory +) +├── when _current index + _quantity are greater than nextTokenIdToLazyMint +│ └── it should revert ✅ +└── when maxTotalSupply does not equal zero and _currentIndex + _quantity is greater than maxTotalSupply + └── it should revert ✅ diff --git a/src/test/drop/drop-erc721/_canSetFunctions/_canSetFunctions.t.sol b/src/test/drop/drop-erc721/_canSetFunctions/_canSetFunctions.t.sol new file mode 100644 index 000000000..61d67d687 --- /dev/null +++ b/src/test/drop/drop-erc721/_canSetFunctions/_canSetFunctions.t.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC721, PlatformFee, PrimarySale, ContractMetadata, Royalty, LazyMint, Drop, Ownable } from "contracts/prebuilts/drop/DropERC721.sol"; + +// Test imports +import "../../../utils/BaseTest.sol"; + +contract DropERC721Test_canSetFunctions is BaseTest { + DropERC721 public drop; + + bytes private canset_data; + string private canset_baseURI; + uint256 private canset_amount; + bytes private canset_encryptedURI; + bytes32 private canset_provenanceHash; + address private unauthorized = address(0x123); + + function setUp() public override { + super.setUp(); + drop = DropERC721(getContract("DropERC721")); + } + + /*/////////////////////////////////////////////////////////////// + Branch Testing + //////////////////////////////////////////////////////////////*/ + + modifier callerNotAdmin() { + vm.startPrank(unauthorized); + _; + } + + modifier callerAdmin() { + vm.startPrank(deployer); + _; + } + + modifier callerNotMinter() { + vm.startPrank(unauthorized); + _; + } + + modifier callerMinter() { + vm.startPrank(deployer); + _; + } + + function test__canSetPlatformFeeInfo_revert_callerNotAdmin() public callerNotAdmin { + vm.expectRevert(abi.encodeWithSelector(PlatformFee.PlatformFeeUnauthorized.selector)); + drop.setPlatformFeeInfo(address(0x1), 1); + } + + function test__canSetPlatformFeeInfo_callerAdmin() public callerAdmin { + drop.setPlatformFeeInfo(address(0x1), 1); + (address recipient, uint16 bps) = drop.getPlatformFeeInfo(); + assertEq(recipient, address(0x1)); + assertEq(bps, 1); + } + + function test__canSetPrimarySaleRecipient_revert_callerNotAdmin() public callerNotAdmin { + vm.expectRevert(abi.encodeWithSelector(PrimarySale.PrimarySaleUnauthorized.selector)); + drop.setPrimarySaleRecipient(address(0x1)); + } + + function test__canSetPrimarySaleRecipient_callerAdmin() public callerAdmin { + drop.setPrimarySaleRecipient(address(0x1)); + assertEq(drop.primarySaleRecipient(), address(0x1)); + } + + function test__canSetOwner_revert_callerNotAdmin() public callerNotAdmin { + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorized.selector)); + drop.setOwner(address(0x1)); + } + + function test__canSetOwner_callerAdmin() public callerAdmin { + drop.setOwner(address(0x1)); + assertEq(drop.owner(), address(0x1)); + } + + function test__canSetRoyaltyInfo_revert_callerNotAdmin() public callerNotAdmin { + vm.expectRevert(abi.encodeWithSelector(Royalty.RoyaltyUnauthorized.selector)); + drop.setDefaultRoyaltyInfo(address(0x1), 1); + } + + function test__canSetRoyaltyInfo_callerAdmin() public callerAdmin { + drop.setDefaultRoyaltyInfo(address(0x1), 1); + (address recipient, uint16 bps) = drop.getDefaultRoyaltyInfo(); + assertEq(recipient, address(0x1)); + assertEq(bps, 1); + } + + function test__canSetContractURI_revert_callerNotAdmin() public callerNotAdmin { + vm.expectRevert(abi.encodeWithSelector(ContractMetadata.ContractMetadataUnauthorized.selector)); + drop.setContractURI("ipfs://"); + } + + function test__canSetContractURI_callerAdmin() public callerAdmin { + drop.setContractURI("ipfs://"); + assertEq(drop.contractURI(), "ipfs://"); + } + + function test__canSetClaimConditions_revert_callerNotAdmin() public callerNotAdmin { + DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = bytes32(0); + conditions[0].pricePerToken = 10; + conditions[0].currency = address(0x111); + vm.expectRevert(abi.encodeWithSelector(Drop.DropUnauthorized.selector)); + drop.setClaimConditions(conditions, true); + } + + function test__canSetClaimConditions_callerAdmin() public callerAdmin { + DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = bytes32(0); + conditions[0].pricePerToken = 10; + conditions[0].currency = address(0x111); + drop.setClaimConditions(conditions, true); + } + + function test__canLazyMint_revert_callerNotMinter() public callerNotMinter { + canset_amount = 10; + canset_baseURI = "ipfs://"; + canset_data = abi.encode(canset_encryptedURI, canset_provenanceHash); + vm.expectRevert(abi.encodeWithSelector(LazyMint.LazyMintUnauthorized.selector)); + drop.lazyMint(canset_amount, canset_baseURI, canset_data); + } + + function test__canLazyMint_callerMinter() public callerMinter { + canset_amount = 10; + canset_baseURI = "ipfs://"; + canset_data = abi.encode(canset_encryptedURI, canset_provenanceHash); + drop.lazyMint(canset_amount, canset_baseURI, canset_data); + } +} diff --git a/src/test/drop/drop-erc721/_canSetFunctions/_canSetFunctions.tree b/src/test/drop/drop-erc721/_canSetFunctions/_canSetFunctions.tree new file mode 100644 index 000000000..72bfbe39a --- /dev/null +++ b/src/test/drop/drop-erc721/_canSetFunctions/_canSetFunctions.tree @@ -0,0 +1,41 @@ +function _canSetPlatformFeeInfo() +├── when caller has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when caller does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + +function _canSetPrimarySaleRecipient() +├── when caller has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when caller does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + +function _canSetOwner() +├── when caller has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when caller does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + +function _canSetRoyaltyInfo() +├── when caller has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when caller does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + +function _canSetContractURI() +├── when caller has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when caller does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + +function _canSetClaimConditions() +├── when caller has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when caller does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + +function _canLazyMint() +├── when caller has minterRole +│ └── it should return true ✅ +└── when caller does not have minterRole + └── it should return false ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc721/_collectPriceOnClaim/_collectPriceOnClaim.t.sol b/src/test/drop/drop-erc721/_collectPriceOnClaim/_collectPriceOnClaim.t.sol new file mode 100644 index 000000000..6b345cf7a --- /dev/null +++ b/src/test/drop/drop-erc721/_collectPriceOnClaim/_collectPriceOnClaim.t.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC721 } from "contracts/prebuilts/drop/DropERC721.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "../../../utils/BaseTest.sol"; + +contract HarnessDropERC721 is DropERC721 { + function collectionPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) public payable { + _collectPriceOnClaim(_primarySaleRecipient, _quantityToClaim, _currency, _pricePerToken); + } +} + +contract DropERC721Test_collectPrice is BaseTest { + address public dropImp; + HarnessDropERC721 public proxy; + + address private collectPrice_saleRecipient = address(0x010); + uint256 private collectPrice_quantityToClaim = 1; + uint256 private collectPrice_pricePerToken; + address private collectPrice_currency; + uint256 private collectPrice_msgValue; + address private defaultFeeRecipient; + + function setUp() public override { + super.setUp(); + + bytes memory initializeData = abi.encodeCall( + DropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ); + + dropImp = address(new HarnessDropERC721()); + proxy = HarnessDropERC721(address(new TWProxy(dropImp, initializeData))); + defaultFeeRecipient = proxy.DEFAULT_FEE_RECIPIENT(); + } + + modifier pricePerTokenZero() { + collectPrice_pricePerToken = 0; + _; + } + + modifier pricePerTokenNotZero() { + collectPrice_pricePerToken = 1 ether; + _; + } + + modifier msgValueNotZero() { + collectPrice_msgValue = 1 ether; + _; + } + + modifier nativeCurrency() { + collectPrice_currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + _; + } + + modifier erc20Currency() { + collectPrice_currency = address(erc20); + erc20.mint(address(this), 1_000 ether); + _; + } + + /*/////////////////////////////////////////////////////////////// + Branch Testing + //////////////////////////////////////////////////////////////*/ + + function test_revert_msgValueNotZero() public nativeCurrency msgValueNotZero pricePerTokenZero { + vm.expectRevert(); + proxy.collectionPriceOnClaim{ value: collectPrice_msgValue }( + collectPrice_saleRecipient, + collectPrice_quantityToClaim, + collectPrice_currency, + collectPrice_pricePerToken + ); + } + + function test_revert_priceValueMismatchNativeCurrency() public nativeCurrency pricePerTokenNotZero { + vm.expectRevert(); + proxy.collectionPriceOnClaim{ value: collectPrice_msgValue }( + collectPrice_saleRecipient, + collectPrice_quantityToClaim, + collectPrice_currency, + collectPrice_pricePerToken + ); + } + + function test_transferNativeCurrency() public nativeCurrency pricePerTokenNotZero msgValueNotZero { + uint256 balanceSaleRecipientBefore = address(saleRecipient).balance; + uint256 platformFeeRecipientBefore = address(platformFeeRecipient).balance; + uint256 defaultFeeRecipientBefore = address(defaultFeeRecipient).balance; + proxy.collectionPriceOnClaim{ value: collectPrice_msgValue }( + saleRecipient, + collectPrice_quantityToClaim, + collectPrice_currency, + collectPrice_pricePerToken + ); + + uint256 balanceSaleRecipientAfter = address(saleRecipient).balance; + uint256 platformFeeRecipientAfter = address(platformFeeRecipient).balance; + uint256 defaultFeeRecipientAfter = address(defaultFeeRecipient).balance; + uint256 defaultPlatformFeeVal = (collectPrice_pricePerToken * 100) / MAX_BPS; + uint256 expectedPlatformFee = (collectPrice_pricePerToken * platformFeeBps) / MAX_BPS; + uint256 expectedSaleRecipientProceed = collectPrice_msgValue - expectedPlatformFee - defaultPlatformFeeVal; + + assertEq(balanceSaleRecipientAfter - balanceSaleRecipientBefore, expectedSaleRecipientProceed); + assertEq(platformFeeRecipientAfter - platformFeeRecipientBefore, expectedPlatformFee); + assertEq(defaultFeeRecipientAfter - defaultFeeRecipientBefore, defaultPlatformFeeVal); + } + + function test_transferERC20() public erc20Currency pricePerTokenNotZero { + uint256 balanceSaleRecipientBefore = erc20.balanceOf(saleRecipient); + uint256 platformFeeRecipientBefore = erc20.balanceOf(platformFeeRecipient); + uint256 defaultFeeRecipientBefore = erc20.balanceOf(defaultFeeRecipient); + erc20.approve(address(proxy), collectPrice_pricePerToken); + proxy.collectionPriceOnClaim( + saleRecipient, + collectPrice_quantityToClaim, + collectPrice_currency, + collectPrice_pricePerToken + ); + + uint256 balanceSaleRecipientAfter = erc20.balanceOf(saleRecipient); + uint256 platformFeeRecipientAfter = erc20.balanceOf(platformFeeRecipient); + uint256 defaultFeeRecipientAfter = erc20.balanceOf(defaultFeeRecipient); + uint256 defaultPlatformFeeVal = (collectPrice_pricePerToken * 100) / MAX_BPS; + uint256 expectedPlatformFee = (collectPrice_pricePerToken * platformFeeBps) / MAX_BPS; + uint256 expectedSaleRecipientProceed = collectPrice_pricePerToken - expectedPlatformFee - defaultPlatformFeeVal; + + assertEq(balanceSaleRecipientAfter - balanceSaleRecipientBefore, expectedSaleRecipientProceed); + assertEq(platformFeeRecipientAfter - platformFeeRecipientBefore, expectedPlatformFee); + assertEq(defaultFeeRecipientAfter - defaultFeeRecipientBefore, defaultPlatformFeeVal); + } +} diff --git a/src/test/drop/drop-erc721/_collectPriceOnClaim/_collectPriceOnClaim.tree b/src/test/drop/drop-erc721/_collectPriceOnClaim/_collectPriceOnClaim.tree new file mode 100644 index 000000000..b962e7d4e --- /dev/null +++ b/src/test/drop/drop-erc721/_collectPriceOnClaim/_collectPriceOnClaim.tree @@ -0,0 +1,20 @@ +function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken +) +├── when _pricePerToken is equal to zero +│ └── when msg.value does not equal to zero +│ └── it should revert ✅ +└── when _pricePerToken is not equal to zero + └── when _primarySaleRecipient is equal to address(0) + ├── when _currency is native token + │ ├── when msg.value does not equal to totalPrice + │ │ └── it should revert ✅ + │ └── when msg.value does equal to totalPrice + │ ├── it should transfer platformFees to platformFeeRecipient in native token ✅ + │ └── it should transfer totalPrice - platformFees to saleRecipient in native token ✅ + └── when _currency is not native token + ├── it should transfer platformFees to platformFeeRecipient in _currency token ✅ + └── it should transfer totalPrice - platformFees to saleRecipient in _currency token ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc721/_transferTokensOnClaim/_tranferTokensOnClaim.tree b/src/test/drop/drop-erc721/_transferTokensOnClaim/_tranferTokensOnClaim.tree new file mode 100644 index 000000000..6bf501586 --- /dev/null +++ b/src/test/drop/drop-erc721/_transferTokensOnClaim/_tranferTokensOnClaim.tree @@ -0,0 +1,2 @@ +function _transferTokensOnClaim(address _to, uint256 _quantityBeingClaimed) +└── it should mint `_quantityBeingClaimed` number of tokens to `to` ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc721/_transferTokensOnClaim/_transferTokensOnClaim.t.sol b/src/test/drop/drop-erc721/_transferTokensOnClaim/_transferTokensOnClaim.t.sol new file mode 100644 index 000000000..87e98d606 --- /dev/null +++ b/src/test/drop/drop-erc721/_transferTokensOnClaim/_transferTokensOnClaim.t.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC721 } from "contracts/prebuilts/drop/DropERC721.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "erc721a-upgradeable/contracts/IERC721AUpgradeable.sol"; +import "../../../utils/BaseTest.sol"; + +contract HarnessDropERC721 is DropERC721 { + function transferTokensOnClaim(address _to, uint256 _quantityToClaim) public payable { + _transferTokensOnClaim(_to, _quantityToClaim); + } +} + +contract DropERC721Test_transferTokensOnClaim is BaseTest { + address public dropImp; + HarnessDropERC721 public proxy; + + address private transferTokens_receiver; + + ERC20 private nonReceiver; + + function setUp() public override { + super.setUp(); + + bytes memory initializeData = abi.encodeCall( + DropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ); + + dropImp = address(new HarnessDropERC721()); + proxy = HarnessDropERC721(address(new TWProxy(dropImp, initializeData))); + + nonReceiver = new ERC20("", ""); + } + + modifier transferToEOA() { + transferTokens_receiver = address(0x111); + _; + } + + modifier transferToNonReceiver() { + transferTokens_receiver = address(nonReceiver); + _; + } + + /*/////////////////////////////////////////////////////////////// + Branch Testing + //////////////////////////////////////////////////////////////*/ + + function test_revert_transferToNonReceiver() public transferToNonReceiver { + vm.expectRevert(IERC721AUpgradeable.TransferToNonERC721ReceiverImplementer.selector); + proxy.transferTokensOnClaim(transferTokens_receiver, 1); + } + + function test_transferToEOA() public transferToEOA { + uint256 eoaBalanceBefore = proxy.balanceOf(transferTokens_receiver); + uint256 supplyBefore = proxy.totalSupply(); + proxy.transferTokensOnClaim(transferTokens_receiver, 1); + assertEq(proxy.totalSupply(), supplyBefore + 1); + assertEq(proxy.balanceOf(transferTokens_receiver), eoaBalanceBefore + 1); + } +} diff --git a/src/test/drop/drop-erc721/freezeBatchBaseURI/freezeBatchBaseURI.t.sol b/src/test/drop/drop-erc721/freezeBatchBaseURI/freezeBatchBaseURI.t.sol new file mode 100644 index 000000000..9cb5c23c2 --- /dev/null +++ b/src/test/drop/drop-erc721/freezeBatchBaseURI/freezeBatchBaseURI.t.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC721, BatchMintMetadata } from "contracts/prebuilts/drop/DropERC721.sol"; + +// Test imports +import "../../../utils/BaseTest.sol"; + +contract DropERC721Test_freezeBatchBaseURI is BaseTest { + event MetadataFrozen(); + + DropERC721 public drop; + + bytes private freeze_data; + string private freeze_baseURI; + uint256 private freeze_amount; + bytes private freeze_encryptedURI; + bytes32 private freeze_provenanceHash; + string private freeze_revealedURI; + bytes private freeze_key; + address private unauthorized = address(0x123); + + function setUp() public override { + super.setUp(); + drop = DropERC721(getContract("DropERC721")); + } + + /*/////////////////////////////////////////////////////////////// + Branch Testing + //////////////////////////////////////////////////////////////*/ + + modifier callerWithoutMetadataRole() { + vm.startPrank(unauthorized); + _; + } + + modifier callerWithMetadataRole() { + vm.startPrank(deployer); + _; + } + + modifier lazyMintEncrypted() { + freeze_amount = 10; + freeze_baseURI = "ipfs://"; + freeze_revealedURI = "ipfs://revealed"; + freeze_key = "key"; + freeze_encryptedURI = drop.encryptDecrypt(bytes(freeze_revealedURI), freeze_key); + freeze_provenanceHash = keccak256(abi.encodePacked(freeze_revealedURI, freeze_key, block.chainid)); + freeze_data = abi.encode(freeze_encryptedURI, freeze_provenanceHash); + vm.prank(deployer); + drop.lazyMint(freeze_amount, freeze_baseURI, freeze_data); + _; + } + + modifier lazyMintUnEncryptedEmptyBaseURI() { + freeze_amount = 10; + freeze_baseURI = ""; + vm.prank(deployer); + drop.lazyMint(freeze_amount, freeze_baseURI, freeze_data); + _; + } + + modifier lazyMintUnEncryptedRegularBaseURI() { + freeze_amount = 10; + freeze_baseURI = "ipfs://"; + vm.prank(deployer); + drop.lazyMint(freeze_amount, freeze_baseURI, freeze_data); + _; + } + + function test_revert_NoMetadataRole() public callerWithoutMetadataRole { + bytes32 role = keccak256("METADATA_ROLE"); + vm.expectRevert( + abi.encodeWithSelector(Permissions.PermissionsUnauthorizedAccount.selector, unauthorized, role) + ); + drop.freezeBatchBaseURI(0); + } + + function test_revert_EncryptedBatch() public lazyMintEncrypted callerWithMetadataRole { + vm.expectRevert("Encrypted batch"); + drop.freezeBatchBaseURI(0); + } + + function test_revert_EmptyBaseURI() public lazyMintUnEncryptedEmptyBaseURI callerWithMetadataRole { + vm.expectRevert( + abi.encodeWithSelector(BatchMintMetadata.BatchMintInvalidBatchId.selector, drop.getBatchIdAtIndex(0)) + ); + drop.freezeBatchBaseURI(0); + } + + function test_state() public lazyMintUnEncryptedRegularBaseURI callerWithMetadataRole { + uint256 batchId = drop.getBatchIdAtIndex(0); + drop.freezeBatchBaseURI(0); + assertEq(drop.batchFrozen(batchId), true); + } + + function test_event() public lazyMintUnEncryptedRegularBaseURI callerWithMetadataRole { + vm.expectEmit(false, false, false, false); + emit MetadataFrozen(); + drop.freezeBatchBaseURI(0); + } +} diff --git a/src/test/drop/drop-erc721/freezeBatchBaseURI/freezeBatchBaseURI.tree b/src/test/drop/drop-erc721/freezeBatchBaseURI/freezeBatchBaseURI.tree new file mode 100644 index 000000000..9c29a9a83 --- /dev/null +++ b/src/test/drop/drop-erc721/freezeBatchBaseURI/freezeBatchBaseURI.tree @@ -0,0 +1,12 @@ +function freezeBatchBaseURI(uint256 _index) +├── when called by a user without the METADATA_ROLE +│ └── it should revert ✅ +└── when called by a user with the METADATA_ROLE + ├── when the batchId for the provided _index is an encrypted batch + │ └── it should revert ✅ + └── when the batchId for the provided _index is not an encrypted batch + ├── when the baseURI for the batchId is empty + │ └── it should revert ✅ + └── when the baseURI for the batchId is not empty + ├── it should set batchFrozen[batchId] as true ✅ + └── it should emit MetadataFrozen ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc721/initalizer/initializer.t.sol b/src/test/drop/drop-erc721/initalizer/initializer.t.sol new file mode 100644 index 000000000..5ef6ba8f1 --- /dev/null +++ b/src/test/drop/drop-erc721/initalizer/initializer.t.sol @@ -0,0 +1,407 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC721, PlatformFee, Royalty } from "contracts/prebuilts/drop/DropERC721.sol"; + +// Test imports +import "../../../utils/BaseTest.sol"; + +contract DropERC721Test_initializer is BaseTest { + DropERC721 public newDropContract; + + event ContractURIUpdated(string prevURI, string newURI); + event OwnerUpdated(address indexed prevOwner, address indexed newOwner); + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole); + event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps); + event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps); + event PrimarySaleRecipientUpdated(address indexed recipient); + + function setUp() public override { + super.setUp(); + } + + modifier royaltyBPSTooHigh() { + uint128 royaltyBps = 10001; + _; + } + + modifier platformFeeBPSTooHigh() { + uint128 platformFeeBps = 10001; + _; + } + + function test_state() public { + deployContractProxy( + "DropERC721", + abi.encodeCall( + DropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + + newDropContract = DropERC721(getContract("DropERC721")); + (address _platformFeeRecipient, uint128 _platformFeeBps) = newDropContract.getPlatformFeeInfo(); + (address _royaltyRecipient, uint128 _royaltyBps) = newDropContract.getDefaultRoyaltyInfo(); + address _saleRecipient = newDropContract.primarySaleRecipient(); + + for (uint256 i = 0; i < forwarders().length; i++) { + assertEq(newDropContract.isTrustedForwarder(forwarders()[i]), true); + } + + assertEq(newDropContract.name(), NAME); + assertEq(newDropContract.symbol(), SYMBOL); + assertEq(newDropContract.contractURI(), CONTRACT_URI); + assertEq(newDropContract.owner(), deployer); + assertEq(_platformFeeRecipient, platformFeeRecipient); + assertEq(_platformFeeBps, platformFeeBps); + assertEq(_royaltyRecipient, royaltyRecipient); + assertEq(_royaltyBps, royaltyBps); + assertEq(_saleRecipient, saleRecipient); + } + + function test_revert_RoyaltyBPSTooHigh() public royaltyBPSTooHigh { + vm.expectRevert(abi.encodeWithSelector(Royalty.RoyaltyExceededMaxFeeBps.selector, 10_000, 10_001)); + deployContractProxy( + "DropERC721", + abi.encodeCall( + DropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + 10001, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + } + + function test_revert_PlatformFeeBPSTooHigh() public platformFeeBPSTooHigh { + vm.expectRevert(abi.encodeWithSelector(PlatformFee.PlatformFeeExceededMaxFeeBps.selector, 10_000, 10_001)); + deployContractProxy( + "DropERC721", + abi.encodeCall( + DropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + 10001, + platformFeeRecipient + ) + ) + ); + } + + function test_event_ContractURIUpdated() public { + vm.expectEmit(false, false, false, true); + emit ContractURIUpdated("", CONTRACT_URI); + deployContractProxy( + "DropERC721", + abi.encodeCall( + DropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + } + + function test_event_OwnerUpdated() public { + vm.expectEmit(true, true, false, false); + emit OwnerUpdated(address(0), deployer); + deployContractProxy( + "DropERC721", + abi.encodeCall( + DropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + } + + function test_event_RoleGrantedDefaultAdminRole() public { + bytes32 role = bytes32(0x00); + vm.expectEmit(true, true, true, false); + emit RoleGranted(role, deployer, factory); + deployContractProxy( + "DropERC721", + abi.encodeCall( + DropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + } + + function test_event_RoleGrantedMinterRole() public { + bytes32 role = keccak256("MINTER_ROLE"); + vm.expectEmit(true, true, true, false); + emit RoleGranted(role, deployer, factory); + deployContractProxy( + "DropERC721", + abi.encodeCall( + DropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + } + + function test_event_RoleGrantedTransferRole() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + vm.expectEmit(true, true, true, false); + emit RoleGranted(role, deployer, factory); + deployContractProxy( + "DropERC721", + abi.encodeCall( + DropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + } + + function test_event_RoleGrantedTransferRoleZeroAddress() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + vm.expectEmit(true, true, true, false); + emit RoleGranted(role, address(0), factory); + deployContractProxy( + "DropERC721", + abi.encodeCall( + DropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + } + + function test_event_RoleGrantedMetadataRole() public { + bytes32 role = keccak256("METADATA_ROLE"); + vm.expectEmit(true, true, true, false); + emit RoleGranted(role, deployer, factory); + deployContractProxy( + "DropERC721", + abi.encodeCall( + DropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + } + + function test_event_RoleAdminChangedMetadataRole() public { + bytes32 role = keccak256("METADATA_ROLE"); + vm.expectEmit(true, true, true, false); + emit RoleAdminChanged(role, bytes32(0x00), role); + deployContractProxy( + "DropERC721", + abi.encodeCall( + DropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + } + + function test_event_PlatformFeeInfoUpdated() public { + vm.expectEmit(true, false, false, true); + emit PlatformFeeInfoUpdated(platformFeeRecipient, platformFeeBps); + deployContractProxy( + "DropERC721", + abi.encodeCall( + DropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + } + + function test_event_DefaultRoyalty() public { + vm.expectEmit(true, false, false, true); + emit DefaultRoyalty(royaltyRecipient, royaltyBps); + deployContractProxy( + "DropERC721", + abi.encodeCall( + DropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + } + + function test_event_PrimarySaleRecipientUpdated() public { + vm.expectEmit(true, false, false, false); + emit PrimarySaleRecipientUpdated(saleRecipient); + deployContractProxy( + "DropERC721", + abi.encodeCall( + DropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + } + + function test_roleCheck() public { + deployContractProxy( + "DropERC721", + abi.encodeCall( + DropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ); + + newDropContract = DropERC721(getContract("DropERC721")); + + assertEq(newDropContract.hasRole(bytes32(0x00), deployer), true); + assertEq(newDropContract.hasRole(keccak256("MINTER_ROLE"), deployer), true); + assertEq(newDropContract.hasRole(keccak256("TRANSFER_ROLE"), deployer), true); + assertEq(newDropContract.hasRole(keccak256("TRANSFER_ROLE"), address(0)), true); + assertEq(newDropContract.hasRole(keccak256("METADATA_ROLE"), deployer), true); + + assertEq(newDropContract.getRoleAdmin(keccak256("METADATA_ROLE")), keccak256("METADATA_ROLE")); + } +} diff --git a/src/test/drop/drop-erc721/initalizer/initializer.tree b/src/test/drop/drop-erc721/initalizer/initializer.tree new file mode 100644 index 000000000..cec2fc97d --- /dev/null +++ b/src/test/drop/drop-erc721/initalizer/initializer.tree @@ -0,0 +1,53 @@ +function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _royaltyRecipient, + uint128 _royaltyBps, + uint128 _platformFeeBps, + address _platformFeeRecipient +) +├── when _trustedForwarders.length > 0 +│ └── it should set _trustedForwarder[_trustedForwarders[i]] as true for each address in _trustedForwarders ✅ +├── it should set _name as the value provided in _name ✅ +├── it should set _symbol as the value provided in _symbol ✅ +├── it should set _currentIndex as 0 +├── it should set contractURI as _contractURI ✅ +├── it should emit ContractURIUpdated with the parameters: prevURI, _uri ✅ +├── it should set _defaultAdmin as the owner of the contract ✅ +├── it should emit OwnerUpdated with the parameters: _prevOwner, _defaultAdmin ✅ +├── it should assign the role DEFAULT_ADMIN_ROLE to _defaultAdmin ✅ +├── it should emit RoleGranted with the parameters: DEFAULT_ADMIN_ROLE, _defaultAdmin, msg.sender ✅ +├── it should assign the role _minterRole to _defaultAdmin ✅ +├── it should emit RoleGranted with the parameters: _minterRole, _defaultAdmin, msg.sender ✅ +├── it should assign the role _transferRole to _defaultAdmin ✅ +├── it should emit RoleGranted with the parameters: _transferRole, _defaultAdmin, msg.sender ✅ +├── it should assign the role _transferRole to address(0) ✅ +├── it should emit RoleGranted with the parameters: _transferRole, address(0), msg.sender ✅ +├── it should assign the role _metadataRole to _defaultAdmin ✅ +├── it should emit RoleGranted with the parameters: _metadataRole, _defaultAdmin, msg.sender ✅ +├── it should set _getAdminRole[_metadataRole] to equal _metadataRole ✅ +├── it should emit RoleAdminChanged with the parameters _metadataRole, previousAdminRole, _metadataRole ✅ +├── when _platformFeeBps is greater than 10_000 +│ └── it should revert ✅ +├── when _platformFeeBps is less than or equal to 10_000 +│ ├── it should set platformFeeBps to uint16(_platformFeeBps) ✅ +│ ├── it should set platformFeeRecipient to _platformFeeRecipient ✅ +│ └── it should emit PlatformFeeInfoUpdated with the following parameters: _platformFeeRecipient, _platformFeeBps ✅ +├── when _royaltyBps is greater than 10_000 +│ └── it should revert ✅ +├── when _royaltyBps is less than or equal to 10_000 +│ ├── it should set royaltyRecipient as _royaltyRecipient ✅ +│ ├── it should set royaltyBps as uint16(_royaltyBps) ✅ +│ └── it should emit DefaultRoyalty with the parameters _royaltyRecipient, _royaltyBps ✅ +├── it should set recipient as _primarySaleRecipient ✅ +├── it should emit PrimarySaleRecipientUpdated with the parameters _primarySaleRecipient ✅ +├── it should set transferRole as keccak256("TRANSFER_ROLE") +├── it should set minterRole as keccak256("MINTER_ROLE") +└── it should set metadataRole as keccak256("METADATA_ROLE") + + + diff --git a/src/test/drop/drop-erc721/lazyMint/lazyMint.t.sol b/src/test/drop/drop-erc721/lazyMint/lazyMint.t.sol new file mode 100644 index 000000000..921976be7 --- /dev/null +++ b/src/test/drop/drop-erc721/lazyMint/lazyMint.t.sol @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC721, LazyMint } from "contracts/prebuilts/drop/DropERC721.sol"; + +// Test imports +import "../../../utils/BaseTest.sol"; + +contract DropERC721Test_lazyMint is BaseTest { + event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI); + + DropERC721 public drop; + + bytes private lazymint_data; + uint256 private lazyMint_amount; + bytes private lazyMint_encryptedURI; + bytes32 private lazyMint_provenanceHash; + string private lazyMint_revealedURI = "test"; + + function setUp() public override { + super.setUp(); + drop = DropERC721(getContract("DropERC721")); + } + + /*/////////////////////////////////////////////////////////////// + Branch Testing + //////////////////////////////////////////////////////////////*/ + + modifier callerWithoutMinterRole() { + vm.startPrank(address(0x123)); + _; + } + + modifier callerWithMinterRole() { + vm.startPrank(deployer); + _; + } + + modifier amountEqualZero() { + lazyMint_amount = 0; + _; + } + + modifier amountNotEqualZero() { + lazyMint_amount = 1; + _; + } + + modifier dataLengthZero() { + lazymint_data = abi.encode(""); + _; + } + + modifier dataInvalidFormat() { + lazyMint_provenanceHash = bytes32("provenanceHash"); + lazymint_data = abi.encode(lazyMint_provenanceHash); + console.log(lazymint_data.length); + _; + } + + modifier dataValidFormat() { + lazyMint_provenanceHash = bytes32("provenanceHash"); + lazyMint_encryptedURI = "encryptedURI"; + lazymint_data = abi.encode(lazyMint_encryptedURI, lazyMint_provenanceHash); + console.log(lazymint_data.length); + _; + } + + modifier dataValidFormatNoURI() { + lazyMint_provenanceHash = bytes32("provenanceHash"); + lazyMint_encryptedURI = ""; + lazymint_data = abi.encode(lazyMint_encryptedURI, lazyMint_provenanceHash); + console.log(lazymint_data.length); + _; + } + + modifier dataValidFormatNoHash() { + lazyMint_provenanceHash = bytes32(""); + lazyMint_encryptedURI = "encryptedURI"; + lazymint_data = abi.encode(lazyMint_encryptedURI, lazyMint_provenanceHash); + console.log(lazymint_data.length); + _; + } + + function test_revert_NoMinterRole() public callerWithoutMinterRole dataLengthZero { + vm.expectRevert(abi.encodeWithSelector(LazyMint.LazyMintUnauthorized.selector)); + drop.lazyMint(lazyMint_amount, lazyMint_revealedURI, lazymint_data); + } + + function test_revert_AmountEqualZero() public callerWithMinterRole dataLengthZero amountEqualZero { + vm.expectRevert(abi.encodeWithSelector(LazyMint.LazyMintInvalidAmount.selector)); + drop.lazyMint(lazyMint_amount, lazyMint_revealedURI, lazymint_data); + } + + function test_revert_DataInvalidFormat() public callerWithMinterRole amountNotEqualZero dataInvalidFormat { + vm.expectRevert(); + drop.lazyMint(lazyMint_amount, lazyMint_revealedURI, lazymint_data); + } + + function test_state_dataLengthZero() public callerWithMinterRole amountNotEqualZero dataLengthZero { + uint256 nextTokenIdToLazyMintBefore = drop.nextTokenIdToMint(); + uint256 expectedBatchId = nextTokenIdToLazyMintBefore + lazyMint_amount; + + uint256 batchIdReturn = drop.lazyMint(lazyMint_amount, lazyMint_revealedURI, lazymint_data); + + uint256 batchIdState = drop.getBatchIdAtIndex(0); + string memory baseURIState = drop.tokenURI(0); + + assertEq(nextTokenIdToLazyMintBefore + lazyMint_amount, drop.nextTokenIdToMint()); + assertEq(expectedBatchId, batchIdReturn); + assertEq(expectedBatchId, batchIdState); + assertEq(string(abi.encodePacked(lazyMint_revealedURI, "0")), baseURIState); + } + + function test_event_dataLengthZero() public callerWithMinterRole amountNotEqualZero dataLengthZero { + uint256 nextTokenIdToLazyMintBefore = drop.nextTokenIdToMint(); + + vm.expectEmit(true, false, false, true); + emit TokensLazyMinted( + nextTokenIdToLazyMintBefore, + nextTokenIdToLazyMintBefore + lazyMint_amount - 1, + lazyMint_revealedURI, + lazymint_data + ); + drop.lazyMint(lazyMint_amount, lazyMint_revealedURI, lazymint_data); + } + + function test_state_noEncryptedURI() public callerWithMinterRole amountNotEqualZero dataValidFormatNoURI { + uint256 nextTokenIdToLazyMintBefore = drop.nextTokenIdToMint(); + uint256 expectedBatchId = nextTokenIdToLazyMintBefore + lazyMint_amount; + bytes memory expectedEncryptedData; + + uint256 batchIdReturn = drop.lazyMint(lazyMint_amount, lazyMint_revealedURI, lazymint_data); + + uint256 batchIdState = drop.getBatchIdAtIndex(0); + string memory baseURIState = drop.tokenURI(0); + bytes memory encryptedDataState = drop.encryptedData(0); + + assertEq(nextTokenIdToLazyMintBefore + lazyMint_amount, drop.nextTokenIdToMint()); + assertEq(expectedBatchId, batchIdReturn); + assertEq(expectedBatchId, batchIdState); + assertEq(string(abi.encodePacked(lazyMint_revealedURI, "0")), baseURIState); + assertEq(expectedEncryptedData, encryptedDataState); + } + + function test_event_noEncryptedURI() public callerWithMinterRole amountNotEqualZero dataValidFormatNoURI { + uint256 nextTokenIdToLazyMintBefore = drop.nextTokenIdToMint(); + + vm.expectEmit(true, false, false, true); + emit TokensLazyMinted( + nextTokenIdToLazyMintBefore, + nextTokenIdToLazyMintBefore + lazyMint_amount - 1, + lazyMint_revealedURI, + lazymint_data + ); + drop.lazyMint(lazyMint_amount, lazyMint_revealedURI, lazymint_data); + } + + function test_state_noProvenanceHash() public callerWithMinterRole amountNotEqualZero dataValidFormatNoHash { + uint256 nextTokenIdToLazyMintBefore = drop.nextTokenIdToMint(); + uint256 expectedBatchId = nextTokenIdToLazyMintBefore + lazyMint_amount; + bytes memory expectedEncryptedData; + + uint256 batchIdReturn = drop.lazyMint(lazyMint_amount, lazyMint_revealedURI, lazymint_data); + + uint256 batchIdState = drop.getBatchIdAtIndex(0); + string memory baseURIState = drop.tokenURI(0); + bytes memory encryptedDataState = drop.encryptedData(0); + + assertEq(nextTokenIdToLazyMintBefore + lazyMint_amount, drop.nextTokenIdToMint()); + assertEq(expectedBatchId, batchIdReturn); + assertEq(expectedBatchId, batchIdState); + assertEq(string(abi.encodePacked(lazyMint_revealedURI, "0")), baseURIState); + assertEq(expectedEncryptedData, encryptedDataState); + } + + function test_event_noProvenanceHash() public callerWithMinterRole amountNotEqualZero dataValidFormatNoHash { + uint256 nextTokenIdToLazyMintBefore = drop.nextTokenIdToMint(); + + vm.expectEmit(true, false, false, true); + emit TokensLazyMinted( + nextTokenIdToLazyMintBefore, + nextTokenIdToLazyMintBefore + lazyMint_amount - 1, + lazyMint_revealedURI, + lazymint_data + ); + drop.lazyMint(lazyMint_amount, lazyMint_revealedURI, lazymint_data); + } + + function test_state_encryptedURIAndHash() public callerWithMinterRole amountNotEqualZero dataValidFormat { + uint256 nextTokenIdToLazyMintBefore = drop.nextTokenIdToMint(); + uint256 expectedBatchId = nextTokenIdToLazyMintBefore + lazyMint_amount; + + uint256 batchIdReturn = drop.lazyMint(lazyMint_amount, lazyMint_revealedURI, lazymint_data); + + uint256 batchIdState = drop.getBatchIdAtIndex(0); + string memory baseURIState = drop.tokenURI(0); + bytes memory encryptedDataState = drop.encryptedData(batchIdReturn); + + assertEq(nextTokenIdToLazyMintBefore + lazyMint_amount, drop.nextTokenIdToMint()); + assertEq(expectedBatchId, batchIdReturn); + assertEq(expectedBatchId, batchIdState); + assertEq(string(abi.encodePacked(lazyMint_revealedURI, "0")), baseURIState); + assertEq(lazymint_data, encryptedDataState); + } + + function test_event_encryptedURIAndHash() public callerWithMinterRole amountNotEqualZero dataValidFormat { + uint256 nextTokenIdToLazyMintBefore = drop.nextTokenIdToMint(); + + vm.expectEmit(true, false, false, true); + emit TokensLazyMinted( + nextTokenIdToLazyMintBefore, + nextTokenIdToLazyMintBefore + lazyMint_amount - 1, + lazyMint_revealedURI, + lazymint_data + ); + drop.lazyMint(lazyMint_amount, lazyMint_revealedURI, lazymint_data); + } +} diff --git a/src/test/drop/drop-erc721/lazyMint/lazyMint.tree b/src/test/drop/drop-erc721/lazyMint/lazyMint.tree new file mode 100644 index 000000000..84738d926 --- /dev/null +++ b/src/test/drop/drop-erc721/lazyMint/lazyMint.tree @@ -0,0 +1,33 @@ +function lazyMint( + uint256 _amount, + string calldata _baseURIForTokens, + bytes calldata _data +) +├── when called by a user without MINTER_ROLE +│ └── it should revert ✅ +└── when called by a user with MINTER_ROLE + ├── when _data.length == 0 + │ ├── when _amount is equal to 0 + │ │ └── it should revert ✅ + │ └── when _amount is greater than 0 + │ ├── it should push batchId (_startId + _amountToMint) to the batchIds array ✅ + │ ├── it should set baseURI[batchId] as _baseURIForTokens ✅ + │ └── it should emit TokensLazyMinted with the parameters: startId, startId + amount - 1, _baseURIForTokens, _data ✅ + └── when _data.length > 0 + ├── when _data invalid format + │ └── it should revert ✅ + └── when _data valid format + ├── it should decode _data into bytes memory encryptedURI and bytes32 provenanceHash ✅ + ├── when encryptedURI.length = 0 + │ ├── it should push batchId (_startId + _amountToMint) to the batchIds array ✅ + │ ├── it should set baseURI[batchId] as _baseURIForTokens ✅ + │ └── it should emit TokensLazyMinted with the parameters: startId, startId + amount - 1, _baseURIForTokens, _data ✅ + ├── when provenanceHash = "" + │ ├── it should push batchId (_startId + _amountToMint) to the batchIds array ✅ + │ ├── it should set baseURI[batchId] as _baseURIForTokens ✅ + │ └── it should emit TokensLazyMinted with the parameters: startId, startId + amount - 1, _baseURIForTokens, _data ✅ + └── when encryptedURI.length > 0 and provenanceHash does not equal "" + ├── it should set the encryptedData[batchId] equal to _data ✅ + ├── it should push batchId (_startId + _amountToMint) to the batchIds array ✅ + ├── it should set baseURI[batchId] as _baseURIForTokens ✅ + └── it should emit TokensLazyMinted with the parameters: startId, startId + amount - 1, _baseURIForTokens, _data ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc721/miscellaneous/miscellaneous.t.sol b/src/test/drop/drop-erc721/miscellaneous/miscellaneous.t.sol new file mode 100644 index 000000000..9ec52d8f8 --- /dev/null +++ b/src/test/drop/drop-erc721/miscellaneous/miscellaneous.t.sol @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC721 } from "contracts/prebuilts/drop/DropERC721.sol"; + +// Test imports +import "erc721a-upgradeable/contracts/IERC721AUpgradeable.sol"; +import "../../../utils/BaseTest.sol"; +import "../../../../../lib/openzeppelin-contracts-upgradeable/contracts/interfaces/IERC2981Upgradeable.sol"; + +contract DropERC721Test_misc is BaseTest { + DropERC721 public drop; + + bytes private misc_data; + string private misc_baseURI; + uint256 private misc_amount; + bytes private misc_encryptedURI; + bytes32 private misc_provenanceHash; + string private misc_revealedURI; + uint256 private misc_index; + bytes private misc_key; + address private unauthorized = address(0x123); + + function setUp() public override { + super.setUp(); + drop = DropERC721(getContract("DropERC721")); + } + + /*/////////////////////////////////////////////////////////////// + Branch Testing + //////////////////////////////////////////////////////////////*/ + + modifier callerNotApproved() { + vm.startPrank(unauthorized); + _; + } + + modifier callerOwner() { + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + vm.startPrank(receiver); + _; + } + + modifier callerApproved() { + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + vm.prank(receiver); + drop.setApprovalForAll(deployer, true); + vm.startPrank(deployer); + _; + } + + modifier validIndex() { + misc_index = 0; + _; + } + + modifier invalidKey() { + misc_key = "invalidKey"; + _; + } + + modifier lazyMintEncrypted() { + misc_amount = 10; + misc_baseURI = "ipfs://"; + misc_revealedURI = "ipfs://revealed"; + misc_key = "key"; + misc_encryptedURI = drop.encryptDecrypt(bytes(misc_revealedURI), misc_key); + misc_provenanceHash = keccak256(abi.encodePacked(misc_revealedURI, misc_key, block.chainid)); + misc_data = abi.encode(misc_encryptedURI, misc_provenanceHash); + vm.prank(deployer); + drop.lazyMint(misc_amount, misc_baseURI, misc_data); + _; + } + + modifier tokenClaimed() { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "0"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + DropERC721.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 0; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, 10, address(erc20), 0, alp, ""); // claims for free, because allowlist price is 0 + _; + } + + function test_totalMinted_TenLazyMintedZeroClaim() public lazyMintEncrypted { + uint256 totalMinted = drop.totalMinted(); + assertEq(totalMinted, 0); + } + + function test_totalMinted_TenLazyMintedTenClaim() public lazyMintEncrypted tokenClaimed { + uint256 totalMinted = drop.totalMinted(); + assertEq(totalMinted, 10); + } + + function test_nextTokenIdToMint_ZeroLazyMinted() public { + uint256 nextTokenIdToMint = drop.nextTokenIdToMint(); + assertEq(nextTokenIdToMint, 0); + } + + function test_nextTokenIdToMint_TenLazyMinted() public lazyMintEncrypted { + uint256 nextTokenIdToMint = drop.nextTokenIdToMint(); + assertEq(nextTokenIdToMint, 10); + } + + function test_nextTokenIdToClaim_ZeroClaimed() public { + uint256 nextTokenIdToClaim = drop.nextTokenIdToClaim(); + assertEq(nextTokenIdToClaim, 0); + } + + function test_nextTokenIdToClaim_TenClaimed() public lazyMintEncrypted tokenClaimed { + uint256 nextTokenIdToClaim = drop.nextTokenIdToClaim(); + assertEq(nextTokenIdToClaim, 10); + } + + function test_burn_revert_callerNotApproved() public lazyMintEncrypted tokenClaimed callerNotApproved { + vm.expectRevert(IERC721AUpgradeable.TransferCallerNotOwnerNorApproved.selector); + drop.burn(0); + } + + function test_burn_CallerApproved() public lazyMintEncrypted tokenClaimed callerApproved { + drop.burn(0); + uint256 totalSupply = drop.totalSupply(); + assertEq(totalSupply, 9); + vm.expectRevert(IERC721AUpgradeable.OwnerQueryForNonexistentToken.selector); + drop.ownerOf(0); + } + + function test_burn_revert_callerOwnerOfToken() public lazyMintEncrypted tokenClaimed callerOwner { + drop.burn(0); + uint256 totalSupply = drop.totalSupply(); + assertEq(totalSupply, 9); + vm.expectRevert(IERC721AUpgradeable.OwnerQueryForNonexistentToken.selector); + drop.ownerOf(0); + } + + function test_contractType() public { + assertEq(drop.contractType(), bytes32("DropERC721")); + } + + function test_contractVersion() public { + assertEq(drop.contractVersion(), uint8(4)); + } + + function test_supportsInterface() public { + assertEq(drop.supportsInterface(type(IERC2981Upgradeable).interfaceId), true); + assertEq(drop.supportsInterface(type(IERC721Upgradeable).interfaceId), true); + assertEq(drop.supportsInterface(type(IERC721MetadataUpgradeable).interfaceId), true); + } + + function test__msgData() public { + HarnessDropERC721MsgData msgDataDrop = new HarnessDropERC721MsgData(); + bytes memory msgData = msgDataDrop.msgData(); + bytes4 expectedData = msgDataDrop.msgData.selector; + assertEq(bytes4(msgData), expectedData); + } +} + +contract HarnessDropERC721MsgData is DropERC721 { + function msgData() public view returns (bytes memory) { + return _msgData(); + } +} diff --git a/src/test/drop/drop-erc721/miscellaneous/miscellaneous.tree b/src/test/drop/drop-erc721/miscellaneous/miscellaneous.tree new file mode 100644 index 000000000..0e15c80ad --- /dev/null +++ b/src/test/drop/drop-erc721/miscellaneous/miscellaneous.tree @@ -0,0 +1,23 @@ +function totalMinted() +└── it should return total number of minted tokens ✅ + +function nextTokenIdToMint() +└── it should return the next tokenId that is to be lazy minted ✅ + +function nextTokenIdToClaim() +└── it should return the next tokenId to be minted ✅ + +function burn(uint256 tokenId) +├── when caller is not the owner of tokenId +│ ├── when caller is not an approved operator of the owner of tokenId +│ │ └── it should revert ✅ +│ └── when caller is an approved operator of the owner of tokenId +│ └── it should burn the token ✅ +└── when caller is the owner of tokenId + └── it should burn the token ✅ + +function contractType() +└── it should return "DropERC721" in bytes32 format ✅ + +function contractVersion() +└── it should return 4 in uint8 format ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc721/reveal/reveal.t.sol b/src/test/drop/drop-erc721/reveal/reveal.t.sol new file mode 100644 index 000000000..998c7ee27 --- /dev/null +++ b/src/test/drop/drop-erc721/reveal/reveal.t.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC721, BatchMintMetadata, DelayedReveal } from "contracts/prebuilts/drop/DropERC721.sol"; + +// Test imports + +import "../../../utils/BaseTest.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; + +contract DropERC721Test_reveal is BaseTest { + using Strings for uint256; + + event TokenURIRevealed(uint256 indexed index, string revealedURI); + + DropERC721 public drop; + + bytes private reveal_data; + string private reveal_baseURI; + uint256 private reveal_amount; + bytes private reveal_encryptedURI; + bytes32 private reveal_provenanceHash; + string private reveal_revealedURI; + uint256 private reveal_index; + bytes private reveal_key; + address private unauthorized = address(0x123); + + function setUp() public override { + super.setUp(); + drop = DropERC721(getContract("DropERC721")); + } + + /*/////////////////////////////////////////////////////////////// + Branch Testing + //////////////////////////////////////////////////////////////*/ + + modifier callerWithoutMetadataRole() { + vm.startPrank(unauthorized); + _; + } + + modifier callerWithMetadataRole() { + vm.startPrank(deployer); + _; + } + + modifier validIndex() { + reveal_index = 0; + _; + } + + modifier invalidKey() { + reveal_key = "invalidKey"; + _; + } + + modifier invalidIndex() { + reveal_index = 1; + _; + } + + modifier lazyMintEncrypted() { + reveal_amount = 10; + reveal_baseURI = "ipfs://"; + reveal_revealedURI = "ipfs://revealed"; + reveal_key = "key"; + reveal_encryptedURI = drop.encryptDecrypt(bytes(reveal_revealedURI), reveal_key); + reveal_provenanceHash = keccak256(abi.encodePacked(reveal_revealedURI, reveal_key, block.chainid)); + reveal_data = abi.encode(reveal_encryptedURI, reveal_provenanceHash); + vm.prank(deployer); + drop.lazyMint(reveal_amount, reveal_baseURI, reveal_data); + _; + } + + modifier lazyMintUnEncrypted() { + reveal_amount = 10; + reveal_baseURI = "ipfs://"; + vm.prank(deployer); + drop.lazyMint(reveal_amount, reveal_baseURI, reveal_data); + _; + } + + function test_revert_NoMetadataRole() public callerWithoutMetadataRole { + bytes32 role = keccak256("METADATA_ROLE"); + + vm.expectRevert( + abi.encodeWithSelector(Permissions.PermissionsUnauthorizedAccount.selector, unauthorized, role) + ); + drop.reveal(reveal_index, reveal_key); + } + + function test_state() public validIndex lazyMintEncrypted callerWithMetadataRole { + for (uint256 i = 0; i < reveal_amount; i += 1) { + string memory uri = drop.tokenURI(i); + assertEq(uri, string(abi.encodePacked(reveal_baseURI, "0"))); + } + + string memory revealedURI = drop.reveal(reveal_index, reveal_key); + assertEq(revealedURI, string(reveal_revealedURI)); + + for (uint256 i = 0; i < reveal_amount; i += 1) { + string memory uri = drop.tokenURI(i); + assertEq(uri, string(abi.encodePacked(reveal_revealedURI, i.toString()))); + } + + assertEq(drop.encryptedData(reveal_index), ""); + } + + function test_event() public validIndex lazyMintEncrypted callerWithMetadataRole { + vm.expectEmit(); + emit TokenURIRevealed(reveal_index, reveal_revealedURI); + drop.reveal(reveal_index, reveal_key); + } + + function test_revert_InvalidIndex() public invalidIndex lazyMintEncrypted callerWithMetadataRole { + vm.expectRevert(abi.encodeWithSelector(BatchMintMetadata.BatchMintInvalidBatchId.selector, reveal_index)); + drop.reveal(reveal_index, reveal_key); + } + + function test_revert_InvalidKey() public validIndex lazyMintEncrypted invalidKey callerWithMetadataRole { + string memory incorrectURI = string(drop.encryptDecrypt(reveal_encryptedURI, reveal_key)); + + vm.expectRevert( + abi.encodeWithSelector( + DelayedReveal.DelayedRevealIncorrectResultHash.selector, + reveal_provenanceHash, + keccak256(abi.encodePacked(incorrectURI, reveal_key, block.chainid)) + ) + ); + drop.reveal(reveal_index, reveal_key); + } + + function test_revert_NoEncryptedData() public validIndex lazyMintUnEncrypted callerWithMetadataRole { + vm.expectRevert(abi.encodeWithSelector(DelayedReveal.DelayedRevealNothingToReveal.selector)); + drop.reveal(reveal_index, reveal_key); + } +} diff --git a/src/test/drop/drop-erc721/reveal/reveal.tree b/src/test/drop/drop-erc721/reveal/reveal.tree new file mode 100644 index 000000000..5e1e6717d --- /dev/null +++ b/src/test/drop/drop-erc721/reveal/reveal.tree @@ -0,0 +1,16 @@ +function reveal(uint256 _index, bytes calldata _key) +├── when called by a user without the METADATA_ROLE +│ └── it should revert ✅ +└── when called by a user with the METADATA_ROLE + ├── when called with an invalid index + │ └── it should revert ✅ + └── when called with a valid index + ├── when called on a batch with no encryptedData + │ └── it should revert ✅ + └── when called a batch with encryptedData + ├── when called with an invalid key + │ └── it should revert ✅ + └── when called with a valid key + ├── it should set encryptedData[(batchId of _index)] as a blank string ("") ✅ + ├── it should set the baseURI[(batchId of _index)] as the revealed uri ✅ + └── it should emit TokenURIRevealed with the following parameters: _index, revealed uri ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc721/setMaxTotalSupply/setMaxTotalSupply.t.sol b/src/test/drop/drop-erc721/setMaxTotalSupply/setMaxTotalSupply.t.sol new file mode 100644 index 000000000..592fa0fac --- /dev/null +++ b/src/test/drop/drop-erc721/setMaxTotalSupply/setMaxTotalSupply.t.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC721 } from "contracts/prebuilts/drop/DropERC721.sol"; + +// Test imports + +import "../../../utils/BaseTest.sol"; + +contract DropERC721Test_setMaxTotalSupply is BaseTest { + event MaxTotalSupplyUpdated(uint256 maxTotalSupply); + + DropERC721 public drop; + + address private unauthorized = address(0x123); + + function setUp() public override { + super.setUp(); + drop = DropERC721(getContract("DropERC721")); + } + + /*/////////////////////////////////////////////////////////////// + Branch Testing + //////////////////////////////////////////////////////////////*/ + + modifier callerNotAdmin() { + vm.startPrank(unauthorized); + _; + } + + modifier callerAdmin() { + vm.startPrank(deployer); + _; + } + + function test_revert_CallerNotAdmin() public callerNotAdmin { + bytes32 role = bytes32(0x00); + vm.expectRevert( + abi.encodeWithSelector(Permissions.PermissionsUnauthorizedAccount.selector, unauthorized, role) + ); + drop.setMaxTotalSupply(0); + } + + function test_state() public callerAdmin { + drop.setMaxTotalSupply(0); + assertEq(drop.maxTotalSupply(), 0); + } + + function test_event() public callerAdmin { + vm.expectEmit(false, false, false, false); + emit MaxTotalSupplyUpdated(0); + drop.setMaxTotalSupply(0); + } +} diff --git a/src/test/drop/drop-erc721/setMaxTotalSupply/setMaxTotalSupply.tree b/src/test/drop/drop-erc721/setMaxTotalSupply/setMaxTotalSupply.tree new file mode 100644 index 000000000..19631c92f --- /dev/null +++ b/src/test/drop/drop-erc721/setMaxTotalSupply/setMaxTotalSupply.tree @@ -0,0 +1,6 @@ +function setMaxTotalSupply(uint256 _maxTotalSupply) +├── when called by a user without the DEFAULT_ADMIN_ROLE +│ └── it should revert ✅ +└── when called by a user with the DEFAULT_ADMIN_ROLE + ├── it should set maxTotalSupply to _maxTotalSupply ✅ + └── it should emit MaxTotalSupplyUpdated with the following parameters: _maxTotalSupply ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc721/updateBatchBaseURI/updateBatchBaseURI.t.sol b/src/test/drop/drop-erc721/updateBatchBaseURI/updateBatchBaseURI.t.sol new file mode 100644 index 000000000..901d7a7b1 --- /dev/null +++ b/src/test/drop/drop-erc721/updateBatchBaseURI/updateBatchBaseURI.t.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC721, BatchMintMetadata, Permissions } from "contracts/prebuilts/drop/DropERC721.sol"; + +// Test imports + +import "../../../utils/BaseTest.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; + +contract DropERC721Test_updateBatchBaseURI is BaseTest { + using Strings for uint256; + + event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); + + DropERC721 public drop; + + bytes private updateBatch_data; + string private updateBatch_baseURI; + string private updateBatch_newBaseURI; + uint256 private updateBatch_amount; + bytes private updateBatch_encryptedURI; + bytes32 private updateBatch_provenanceHash; + string private updateBatch_revealedURI; + bytes private updateBatch_key; + address private unauthorized = address(0x123); + + function setUp() public override { + super.setUp(); + drop = DropERC721(getContract("DropERC721")); + } + + /*/////////////////////////////////////////////////////////////// + Branch Testing + //////////////////////////////////////////////////////////////*/ + + modifier callerWithoutMetadataRole() { + vm.startPrank(unauthorized); + _; + } + + modifier callerWithMetadataRole() { + vm.startPrank(deployer); + _; + } + + modifier lazyMintEncrypted() { + updateBatch_amount = 10; + updateBatch_baseURI = "ipfs://"; + updateBatch_revealedURI = "ipfs://revealed"; + updateBatch_key = "key"; + updateBatch_encryptedURI = drop.encryptDecrypt(bytes(updateBatch_revealedURI), updateBatch_key); + updateBatch_provenanceHash = keccak256( + abi.encodePacked(updateBatch_revealedURI, updateBatch_key, block.chainid) + ); + updateBatch_data = abi.encode(updateBatch_encryptedURI, updateBatch_provenanceHash); + vm.prank(deployer); + drop.lazyMint(updateBatch_amount, updateBatch_baseURI, updateBatch_data); + _; + } + + modifier lazyMintUnEncryptedEmptyBaseURI() { + updateBatch_amount = 10; + updateBatch_baseURI = ""; + vm.prank(deployer); + drop.lazyMint(updateBatch_amount, updateBatch_baseURI, updateBatch_data); + _; + } + + modifier lazyMintUnEncryptedRegularBaseURI() { + updateBatch_amount = 10; + updateBatch_baseURI = "ipfs://"; + vm.prank(deployer); + drop.lazyMint(updateBatch_amount, updateBatch_baseURI, updateBatch_data); + _; + } + + modifier batchFrozen() { + drop.freezeBatchBaseURI(0); + _; + } + + function test_revert_NoMetadataRole() public callerWithoutMetadataRole { + bytes32 role = keccak256("METADATA_ROLE"); + vm.expectRevert( + abi.encodeWithSelector(Permissions.PermissionsUnauthorizedAccount.selector, unauthorized, role) + ); + drop.updateBatchBaseURI(0, updateBatch_newBaseURI); + } + + function test_revert_EncryptedBatch() public lazyMintEncrypted callerWithMetadataRole { + vm.expectRevert("Encrypted batch"); + drop.updateBatchBaseURI(0, updateBatch_newBaseURI); + } + + function test_revert_FrozenBatch() public lazyMintUnEncryptedRegularBaseURI callerWithMetadataRole batchFrozen { + vm.expectRevert( + abi.encodeWithSelector(BatchMintMetadata.BatchMintMetadataFrozen.selector, drop.getBatchIdAtIndex(0)) + ); + drop.updateBatchBaseURI(0, updateBatch_newBaseURI); + } + + function test_state() public lazyMintUnEncryptedRegularBaseURI callerWithMetadataRole { + drop.updateBatchBaseURI(0, updateBatch_newBaseURI); + for (uint256 i = 0; i < updateBatch_amount; i += 1) { + string memory uri = drop.tokenURI(i); + assertEq(uri, string(abi.encodePacked(updateBatch_newBaseURI, i.toString()))); + } + } + + function test_event() public lazyMintUnEncryptedRegularBaseURI callerWithMetadataRole { + vm.expectEmit(false, false, false, false); + emit BatchMetadataUpdate(0, 10); + drop.updateBatchBaseURI(0, updateBatch_newBaseURI); + } +} diff --git a/src/test/drop/drop-erc721/updateBatchBaseURI/updateBatchBaseURI.tree b/src/test/drop/drop-erc721/updateBatchBaseURI/updateBatchBaseURI.tree new file mode 100644 index 000000000..b03e1198f --- /dev/null +++ b/src/test/drop/drop-erc721/updateBatchBaseURI/updateBatchBaseURI.tree @@ -0,0 +1,12 @@ +function updateBatchBaseURI(uint256 _index, string calldata _uri) +├── when called by a user without the METADATA_ROLE +│ └── it should revert ✅ +└── when called by a user with the METADATA_ROLE + ├── when the batchId for the provided _index is an encrypted batch + │ └── it should revert ✅ + └── when the batchId for the provided _index is not an encrypted batch + ├── when the batchId for the provided _index is frozen + │ └── it should revert ✅ + └── when the batchId for the provided _index is not frozen + ├── it should set the baseURI for the batchId as _uri ✅ + └── it should emit BatchMetadataUpdate with the following parameters: starting tokenId of batch, ending tokenId of batch ✅ \ No newline at end of file diff --git a/src/test/marketplace/DirectListings.t.sol b/src/test/marketplace/DirectListings.t.sol new file mode 100644 index 000000000..ead55d4f7 --- /dev/null +++ b/src/test/marketplace/DirectListings.t.sol @@ -0,0 +1,2163 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test helper imports +import "../utils/BaseTest.sol"; + +// Test contracts and interfaces +import { RoyaltyPaymentsLogic } from "contracts/extension/plugin/RoyaltyPayments.sol"; +import { MarketplaceV3, IPlatformFee } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { DirectListingsLogic } from "contracts/prebuilts/marketplace/direct-listings/DirectListingsLogic.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { ERC721Base } from "contracts/base/ERC721Base.sol"; +import { MockRoyaltyEngineV1 } from "../mocks/MockRoyaltyEngineV1.sol"; + +import { IDirectListings } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +contract MarketplaceDirectListingsTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + address private defaultFeeRecipient; + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles for seller and assets + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc721)); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc1155)); + + vm.stopPrank(); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(buyer, "Buyer"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `DirectListings` + address directListings = address(new DirectListingsLogic(address(weth))); + defaultFeeRecipient = 0x1Af20C6B23373350aD464700B5965CE4B0D2aD94; + vm.label(directListings, "DirectListings_Extension"); + + // Extension: DirectListingsLogic + Extension memory extension_directListings; + extension_directListings.metadata = ExtensionMetadata({ + name: "DirectListingsLogic", + metadataURI: "ipfs://DirectListings", + implementation: directListings + }); + + extension_directListings.functions = new ExtensionFunction[](13); + extension_directListings.functions[0] = ExtensionFunction( + DirectListingsLogic.totalListings.selector, + "totalListings()" + ); + extension_directListings.functions[1] = ExtensionFunction( + DirectListingsLogic.isBuyerApprovedForListing.selector, + "isBuyerApprovedForListing(uint256,address)" + ); + extension_directListings.functions[2] = ExtensionFunction( + DirectListingsLogic.isCurrencyApprovedForListing.selector, + "isCurrencyApprovedForListing(uint256,address)" + ); + extension_directListings.functions[3] = ExtensionFunction( + DirectListingsLogic.currencyPriceForListing.selector, + "currencyPriceForListing(uint256,address)" + ); + extension_directListings.functions[4] = ExtensionFunction( + DirectListingsLogic.createListing.selector, + "createListing((address,uint256,uint256,address,uint256,uint128,uint128,bool))" + ); + extension_directListings.functions[5] = ExtensionFunction( + DirectListingsLogic.updateListing.selector, + "updateListing(uint256,(address,uint256,uint256,address,uint256,uint128,uint128,bool))" + ); + extension_directListings.functions[6] = ExtensionFunction( + DirectListingsLogic.cancelListing.selector, + "cancelListing(uint256)" + ); + extension_directListings.functions[7] = ExtensionFunction( + DirectListingsLogic.approveBuyerForListing.selector, + "approveBuyerForListing(uint256,address,bool)" + ); + extension_directListings.functions[8] = ExtensionFunction( + DirectListingsLogic.approveCurrencyForListing.selector, + "approveCurrencyForListing(uint256,address,uint256)" + ); + extension_directListings.functions[9] = ExtensionFunction( + DirectListingsLogic.buyFromListing.selector, + "buyFromListing(uint256,address,uint256,address,uint256)" + ); + extension_directListings.functions[10] = ExtensionFunction( + DirectListingsLogic.getAllListings.selector, + "getAllListings(uint256,uint256)" + ); + extension_directListings.functions[11] = ExtensionFunction( + DirectListingsLogic.getAllValidListings.selector, + "getAllValidListings(uint256,uint256)" + ); + extension_directListings.functions[12] = ExtensionFunction( + DirectListingsLogic.getListing.selector, + "getListing(uint256)" + ); + + extensions[0] = extension_directListings; + } + + function _setupERC721BalanceForSeller(address _seller, uint256 _numOfTokens) private { + erc721.mint(_seller, _numOfTokens); + } + + function test_state_initial() public { + uint256 totalListings = DirectListingsLogic(marketplace).totalListings(); + assertEq(totalListings, 0); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function test_getValidListings_burnListedTokens() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + DirectListingsLogic(marketplace).createListing(listingParams); + + // Total listings incremented + assertEq(DirectListingsLogic(marketplace).totalListings(), 1); + + // burn listed token + vm.prank(seller); + erc721.burn(0); + + vm.warp(150); + // Fetch listing and verify state. + uint256 totalListings = DirectListingsLogic(marketplace).totalListings(); + assertEq(DirectListingsLogic(marketplace).getAllValidListings(0, totalListings - 1).length, 0); + } + + /** + * @dev Tests contract state for Lister role. + */ + function test_state_getRoleMember_listerRole() public { + bytes32 role = keccak256("LISTER_ROLE"); + + uint256 roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); + assertEq(roleMemberCount, 1); + + address roleMember = PermissionsEnumerable(marketplace).getRoleMember(role, 1); + assertEq(roleMember, address(0)); + + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).grantRole(role, address(2)); + Permissions(marketplace).grantRole(role, address(3)); + Permissions(marketplace).grantRole(role, address(4)); + + roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); + assertEq(roleMemberCount, 4); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(marketplace).getRoleMember(role, i)); + } + console.log(""); + + Permissions(marketplace).revokeRole(role, address(2)); + roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); + assertEq(roleMemberCount, 3); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(marketplace).getRoleMember(role, i)); + } + console.log(""); + + Permissions(marketplace).grantRole(role, address(5)); + roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); + assertEq(roleMemberCount, 4); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(marketplace).getRoleMember(role, i)); + } + console.log(""); + + Permissions(marketplace).grantRole(role, address(0)); + roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); + assertEq(roleMemberCount, 5); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(marketplace).getRoleMember(role, i)); + } + console.log(""); + + Permissions(marketplace).grantRole(role, address(6)); + roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); + assertEq(roleMemberCount, 6); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(marketplace).getRoleMember(role, i)); + } + console.log(""); + + Permissions(marketplace).revokeRole(role, address(3)); + roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); + assertEq(roleMemberCount, 5); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(marketplace).getRoleMember(role, i)); + } + console.log(""); + + Permissions(marketplace).revokeRole(role, address(4)); + roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); + assertEq(roleMemberCount, 4); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(marketplace).getRoleMember(role, i)); + } + console.log(""); + + Permissions(marketplace).revokeRole(role, address(0)); + roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); + assertEq(roleMemberCount, 3); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(marketplace).getRoleMember(role, i)); + } + console.log(""); + + vm.stopPrank(); + } + + function test_state_approvedCurrencies() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParams) = _setup_updateListing(); + address currencyToApprove = address(erc20); // same currency as main listing + uint256 pricePerTokenForCurrency = 2 ether; + + // Seller approves currency for listing. + vm.prank(seller); + vm.expectRevert("Marketplace: approving listing currency with different price."); + DirectListingsLogic(marketplace).approveCurrencyForListing( + listingId, + currencyToApprove, + pricePerTokenForCurrency + ); + + // change currency + currencyToApprove = NATIVE_TOKEN; + + vm.prank(seller); + DirectListingsLogic(marketplace).approveCurrencyForListing( + listingId, + currencyToApprove, + pricePerTokenForCurrency + ); + + assertEq(DirectListingsLogic(marketplace).isCurrencyApprovedForListing(listingId, NATIVE_TOKEN), true); + assertEq( + DirectListingsLogic(marketplace).currencyPriceForListing(listingId, NATIVE_TOKEN), + pricePerTokenForCurrency + ); + + // should revert when updating listing with an approved currency but different price + listingParams.currency = NATIVE_TOKEN; + vm.prank(seller); + vm.expectRevert("Marketplace: price different from approved price"); + DirectListingsLogic(marketplace).updateListing(listingId, listingParams); + + // change listingParams.pricePerToken to approved price + listingParams.pricePerToken = pricePerTokenForCurrency; + vm.prank(seller); + DirectListingsLogic(marketplace).updateListing(listingId, listingParams); + } + + /*/////////////////////////////////////////////////////////////// + Royalty Tests (incl Royalty Engine / Registry) + //////////////////////////////////////////////////////////////*/ + + function _setupRoyaltyEngine() + private + returns ( + MockRoyaltyEngineV1 royaltyEngine, + address payable[] memory mockRecipients, + uint256[] memory mockAmounts + ) + { + mockRecipients = new address payable[](2); + mockAmounts = new uint256[](2); + + mockRecipients[0] = payable(address(0x12345)); + mockRecipients[1] = payable(address(0x56789)); + + mockAmounts[0] = 10; + mockAmounts[1] = 15; + + royaltyEngine = new MockRoyaltyEngineV1(mockRecipients, mockAmounts); + } + + function _setupListingForRoyaltyTests(address erc721TokenAddress) private returns (uint256 listingId) { + // Sample listing parameters. + address assetContract = erc721TokenAddress; + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 100 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = false; + + // Approve Marketplace to transfer token. + vm.prank(seller); + IERC721(erc721TokenAddress).setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + listingId = DirectListingsLogic(marketplace).createListing(listingParams); + } + + function _buyFromListingForRoyaltyTests(uint256 listingId) private returns (uint256 totalPrice) { + IDirectListings.Listing memory listing = DirectListingsLogic(marketplace).getListing(listingId); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + totalPrice = pricePerToken * quantityToBuy; + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + DirectListingsLogic(marketplace).buyFromListing(listingId, buyFor, quantityToBuy, currency, totalPrice); + } + + function test_royaltyEngine_tokenWithCustomRoyalties() public { + ( + MockRoyaltyEngineV1 royaltyEngine, + address payable[] memory customRoyaltyRecipients, + uint256[] memory customRoyaltyAmounts + ) = _setupRoyaltyEngine(); + + // Add RoyaltyEngine to marketplace + vm.prank(marketplaceDeployer); + RoyaltyPaymentsLogic(marketplace).setRoyaltyEngine(address(royaltyEngine)); + + assertEq(RoyaltyPaymentsLogic(marketplace).getRoyaltyEngineAddress(), address(royaltyEngine)); + + // 1. ========= Create listing ========= + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + uint256 listingId = _setupListingForRoyaltyTests(address(erc721)); + + // 2. ========= Buy from listing ========= + + uint256 totalPrice = _buyFromListingForRoyaltyTests(listingId); + + // 3. ======== Check balances after royalty payments ======== + + { + uint256 defaultFee = (totalPrice * 100) / 10_000; + + // Royalty recipients receive correct amounts + assertBalERC20Eq(address(erc20), customRoyaltyRecipients[0], customRoyaltyAmounts[0]); + assertBalERC20Eq(address(erc20), customRoyaltyRecipients[1], customRoyaltyAmounts[1]); + + // Seller gets total price minus royalty amounts + assertBalERC20Eq( + address(erc20), + seller, + totalPrice - customRoyaltyAmounts[0] - customRoyaltyAmounts[1] - defaultFee + ); + } + } + + function test_royaltyEngine_tokenWithERC2981() public { + (MockRoyaltyEngineV1 royaltyEngine, , ) = _setupRoyaltyEngine(); + + // Add RoyaltyEngine to marketplace + vm.prank(marketplaceDeployer); + RoyaltyPaymentsLogic(marketplace).setRoyaltyEngine(address(royaltyEngine)); + + assertEq(RoyaltyPaymentsLogic(marketplace).getRoyaltyEngineAddress(), address(royaltyEngine)); + + // create token with ERC2981 + address royaltyRecipient = address(0x12345); + uint128 royaltyBps = 10; + ERC721Base nft2981 = new ERC721Base(address(0x12345), "NFT 2981", "NFT2981", royaltyRecipient, royaltyBps); + // Mint the ERC721 tokens to seller. These tokens will be listed. + vm.prank(address(0x12345)); + nft2981.mintTo(seller, ""); + + vm.prank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(nft2981)); + + // 1. ========= Create listing ========= + + uint256 listingId = _setupListingForRoyaltyTests(address(nft2981)); + + // 2. ========= Buy from listing ========= + + uint256 totalPrice = _buyFromListingForRoyaltyTests(listingId); + + // 3. ======== Check balances after royalty payments ======== + + { + uint256 defaultFee = (totalPrice * 100) / 10_000; + + uint256 royaltyAmount = (royaltyBps * totalPrice) / 10_000; + // Royalty recipient receives correct amounts + assertBalERC20Eq(address(erc20), royaltyRecipient, royaltyAmount); + + // Seller gets total price minus royalty amount + assertBalERC20Eq(address(erc20), seller, totalPrice - royaltyAmount - defaultFee); + } + } + + function test_noRoyaltyEngine_defaultERC2981Token() public { + // create token with ERC2981 + address royaltyRecipient = address(0x12345); + uint128 royaltyBps = 10; + ERC721Base nft2981 = new ERC721Base(address(0x12345), "NFT 2981", "NFT2981", royaltyRecipient, royaltyBps); + vm.prank(address(0x12345)); + nft2981.mintTo(seller, ""); + + vm.prank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(nft2981)); + + // 1. ========= Create listing ========= + + uint256 listingId = _setupListingForRoyaltyTests(address(nft2981)); + + // 2. ========= Buy from listing ========= + + uint256 totalPrice = _buyFromListingForRoyaltyTests(listingId); + uint256 defaultFee = (totalPrice * 100) / 10_000; + + // 3. ======== Check balances after royalty payments ======== + + { + uint256 royaltyAmount = (royaltyBps * totalPrice) / 10_000; + // Royalty recipient receives correct amounts + assertBalERC20Eq(address(erc20), royaltyRecipient, royaltyAmount); + + // Seller gets total price minus royalty amount + assertBalERC20Eq(address(erc20), seller, totalPrice - royaltyAmount - defaultFee); + } + } + + function test_royaltyEngine_correctlyDistributeAllFees() public { + ( + MockRoyaltyEngineV1 royaltyEngine, + address payable[] memory customRoyaltyRecipients, + uint256[] memory customRoyaltyAmounts + ) = _setupRoyaltyEngine(); + + // Add RoyaltyEngine to marketplace + vm.prank(marketplaceDeployer); + RoyaltyPaymentsLogic(marketplace).setRoyaltyEngine(address(royaltyEngine)); + + assertEq(RoyaltyPaymentsLogic(marketplace).getRoyaltyEngineAddress(), address(royaltyEngine)); + + // Set platform fee on marketplace + address platformFeeRecipient = marketplaceDeployer; + uint128 platformFeeBps = 5; + vm.prank(marketplaceDeployer); + IPlatformFee(marketplace).setPlatformFeeInfo(platformFeeRecipient, platformFeeBps); + + // 1. ========= Create listing ========= + + _setupERC721BalanceForSeller(seller, 1); + uint256 listingId = _setupListingForRoyaltyTests(address(erc721)); + + // 2. ========= Buy from listing ========= + + uint256 totalPrice = _buyFromListingForRoyaltyTests(listingId); + + // 3. ======== Check balances after fee payments (platform fee + royalty) ======== + + { + uint256 defaultFee = (totalPrice * 100) / 10_000; + + // Royalty recipients receive correct amounts + assertBalERC20Eq(address(erc20), customRoyaltyRecipients[0], customRoyaltyAmounts[0]); + assertBalERC20Eq(address(erc20), customRoyaltyRecipients[1], customRoyaltyAmounts[1]); + + // Platform fee recipient + uint256 platformFeeAmount = (platformFeeBps * totalPrice) / 10_000; + assertBalERC20Eq(address(erc20), platformFeeRecipient, platformFeeAmount); + + // Seller gets total price minus royalty amounts + assertBalERC20Eq( + address(erc20), + seller, + totalPrice - customRoyaltyAmounts[0] - customRoyaltyAmounts[1] - platformFeeAmount - defaultFee + ); + } + } + + function test_revert_feesExceedTotalPrice() public { + (MockRoyaltyEngineV1 royaltyEngine, , ) = _setupRoyaltyEngine(); + + // Add RoyaltyEngine to marketplace + vm.prank(marketplaceDeployer); + RoyaltyPaymentsLogic(marketplace).setRoyaltyEngine(address(royaltyEngine)); + + assertEq(RoyaltyPaymentsLogic(marketplace).getRoyaltyEngineAddress(), address(royaltyEngine)); + + // Set platform fee on marketplace + address platformFeeRecipient = marketplaceDeployer; + uint128 platformFeeBps = 9900; // along with default fee of 100 bps => equal to max bps 10_000 or 100% + vm.prank(marketplaceDeployer); + IPlatformFee(marketplace).setPlatformFeeInfo(platformFeeRecipient, platformFeeBps); + + // 1. ========= Create listing ========= + + _setupERC721BalanceForSeller(seller, 1); + uint256 listingId = _setupListingForRoyaltyTests(address(erc721)); + + // 2. ========= Buy from listing ========= + + IDirectListings.Listing memory listing = DirectListingsLogic(marketplace).getListing(listingId); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + + vm.expectRevert("fees exceed the price"); + vm.prank(buyer); + DirectListingsLogic(marketplace).buyFromListing(listingId, buyFor, quantityToBuy, currency, totalPrice); + } + + /*/////////////////////////////////////////////////////////////// + Create listing + //////////////////////////////////////////////////////////////*/ + + function test_state_createListing() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + uint256 listingId = DirectListingsLogic(marketplace).createListing(listingParams); + + // Test consequent state of the contract. + + // Seller is still owner of token. + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Total listings incremented + assertEq(DirectListingsLogic(marketplace).totalListings(), 1); + + // Fetch listing and verify state. + IDirectListings.Listing memory listing = DirectListingsLogic(marketplace).getListing(listingId); + + assertEq(listing.listingId, listingId); + assertEq(listing.listingCreator, seller); + assertEq(listing.assetContract, assetContract); + assertEq(listing.tokenId, tokenId); + assertEq(listing.quantity, quantity); + assertEq(listing.currency, currency); + assertEq(listing.pricePerToken, pricePerToken); + assertEq(listing.startTimestamp, startTimestamp); + assertEq(listing.endTimestamp, endTimestamp); + assertEq(listing.reserved, reserved); + assertEq(uint256(listing.tokenType), uint256(IDirectListings.TokenType.ERC721)); + } + + function test_revert_createListing_notOwnerOfListedToken() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Don't mint to 'token to be listed' to the seller. + address someWallet = getActor(1000); + _setupERC721BalanceForSeller(someWallet, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), someWallet, tokenIds); + assertIsNotOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(someWallet); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + vm.expectRevert("Marketplace: not owner or approved tokens."); + DirectListingsLogic(marketplace).createListing(listingParams); + } + + function test_revert_createListing_notApprovedMarketplaceToTransferToken() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Don't approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, false); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + vm.expectRevert("Marketplace: not owner or approved tokens."); + DirectListingsLogic(marketplace).createListing(listingParams); + } + + function test_revert_createListing_listingZeroQuantity() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 0; // Listing ZERO quantity + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + vm.expectRevert("Marketplace: listing zero quantity."); + DirectListingsLogic(marketplace).createListing(listingParams); + } + + function test_revert_createListing_listingInvalidQuantity() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 2; // Listing more than `1` quantity + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + vm.expectRevert("Marketplace: listing invalid quantity."); + DirectListingsLogic(marketplace).createListing(listingParams); + } + + function test_revert_createListing_invalidStartTimestamp() public { + uint256 blockTimestamp = 100 minutes; + // Set block.timestamp + vm.warp(blockTimestamp); + + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = uint128(blockTimestamp - 61 minutes); // start time is less than block timestamp. + uint128 endTimestamp = uint128(startTimestamp + 1); + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + vm.expectRevert("Marketplace: invalid startTimestamp."); + DirectListingsLogic(marketplace).createListing(listingParams); + } + + function test_revert_createListing_invalidEndTimestamp() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = uint128(startTimestamp - 1); // End timestamp is less than start timestamp. + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + vm.expectRevert("Marketplace: endTimestamp not greater than startTimestamp."); + DirectListingsLogic(marketplace).createListing(listingParams); + } + + function test_revert_createListing_listingNonERC721OrERC1155Token() public { + // Sample listing parameters. + address assetContract = address(erc20); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + // Grant ERC20 token asset role. + vm.prank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc20)); + + vm.prank(seller); + vm.expectRevert("Marketplace: listed token must be ERC1155 or ERC721."); + DirectListingsLogic(marketplace).createListing(listingParams); + } + + function test_revert_createListing_noListerRoleWhenRestrictionsActive() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + // Revoke LISTER_ROLE from seller. + vm.startPrank(marketplaceDeployer); + assertEq(Permissions(marketplace).hasRole(keccak256("LISTER_ROLE"), address(0)), false); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), seller); + assertEq(Permissions(marketplace).hasRole(keccak256("LISTER_ROLE"), seller), false); + + vm.stopPrank(); + + vm.prank(seller); + vm.expectRevert("!LISTER_ROLE"); + DirectListingsLogic(marketplace).createListing(listingParams); + } + + function test_revert_createListing_noAssetRoleWhenRestrictionsActive() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + // Revoke ASSET_ROLE from token to list. + vm.startPrank(marketplaceDeployer); + assertEq(Permissions(marketplace).hasRole(keccak256("ASSET_ROLE"), address(0)), false); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(erc721)); + assertEq(Permissions(marketplace).hasRole(keccak256("ASSET_ROLE"), address(erc721)), false); + + vm.stopPrank(); + + vm.prank(seller); + vm.expectRevert("!ASSET_ROLE"); + DirectListingsLogic(marketplace).createListing(listingParams); + } + + /*/////////////////////////////////////////////////////////////// + Update listing + //////////////////////////////////////////////////////////////*/ + + function _setup_updateListing() + private + returns (uint256 listingId, IDirectListings.ListingParameters memory listingParams) + { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + listingId = DirectListingsLogic(marketplace).createListing(listingParams); + } + + function test_state_updateListing() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + listingParamsToUpdate.pricePerToken = 2 ether; + + vm.prank(seller); + DirectListingsLogic(marketplace).updateListing(listingId, listingParamsToUpdate); + + // Test consequent state of the contract. + + // Seller is still owner of token. + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Total listings not incremented on update. + assertEq(DirectListingsLogic(marketplace).totalListings(), 1); + + // Fetch listing and verify state. + IDirectListings.Listing memory listing = DirectListingsLogic(marketplace).getListing(listingId); + + assertEq(listing.listingId, listingId); + assertEq(listing.listingCreator, seller); + assertEq(listing.assetContract, listingParamsToUpdate.assetContract); + assertEq(listing.tokenId, 0); + assertEq(listing.quantity, listingParamsToUpdate.quantity); + assertEq(listing.currency, listingParamsToUpdate.currency); + assertEq(listing.pricePerToken, listingParamsToUpdate.pricePerToken); + assertEq(listing.startTimestamp, listingParamsToUpdate.startTimestamp); + assertEq(listing.endTimestamp, listingParamsToUpdate.endTimestamp); + assertEq(listing.reserved, listingParamsToUpdate.reserved); + assertEq(uint256(listing.tokenType), uint256(IDirectListings.TokenType.ERC721)); + } + + function test_revert_updateListing_notListingCreator() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + address notSeller = getActor(1000); // Someone other than the seller calls update. + vm.prank(notSeller); + vm.expectRevert("Marketplace: not listing creator."); + DirectListingsLogic(marketplace).updateListing(listingId, listingParamsToUpdate); + } + + function test_revert_updateListing_notOwnerOfListedToken() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + + // Mint MORE ERC721 tokens but NOT to seller. A new tokenId will be listed. + address notSeller = getActor(1000); + _setupERC721BalanceForSeller(notSeller, 1); + + // Approve Marketplace to transfer token. + vm.prank(notSeller); + erc721.setApprovalForAll(marketplace, true); + + // Transfer away owned token. + vm.prank(seller); + erc721.transferFrom(seller, address(0x1234), 0); + + vm.prank(seller); + vm.expectRevert("Marketplace: not owner or approved tokens."); + DirectListingsLogic(marketplace).updateListing(listingId, listingParamsToUpdate); + } + + function test_revert_updateListing_notApprovedMarketplaceToTransferToken() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Don't approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, false); + + vm.prank(seller); + vm.expectRevert("Marketplace: not owner or approved tokens."); + DirectListingsLogic(marketplace).updateListing(listingId, listingParamsToUpdate); + } + + function test_revert_updateListing_listingZeroQuantity() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + listingParamsToUpdate.quantity = 0; // Listing zero quantity + + vm.prank(seller); + vm.expectRevert("Marketplace: listing zero quantity."); + DirectListingsLogic(marketplace).updateListing(listingId, listingParamsToUpdate); + } + + function test_revert_updateListing_listingInvalidQuantity() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + listingParamsToUpdate.quantity = 2; // Listing more than `1` of the ERC721 token + + vm.prank(seller); + vm.expectRevert("Marketplace: listing invalid quantity."); + DirectListingsLogic(marketplace).updateListing(listingId, listingParamsToUpdate); + } + + function test_revert_updateListing_listingNonERC721OrERC1155Token() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + listingParamsToUpdate.assetContract = address(erc20); // Listing non ERC721 / ERC1155 token. + + // Grant ERC20 token asset role. + vm.prank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc20)); + + vm.prank(seller); + vm.expectRevert("Marketplace: listed token must be ERC1155 or ERC721."); + DirectListingsLogic(marketplace).updateListing(listingId, listingParamsToUpdate); + } + + function test_revert_updateListing_invalidStartTimestamp() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + uint128 currentStartTimestamp = listingParamsToUpdate.startTimestamp; + listingParamsToUpdate.startTimestamp = currentStartTimestamp - 1; // Retroactively decreasing startTimestamp. + + vm.warp(currentStartTimestamp + 50); + vm.prank(seller); + vm.expectRevert("Marketplace: listing already active."); + DirectListingsLogic(marketplace).updateListing(listingId, listingParamsToUpdate); + } + + function test_revert_updateListing_invalidEndTimestamp() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + uint128 currentStartTimestamp = listingParamsToUpdate.startTimestamp; + listingParamsToUpdate.endTimestamp = currentStartTimestamp - 1; // End timestamp less than startTimestamp + + vm.prank(seller); + vm.expectRevert("Marketplace: endTimestamp not greater than startTimestamp."); + DirectListingsLogic(marketplace).updateListing(listingId, listingParamsToUpdate); + } + + function test_revert_updateListing_noAssetRoleWhenRestrictionsActive() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Revoke ASSET_ROLE from token to list. + vm.startPrank(marketplaceDeployer); + assertEq(Permissions(marketplace).hasRole(keccak256("ASSET_ROLE"), address(0)), false); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(erc721)); + assertEq(Permissions(marketplace).hasRole(keccak256("ASSET_ROLE"), address(erc721)), false); + + vm.stopPrank(); + + vm.prank(seller); + vm.expectRevert("!ASSET_ROLE"); + DirectListingsLogic(marketplace).updateListing(listingId, listingParamsToUpdate); + } + + /*/////////////////////////////////////////////////////////////// + Cancel listing + //////////////////////////////////////////////////////////////*/ + + function _setup_cancelListing() private returns (uint256 listingId, IDirectListings.Listing memory listing) { + (listingId, ) = _setup_updateListing(); + listing = DirectListingsLogic(marketplace).getListing(listingId); + } + + function test_state_cancelListing() public { + (uint256 listingId, IDirectListings.Listing memory existingListingAtId) = _setup_cancelListing(); + + // Verify existing listing at `listingId` + assertEq(existingListingAtId.assetContract, address(erc721)); + + vm.prank(seller); + DirectListingsLogic(marketplace).cancelListing(listingId); + + // status should be `CANCELLED` + IDirectListings.Listing memory cancelledListing = DirectListingsLogic(marketplace).getListing(listingId); + assertTrue(cancelledListing.status == IDirectListings.Status.CANCELLED); + } + + function test_revert_cancelListing_notListingCreator() public { + (uint256 listingId, IDirectListings.Listing memory existingListingAtId) = _setup_cancelListing(); + + // Verify existing listing at `listingId` + assertEq(existingListingAtId.assetContract, address(erc721)); + + address notSeller = getActor(1000); + vm.prank(notSeller); + vm.expectRevert("Marketplace: not listing creator."); + DirectListingsLogic(marketplace).cancelListing(listingId); + } + + function test_revert_cancelListing_nonExistentListing() public { + _setup_cancelListing(); + + // Verify no listing exists at `nexListingId` + uint256 nextListingId = DirectListingsLogic(marketplace).totalListings(); + + vm.prank(seller); + vm.expectRevert("Marketplace: invalid listing."); + DirectListingsLogic(marketplace).cancelListing(nextListingId); + } + + /*/////////////////////////////////////////////////////////////// + Approve buyer for listing + //////////////////////////////////////////////////////////////*/ + + function _setup_approveBuyerForListing() private returns (uint256 listingId) { + (listingId, ) = _setup_updateListing(); + } + + function test_state_approveBuyerForListing() public { + uint256 listingId = _setup_approveBuyerForListing(); + bool toApprove = true; + + assertEq(DirectListingsLogic(marketplace).getListing(listingId).reserved, true); + + // Seller approves buyer for reserved listing. + vm.prank(seller); + DirectListingsLogic(marketplace).approveBuyerForListing(listingId, buyer, toApprove); + + assertEq(DirectListingsLogic(marketplace).isBuyerApprovedForListing(listingId, buyer), true); + } + + function test_revert_approveBuyerForListing_notListingCreator() public { + uint256 listingId = _setup_approveBuyerForListing(); + bool toApprove = true; + + assertEq(DirectListingsLogic(marketplace).getListing(listingId).reserved, true); + + // Someone other than the seller approves buyer for reserved listing. + address notSeller = getActor(1000); + vm.prank(notSeller); + vm.expectRevert("Marketplace: not listing creator."); + DirectListingsLogic(marketplace).approveBuyerForListing(listingId, buyer, toApprove); + } + + function test_revert_approveBuyerForListing_listingNotReserved() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + bool toApprove = true; + + assertEq(DirectListingsLogic(marketplace).getListing(listingId).reserved, true); + + listingParamsToUpdate.reserved = false; + + vm.prank(seller); + DirectListingsLogic(marketplace).updateListing(listingId, listingParamsToUpdate); + + assertEq(DirectListingsLogic(marketplace).getListing(listingId).reserved, false); + + // Seller approves buyer for reserved listing. + vm.prank(seller); + vm.expectRevert("Marketplace: listing not reserved."); + DirectListingsLogic(marketplace).approveBuyerForListing(listingId, buyer, toApprove); + } + + /*/////////////////////////////////////////////////////////////// + Approve currency for listing + //////////////////////////////////////////////////////////////*/ + + function _setup_approveCurrencyForListing() private returns (uint256 listingId) { + (listingId, ) = _setup_updateListing(); + } + + function test_state_approveCurrencyForListing() public { + uint256 listingId = _setup_approveCurrencyForListing(); + address currencyToApprove = NATIVE_TOKEN; + uint256 pricePerTokenForCurrency = 2 ether; + + // Seller approves buyer for reserved listing. + vm.prank(seller); + DirectListingsLogic(marketplace).approveCurrencyForListing( + listingId, + currencyToApprove, + pricePerTokenForCurrency + ); + + assertEq(DirectListingsLogic(marketplace).isCurrencyApprovedForListing(listingId, NATIVE_TOKEN), true); + assertEq( + DirectListingsLogic(marketplace).currencyPriceForListing(listingId, NATIVE_TOKEN), + pricePerTokenForCurrency + ); + } + + function test_revert_approveCurrencyForListing_notListingCreator() public { + uint256 listingId = _setup_approveCurrencyForListing(); + address currencyToApprove = NATIVE_TOKEN; + uint256 pricePerTokenForCurrency = 2 ether; + + // Someone other than seller approves buyer for reserved listing. + address notSeller = getActor(1000); + vm.prank(notSeller); + vm.expectRevert("Marketplace: not listing creator."); + DirectListingsLogic(marketplace).approveCurrencyForListing( + listingId, + currencyToApprove, + pricePerTokenForCurrency + ); + } + + function test_revert_approveCurrencyForListing_reApprovingMainCurrency() public { + uint256 listingId = _setup_approveCurrencyForListing(); + address currencyToApprove = DirectListingsLogic(marketplace).getListing(listingId).currency; + uint256 pricePerTokenForCurrency = 2 ether; + + // Seller approves buyer for reserved listing. + vm.prank(seller); + vm.expectRevert("Marketplace: approving listing currency with different price."); + DirectListingsLogic(marketplace).approveCurrencyForListing( + listingId, + currencyToApprove, + pricePerTokenForCurrency + ); + } + + /*/////////////////////////////////////////////////////////////// + Buy from listing + //////////////////////////////////////////////////////////////*/ + + function _setup_buyFromListing() private returns (uint256 listingId, IDirectListings.Listing memory listing) { + (listingId, ) = _setup_updateListing(); + listing = DirectListingsLogic(marketplace).getListing(listingId); + } + + function test_state_buyFromListing() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Seller approves buyer for listing + vm.prank(seller); + DirectListingsLogic(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + DirectListingsLogic(marketplace).buyFromListing(listingId, buyFor, quantityToBuy, currency, totalPrice); + + // Verify that buyer is owner of listed tokens, post-sale. + assertIsOwnerERC721(address(erc721), buyer, tokenIds); + assertIsNotOwnerERC721(address(erc721), seller, tokenIds); + + uint256 defaultFee = (totalPrice * 100) / 10_000; + + // Verify seller is paid total price. + assertBalERC20Eq(address(erc20), buyer, 0); + assertBalERC20Eq(address(erc20), seller, totalPrice - defaultFee); + + if (quantityToBuy == listing.quantity) { + // Verify listing status is `COMPLETED` if listing tokens are all bought. + IDirectListings.Listing memory completedListing = DirectListingsLogic(marketplace).getListing(listingId); + assertTrue(completedListing.status == IDirectListings.Status.COMPLETED); + } + } + + function test_state_buyFromListing_nativeToken() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = NATIVE_TOKEN; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Approve NATIVE_TOKEN for listing + vm.prank(seller); + DirectListingsLogic(marketplace).approveCurrencyForListing(listingId, currency, pricePerToken); + + // Seller approves buyer for listing + vm.prank(seller); + DirectListingsLogic(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Deal requisite total price to buyer. + vm.deal(buyer, totalPrice); + uint256 buyerBalBefore = buyer.balance; + uint256 sellerBalBefore = seller.balance; + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + DirectListingsLogic(marketplace).buyFromListing{ value: totalPrice }( + listingId, + buyFor, + quantityToBuy, + currency, + totalPrice + ); + + // Verify that buyer is owner of listed tokens, post-sale. + assertIsOwnerERC721(address(erc721), buyer, tokenIds); + assertIsNotOwnerERC721(address(erc721), seller, tokenIds); + + uint256 defaultFee = (totalPrice * 100) / 10_000; + + // Verify seller is paid total price. + assertEq(buyer.balance, buyerBalBefore - totalPrice); + assertEq(seller.balance, sellerBalBefore + totalPrice - defaultFee); + + if (quantityToBuy == listing.quantity) { + // Verify listing status is `COMPLETED` if listing tokens are all bought. + IDirectListings.Listing memory completedListing = DirectListingsLogic(marketplace).getListing(listingId); + assertTrue(completedListing.status == IDirectListings.Status.COMPLETED); + } + } + + function test_revert_buyFromListing_nativeToken_incorrectValueSent() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = NATIVE_TOKEN; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Approve NATIVE_TOKEN for listing + vm.prank(seller); + DirectListingsLogic(marketplace).approveCurrencyForListing(listingId, currency, pricePerToken); + + // Seller approves buyer for listing + vm.prank(seller); + DirectListingsLogic(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Deal requisite total price to buyer. + vm.deal(buyer, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("Marketplace: msg.value must exactly be the total price."); + DirectListingsLogic(marketplace).buyFromListing{ value: totalPrice - 1 }( // sending insufficient value + listingId, + buyFor, + quantityToBuy, + currency, + totalPrice + ); + } + + function test_revert_buyFromListing_unexpectedTotalPrice() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = NATIVE_TOKEN; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Approve NATIVE_TOKEN for listing + vm.prank(seller); + DirectListingsLogic(marketplace).approveCurrencyForListing(listingId, currency, pricePerToken); + + // Seller approves buyer for listing + vm.prank(seller); + DirectListingsLogic(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Deal requisite total price to buyer. + vm.deal(buyer, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("Unexpected total price"); + DirectListingsLogic(marketplace).buyFromListing{ value: totalPrice }( + listingId, + buyFor, + quantityToBuy, + currency, + totalPrice + 1 // Pass unexpected total price + ); + } + + function test_revert_buyFromListing_invalidCurrency() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Seller approves buyer for listing + vm.prank(seller); + DirectListingsLogic(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + + assertEq(listing.currency, address(erc20)); + assertEq(DirectListingsLogic(marketplace).isCurrencyApprovedForListing(listingId, NATIVE_TOKEN), false); + + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("Paying in invalid currency."); + DirectListingsLogic(marketplace).buyFromListing(listingId, buyFor, quantityToBuy, NATIVE_TOKEN, totalPrice); + } + + function test_revert_buyFromListing_buyerBalanceLessThanPrice() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Seller approves buyer for listing + vm.prank(seller); + DirectListingsLogic(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice - 1); // Buyer balance less than total price + assertBalERC20Eq(address(erc20), buyer, totalPrice - 1); + assertBalERC20Eq(address(erc20), seller, 0); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("!BAL20"); + DirectListingsLogic(marketplace).buyFromListing(listingId, buyFor, quantityToBuy, currency, totalPrice); + } + + function test_revert_buyFromListing_notApprovedMarketplaceToTransferPrice() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Seller approves buyer for listing + vm.prank(seller); + DirectListingsLogic(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Don't approve marketplace to transfer currency + vm.prank(buyer); + erc20.approve(marketplace, 0); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("!BAL20"); + DirectListingsLogic(marketplace).buyFromListing(listingId, buyFor, quantityToBuy, currency, totalPrice); + } + + function test_revert_buyFromListing_buyingZeroQuantity() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + address buyFor = buyer; + uint256 quantityToBuy = 0; // Buying zero quantity + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Seller approves buyer for listing + vm.prank(seller); + DirectListingsLogic(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Don't approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("Buying invalid quantity"); + DirectListingsLogic(marketplace).buyFromListing(listingId, buyFor, quantityToBuy, currency, totalPrice); + } + + function test_revert_buyFromListing_buyingMoreQuantityThanListed() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity + 1; // Buying more than listed. + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Seller approves buyer for listing + vm.prank(seller); + DirectListingsLogic(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Don't approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("Buying invalid quantity"); + DirectListingsLogic(marketplace).buyFromListing(listingId, buyFor, quantityToBuy, currency, totalPrice); + } + + /*/////////////////////////////////////////////////////////////// + View functions + //////////////////////////////////////////////////////////////*/ + + function _createListing(address _seller) private returns (uint256 listingId) { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(_seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), _seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(_seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(_seller); + listingId = DirectListingsLogic(marketplace).createListing(listingParams); + } + + function test_audit_native_tokens_locked() public { + (uint256 listingId, IDirectListings.Listing memory existingListing) = _setup_buyFromListing(); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingListing.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingListing.assetContract, address(erc721)); + + vm.warp(existingListing.startTimestamp); + + // No ether is locked in contract + assertEq(marketplace.balance, 0); + + // buy from listing + erc20.mint(buyer, 10 ether); + vm.deal(buyer, 1 ether); + + vm.prank(seller); + DirectListingsLogic(marketplace).approveBuyerForListing(listingId, buyer, true); + + vm.startPrank(buyer); + erc20.approve(marketplace, 10 ether); + + vm.expectRevert("Marketplace: invalid native tokens sent."); + DirectListingsLogic(marketplace).buyFromListing{ value: 1 ether }(listingId, buyer, 1, address(erc20), 1 ether); + vm.stopPrank(); + + // 1 ether is temporary locked in contract + assertEq(marketplace.balance, 0 ether); + } +} + +contract IssueC2_MarketplaceDirectListingsTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles for seller and assets + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc721)); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc1155)); + + vm.stopPrank(); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(buyer, "Buyer"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `DirectListings` + address directListings = address(new DirectListingsLogic(address(weth))); + vm.label(directListings, "DirectListings_Extension"); + + // Extension: DirectListingsLogic + Extension memory extension_directListings; + extension_directListings.metadata = ExtensionMetadata({ + name: "DirectListingsLogic", + metadataURI: "ipfs://DirectListings", + implementation: directListings + }); + + extension_directListings.functions = new ExtensionFunction[](13); + extension_directListings.functions[0] = ExtensionFunction( + DirectListingsLogic.totalListings.selector, + "totalListings()" + ); + extension_directListings.functions[1] = ExtensionFunction( + DirectListingsLogic.isBuyerApprovedForListing.selector, + "isBuyerApprovedForListing(uint256,address)" + ); + extension_directListings.functions[2] = ExtensionFunction( + DirectListingsLogic.isCurrencyApprovedForListing.selector, + "isCurrencyApprovedForListing(uint256,address)" + ); + extension_directListings.functions[3] = ExtensionFunction( + DirectListingsLogic.currencyPriceForListing.selector, + "currencyPriceForListing(uint256,address)" + ); + extension_directListings.functions[4] = ExtensionFunction( + DirectListingsLogic.createListing.selector, + "createListing((address,uint256,uint256,address,uint256,uint128,uint128,bool))" + ); + extension_directListings.functions[5] = ExtensionFunction( + DirectListingsLogic.updateListing.selector, + "updateListing(uint256,(address,uint256,uint256,address,uint256,uint128,uint128,bool))" + ); + extension_directListings.functions[6] = ExtensionFunction( + DirectListingsLogic.cancelListing.selector, + "cancelListing(uint256)" + ); + extension_directListings.functions[7] = ExtensionFunction( + DirectListingsLogic.approveBuyerForListing.selector, + "approveBuyerForListing(uint256,address,bool)" + ); + extension_directListings.functions[8] = ExtensionFunction( + DirectListingsLogic.approveCurrencyForListing.selector, + "approveCurrencyForListing(uint256,address,uint256)" + ); + extension_directListings.functions[9] = ExtensionFunction( + DirectListingsLogic.buyFromListing.selector, + "buyFromListing(uint256,address,uint256,address,uint256)" + ); + extension_directListings.functions[10] = ExtensionFunction( + DirectListingsLogic.getAllListings.selector, + "getAllListings(uint256,uint256)" + ); + extension_directListings.functions[11] = ExtensionFunction( + DirectListingsLogic.getAllValidListings.selector, + "getAllValidListings(uint256,uint256)" + ); + extension_directListings.functions[12] = ExtensionFunction( + DirectListingsLogic.getListing.selector, + "getListing(uint256)" + ); + + extensions[0] = extension_directListings; + } + + function _setupERC721BalanceForSeller(address _seller, uint256 _numOfTokens) private { + erc721.mint(_seller, _numOfTokens); + } + + function _setup_updateListing() + private + returns (uint256 listingId, IDirectListings.ListingParameters memory listingParams) + { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + listingId = DirectListingsLogic(marketplace).createListing(listingParams); + } + + function _setup_buyFromListing() private returns (uint256 listingId, IDirectListings.Listing memory listing) { + (listingId, ) = _setup_updateListing(); + listing = DirectListingsLogic(marketplace).getListing(listingId); + } + + function test_state_buyFromListing_after_update() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + uint256 quantityToBuy = listing.quantity; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Seller approves buyer for listing + vm.prank(seller); + DirectListingsLogic(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + // This token (Id = 0) was created in the above _setup_buyFromListing + uint256[] memory expectedTokenIds = new uint256[](1); + expectedTokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, expectedTokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, expectedTokenIds); + + // Mint a new token. This is token we will "swap out" via updateListing + // It should be tokenId of 1 + _setupERC721BalanceForSeller(seller, 1); + + // Verify that seller is owner of new token, pre-sale. + uint256[] memory swappedTokenIds = new uint256[](1); + swappedTokenIds[0] = 1; + assertIsOwnerERC721(address(erc721), seller, swappedTokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, swappedTokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // Create ListingParameters with new tokenId (1) and update + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + address(erc721), + 1, + 1, + address(erc20), + 1 ether, + 100, + 200, + true + ); + vm.prank(seller); + vm.expectRevert("Marketplace: cannot update what token is listed."); + DirectListingsLogic(marketplace).updateListing(listingId, listingParams); + + // Buy listing + // vm.warp(listing.startTimestamp); + // vm.prank(buyer); + // DirectListingsLogic(marketplace).buyFromListing(listingId, buyFor, quantityToBuy, currency, totalPrice); + + // // Buyer is owner of the swapped out token (tokenId = 1) and not the expected (tokenId = 0) + // assertIsOwnerERC721(address(erc721), buyer, swappedTokenIds); + // assertIsNotOwnerERC721(address(erc721), buyer, expectedTokenIds); + + // // Verify seller is paid total price. + // assertBalERC20Eq(address(erc20), buyer, 0); + // assertBalERC20Eq(address(erc20), seller, totalPrice); + } +} diff --git a/src/test/marketplace/EnglishAuctions.t.sol b/src/test/marketplace/EnglishAuctions.t.sol new file mode 100644 index 000000000..2fd139eea --- /dev/null +++ b/src/test/marketplace/EnglishAuctions.t.sol @@ -0,0 +1,2744 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test helper imports +import "../utils/BaseTest.sol"; + +// Test contracts and interfaces +import { PluginMap, IPluginMap } from "contracts/extension/plugin/PluginMap.sol"; +import { RoyaltyPaymentsLogic } from "contracts/extension/plugin/RoyaltyPayments.sol"; +import { MarketplaceV3, IPlatformFee } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { EnglishAuctionsLogic } from "contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsLogic.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { ERC721Base } from "contracts/base/ERC721Base.sol"; +import { MockRoyaltyEngineV1 } from "../mocks/MockRoyaltyEngineV1.sol"; + +import { IEnglishAuctions } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +contract MarketplaceEnglishAuctionsTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + address private defaultFeeRecipient; + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles for seller and assets + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc721)); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc1155)); + + vm.stopPrank(); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(buyer, "Buyer"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `EnglishAuctions` + address englishAuctions = address(new EnglishAuctionsLogic(address(weth))); + defaultFeeRecipient = 0x1Af20C6B23373350aD464700B5965CE4B0D2aD94; + vm.label(englishAuctions, "EnglishAuctions_Extension"); + + // Extension: EnglishAuctionsLogic + Extension memory extension_englishAuctions; + extension_englishAuctions.metadata = ExtensionMetadata({ + name: "EnglishAuctionsLogic", + metadataURI: "ipfs://EnglishAuctions", + implementation: englishAuctions + }); + + extension_englishAuctions.functions = new ExtensionFunction[](12); + extension_englishAuctions.functions[0] = ExtensionFunction( + EnglishAuctionsLogic.totalAuctions.selector, + "totalAuctions()" + ); + extension_englishAuctions.functions[1] = ExtensionFunction( + EnglishAuctionsLogic.createAuction.selector, + "createAuction((address,uint256,uint256,address,uint256,uint256,uint64,uint64,uint64,uint64))" + ); + extension_englishAuctions.functions[2] = ExtensionFunction( + EnglishAuctionsLogic.cancelAuction.selector, + "cancelAuction(uint256)" + ); + extension_englishAuctions.functions[3] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionPayout.selector, + "collectAuctionPayout(uint256)" + ); + extension_englishAuctions.functions[4] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionTokens.selector, + "collectAuctionTokens(uint256)" + ); + extension_englishAuctions.functions[5] = ExtensionFunction( + EnglishAuctionsLogic.bidInAuction.selector, + "bidInAuction(uint256,uint256)" + ); + extension_englishAuctions.functions[6] = ExtensionFunction( + EnglishAuctionsLogic.isNewWinningBid.selector, + "isNewWinningBid(uint256,uint256)" + ); + extension_englishAuctions.functions[7] = ExtensionFunction( + EnglishAuctionsLogic.getAuction.selector, + "getAuction(uint256)" + ); + extension_englishAuctions.functions[8] = ExtensionFunction( + EnglishAuctionsLogic.getAllAuctions.selector, + "getAllAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[9] = ExtensionFunction( + EnglishAuctionsLogic.getAllValidAuctions.selector, + "getAllValidAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[10] = ExtensionFunction( + EnglishAuctionsLogic.getWinningBid.selector, + "getWinningBid(uint256)" + ); + extension_englishAuctions.functions[11] = ExtensionFunction( + EnglishAuctionsLogic.isAuctionExpired.selector, + "isAuctionExpired(uint256)" + ); + + extensions[0] = extension_englishAuctions; + } + + function _setupERC721BalanceForSeller(address _seller, uint256 _numOfTokens) private { + erc721.mint(_seller, _numOfTokens); + } + + function test_state_initial() public { + uint256 totoalAuctions = EnglishAuctionsLogic(marketplace).totalAuctions(); + assertEq(totoalAuctions, 0); + } + + /*/////////////////////////////////////////////////////////////// + Royalty Tests (incl Royalty Engine / Registry) + //////////////////////////////////////////////////////////////*/ + + function _setupRoyaltyEngine() + private + returns ( + MockRoyaltyEngineV1 royaltyEngine, + address payable[] memory mockRecipients, + uint256[] memory mockAmounts + ) + { + mockRecipients = new address payable[](2); + mockAmounts = new uint256[](2); + + mockRecipients[0] = payable(address(0x12345)); + mockRecipients[1] = payable(address(0x56789)); + + mockAmounts[0] = 10; + mockAmounts[1] = 15; + + royaltyEngine = new MockRoyaltyEngineV1(mockRecipients, mockAmounts); + } + + function _setupAuctionForRoyaltyTests(address erc721TokenAddress) private returns (uint256 auctionId) { + // Sample auction parameters. + address assetContract = erc721TokenAddress; + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100; + uint64 endTimestamp = 200; + + // Approve Marketplace to transfer token. + vm.prank(seller); + IERC721(erc721TokenAddress).setApprovalForAll(marketplace, true); + + // Auction tokens. + IEnglishAuctions.AuctionParameters memory auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + vm.prank(seller); + auctionId = EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + function _buyoutAuctionForRoyaltyTests(uint256 auctionId) private returns (uint256 buyoutAmount) { + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + buyoutAmount = existingAuction.buyoutBidAmount; + + // Mint requisite total price to buyer. + erc20.mint(buyer, buyoutAmount); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.approve(marketplace, buyoutAmount); + + // Place buyout bid in auction. + vm.warp(existingAuction.startTimestamp); + vm.prank(buyer); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, buyoutAmount); + } + + function test_royaltyEngine_tokenWithCustomRoyalties() public { + ( + MockRoyaltyEngineV1 royaltyEngine, + address payable[] memory customRoyaltyRecipients, + uint256[] memory customRoyaltyAmounts + ) = _setupRoyaltyEngine(); + + // Add RoyaltyEngine to marketplace + vm.prank(marketplaceDeployer); + RoyaltyPaymentsLogic(marketplace).setRoyaltyEngine(address(royaltyEngine)); + + assertEq(RoyaltyPaymentsLogic(marketplace).getRoyaltyEngineAddress(), address(royaltyEngine)); + + // 1. ========= Create auction ========= + + // Mint the ERC721 tokens to seller. These tokens will be auctioned. + _setupERC721BalanceForSeller(seller, 1); + uint256 auctionId = _setupAuctionForRoyaltyTests(address(erc721)); + + // 2. ========= Bid in auction ========= + + uint256 buyoutAmount = _buyoutAuctionForRoyaltyTests(auctionId); + + // 3. ========= Seller collects auction payout + + vm.prank(seller); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + + // 4. ======== Check balances after royalty payments ======== + + { + uint256 defaultFee = (buyoutAmount * 100) / 10_000; + + // Royalty recipients receive correct amounts + assertBalERC20Eq(address(erc20), customRoyaltyRecipients[0], customRoyaltyAmounts[0]); + assertBalERC20Eq(address(erc20), customRoyaltyRecipients[1], customRoyaltyAmounts[1]); + + // Seller gets total price minus royalty amounts + assertBalERC20Eq( + address(erc20), + seller, + buyoutAmount - customRoyaltyAmounts[0] - customRoyaltyAmounts[1] - defaultFee + ); + } + } + + function test_royaltyEngine_tokenWithERC2981() public { + (MockRoyaltyEngineV1 royaltyEngine, , ) = _setupRoyaltyEngine(); + + // Add RoyaltyEngine to marketplace + vm.prank(marketplaceDeployer); + RoyaltyPaymentsLogic(marketplace).setRoyaltyEngine(address(royaltyEngine)); + + assertEq(RoyaltyPaymentsLogic(marketplace).getRoyaltyEngineAddress(), address(royaltyEngine)); + + // create token with ERC2981 + address royaltyRecipient = address(0x12345); + uint128 royaltyBps = 10; + ERC721Base nft2981 = new ERC721Base(address(0x12345), "NFT 2981", "NFT2981", royaltyRecipient, royaltyBps); + // Mint the ERC721 tokens to seller. These tokens will be listed. + vm.prank(address(0x12345)); + nft2981.mintTo(seller, ""); + + vm.prank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(nft2981)); + + // 1. ========= Create auction ========= + + uint256 auctionId = _setupAuctionForRoyaltyTests(address(nft2981)); + + // 2. ========= Bid in auction ========= + + uint256 buyoutAmount = _buyoutAuctionForRoyaltyTests(auctionId); + + // 3. ========= Seller collects auction payout + + vm.prank(seller); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + + // 4. ======== Check balances after royalty payments ======== + + { + uint256 defaultFee = (buyoutAmount * 100) / 10_000; + + uint256 royaltyAmount = (royaltyBps * buyoutAmount) / 10_000; + // Royalty recipient receives correct amounts + assertBalERC20Eq(address(erc20), royaltyRecipient, royaltyAmount); + + // Seller gets total price minus royalty amount + assertBalERC20Eq(address(erc20), seller, buyoutAmount - royaltyAmount - defaultFee); + } + } + + function test_noRoyaltyEngine_defaultERC2981Token() public { + // create token with ERC2981 + address royaltyRecipient = address(0x12345); + uint128 royaltyBps = 10; + ERC721Base nft2981 = new ERC721Base(address(0x12345), "NFT 2981", "NFT2981", royaltyRecipient, royaltyBps); + vm.prank(address(0x12345)); + nft2981.mintTo(seller, ""); + + vm.prank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(nft2981)); + + // 1. ========= Create auction ========= + + uint256 auctionId = _setupAuctionForRoyaltyTests(address(nft2981)); + + // 2. ========= Bid in auction ========= + + uint256 buyoutAmount = _buyoutAuctionForRoyaltyTests(auctionId); + + // 3. ========= Seller collects auction payout + + vm.prank(seller); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + + // 4. ======== Check balances after royalty payments ======== + + { + uint256 defaultFee = (buyoutAmount * 100) / 10_000; + uint256 royaltyAmount = (royaltyBps * buyoutAmount) / 10_000; + // Royalty recipient receives correct amounts + assertBalERC20Eq(address(erc20), royaltyRecipient, royaltyAmount); + + // Seller gets total price minus royalty amount + assertBalERC20Eq(address(erc20), seller, buyoutAmount - royaltyAmount - defaultFee); + } + } + + function test_royaltyEngine_correctlyDistributeAllFees() public { + ( + MockRoyaltyEngineV1 royaltyEngine, + address payable[] memory customRoyaltyRecipients, + uint256[] memory customRoyaltyAmounts + ) = _setupRoyaltyEngine(); + + // Add RoyaltyEngine to marketplace + vm.prank(marketplaceDeployer); + RoyaltyPaymentsLogic(marketplace).setRoyaltyEngine(address(royaltyEngine)); + + assertEq(RoyaltyPaymentsLogic(marketplace).getRoyaltyEngineAddress(), address(royaltyEngine)); + + // Set platform fee on marketplace + address platformFeeRecipient = marketplaceDeployer; + uint128 platformFeeBps = 5; + vm.prank(marketplaceDeployer); + IPlatformFee(marketplace).setPlatformFeeInfo(platformFeeRecipient, platformFeeBps); + + // 1. ========= Create auction ========= + + // Mint the ERC721 tokens to seller. These tokens will be auctioned. + _setupERC721BalanceForSeller(seller, 1); + uint256 auctionId = _setupAuctionForRoyaltyTests(address(erc721)); + + // 2. ========= Bid in auction ========= + + uint256 buyoutAmount = _buyoutAuctionForRoyaltyTests(auctionId); + + // 3. ========= Seller collects auction payout + + vm.prank(seller); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + + // 4. ======== Check balances after royalty payments ======== + + { + uint256 defaultFee = (buyoutAmount * 100) / 10_000; + + // Royalty recipients receive correct amounts + assertBalERC20Eq(address(erc20), customRoyaltyRecipients[0], customRoyaltyAmounts[0]); + assertBalERC20Eq(address(erc20), customRoyaltyRecipients[1], customRoyaltyAmounts[1]); + + // Platform fee recipient + uint256 platformFeeAmount = (platformFeeBps * buyoutAmount) / 10_000; + assertBalERC20Eq(address(erc20), platformFeeRecipient, platformFeeAmount); + + // Seller gets total price minus royalty amounts + assertBalERC20Eq( + address(erc20), + seller, + buyoutAmount - customRoyaltyAmounts[0] - customRoyaltyAmounts[1] - platformFeeAmount - defaultFee + ); + } + } + + function test_revert_feesExceedTotalPrice() public { + (MockRoyaltyEngineV1 royaltyEngine, , ) = _setupRoyaltyEngine(); + + // Add RoyaltyEngine to marketplace + vm.prank(marketplaceDeployer); + RoyaltyPaymentsLogic(marketplace).setRoyaltyEngine(address(royaltyEngine)); + + assertEq(RoyaltyPaymentsLogic(marketplace).getRoyaltyEngineAddress(), address(royaltyEngine)); + + // Set platform fee on marketplace + address platformFeeRecipient = marketplaceDeployer; + uint128 platformFeeBps = 9900; // equal to max bps 10_000 or 100% with 100 bps default + vm.prank(marketplaceDeployer); + IPlatformFee(marketplace).setPlatformFeeInfo(platformFeeRecipient, platformFeeBps); + + // 1. ========= Create auction ========= + + _setupERC721BalanceForSeller(seller, 1); + uint256 auctionId = _setupAuctionForRoyaltyTests(address(erc721)); + + // 2. ========= Bid in auction ========= + + IEnglishAuctions.Auction memory auction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + uint256 buyoutAmount = auction.buyoutBidAmount; + + // Mint requisite total price to buyer. + erc20.mint(buyer, buyoutAmount); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, buyoutAmount); + + // Buy tokens from auction. + vm.warp(auction.startTimestamp); + + vm.prank(buyer); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, buyoutAmount); + + // 3. ========= Seller collects auction payout + + vm.expectRevert("fees exceed the price"); + vm.prank(seller); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + } + + /*/////////////////////////////////////////////////////////////// + Create Auction + //////////////////////////////////////////////////////////////*/ + + function test_state_createAuction() public { + // Sample auction parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100; + uint64 endTimestamp = 200; + + // Mint the ERC721 tokens to seller. These tokens will be auctioned. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // Auction tokens. + IEnglishAuctions.AuctionParameters memory auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + vm.prank(seller); + uint256 auctionId = EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + + // Test consequent state of the contract. + + // Marketplace is owner of token. + assertIsOwnerERC721(address(erc721), marketplace, tokenIds); + + // Total listings incremented + assertEq(EnglishAuctionsLogic(marketplace).totalAuctions(), 1); + + // Fetch listing and verify state. + IEnglishAuctions.Auction memory auction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + assertEq(auction.auctionId, auctionId); + assertEq(auction.auctionCreator, seller); + assertEq(auction.assetContract, assetContract); + assertEq(auction.tokenId, tokenId); + assertEq(auction.quantity, quantity); + assertEq(auction.currency, currency); + assertEq(auction.minimumBidAmount, minimumBidAmount); + assertEq(auction.buyoutBidAmount, buyoutBidAmount); + assertEq(auction.timeBufferInSeconds, timeBufferInSeconds); + assertEq(auction.bidBufferBps, bidBufferBps); + assertEq(auction.startTimestamp, startTimestamp); + assertEq(auction.endTimestamp, endTimestamp); + assertEq(uint256(auction.tokenType), uint256(IEnglishAuctions.TokenType.ERC721)); + } + + function test_revert_createAuction_notOwnerOfAuctionedToken() public { + // Sample auction parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100; + uint64 endTimestamp = 200; + + // Don't mint to 'token to be auctioned' to the seller. + address someWallet = getActor(1000); + _setupERC721BalanceForSeller(someWallet, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), someWallet, tokenIds); + assertIsNotOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(someWallet); + erc721.setApprovalForAll(marketplace, true); + + // Auction tokens. + IEnglishAuctions.AuctionParameters memory auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + vm.prank(seller); + vm.expectRevert("ERC721: transfer from incorrect owner"); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + function test_revert_createAuction_notApprovedMarketplaceToTransferToken() public { + // Sample auction parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100; + uint64 endTimestamp = 200; + + // Mint the ERC721 tokens to seller. These tokens will be auctioned. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Don't approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, false); + + // Auction tokens. + IEnglishAuctions.AuctionParameters memory auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + vm.prank(seller); + vm.expectRevert("ERC721: caller is not token owner or approved"); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + function test_revert_createAuction_auctioningZeroQuantity() public { + // Sample auction parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 0; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100; + uint64 endTimestamp = 200; + + // Mint the ERC721 tokens to seller. These tokens will be auctioned. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // Auction tokens. + IEnglishAuctions.AuctionParameters memory auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + vm.prank(seller); + vm.expectRevert("Marketplace: auctioning zero quantity."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + function test_revert_createAuction_invalidQuantity() public { + // Sample auction parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 2; // Listing more than `1` quantity + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100; + uint64 endTimestamp = 200; + + // Mint the ERC721 tokens to seller. These tokens will be auctioned. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // Auction tokens. + IEnglishAuctions.AuctionParameters memory auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + vm.prank(seller); + vm.expectRevert("Marketplace: auctioning invalid quantity."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + function test_revert_createAuction_noBidOrTimeBuffer() public { + // Sample auction parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 0; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100; + uint64 endTimestamp = 200; + + // Mint the ERC721 tokens to seller. These tokens will be auctioned. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // Auction tokens. + IEnglishAuctions.AuctionParameters memory auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + vm.prank(seller); + vm.expectRevert("Marketplace: no time-buffer."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + + timeBufferInSeconds = 10 seconds; + bidBufferBps = 0; + + auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + vm.prank(seller); + vm.expectRevert("Marketplace: no bid-buffer."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + function test_revert_createAuction_invalidBidAmounts() public { + // Sample auction parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 10 ether; // set minimumBidAmount greater than buyoutBidAmount + uint256 buyoutBidAmount = 1 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100; + uint64 endTimestamp = 200; + + // Mint the ERC721 tokens to seller. These tokens will be auctioned. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // Auction tokens. + IEnglishAuctions.AuctionParameters memory auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + vm.prank(seller); + vm.expectRevert("Marketplace: invalid bid amounts."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + function test_revert_createAuction_invalidStartTimestamp() public { + uint256 blockTimestamp = 100 minutes; + // Set block.timestamp + vm.warp(blockTimestamp); + + // Sample auction parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = uint64(blockTimestamp - 61 minutes); // start time is less than block timestamp. + uint64 endTimestamp = startTimestamp + 1; + + // Mint the ERC721 tokens to seller. These tokens will be auctioned. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // Auction tokens. + IEnglishAuctions.AuctionParameters memory auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + vm.prank(seller); + vm.expectRevert("Marketplace: invalid timestamps."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + function test_revert_createAuction_invalidEndTimestamp() public { + // Sample auction parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100; + uint64 endTimestamp = startTimestamp - 1; + + // Mint the ERC721 tokens to seller. These tokens will be auctioned. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // Auction tokens. + IEnglishAuctions.AuctionParameters memory auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + vm.prank(seller); + vm.expectRevert("Marketplace: invalid timestamps."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + function test_revert_createAuction_invalidAssetContract() public { + // Sample auction parameters. + address assetContract = address(erc20); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100; + uint64 endTimestamp = startTimestamp - 1; + + // Auction tokens. + IEnglishAuctions.AuctionParameters memory auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + // Grant ERC20 token asset role. + vm.prank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc20)); + + vm.prank(seller); + vm.expectRevert("Marketplace: auctioned token must be ERC1155 or ERC721."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + function test_revert_createAuction_noListerRoleWhenRestrictionsActive() public { + // Sample auction parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100; + uint64 endTimestamp = 200; + + // Mint the ERC721 tokens to seller. These tokens will be auctioned. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // Auction tokens. + IEnglishAuctions.AuctionParameters memory auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + // Revoke LISTER_ROLE from seller. + vm.startPrank(marketplaceDeployer); + assertEq(Permissions(marketplace).hasRole(keccak256("LISTER_ROLE"), address(0)), false); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), seller); + assertEq(Permissions(marketplace).hasRole(keccak256("LISTER_ROLE"), seller), false); + + vm.stopPrank(); + + vm.prank(seller); + vm.expectRevert("!LISTER_ROLE"); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + function test_revert_createAuction_noAssetRoleWhenRestrictionsActive() public { + // Sample auction parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100; + uint64 endTimestamp = 200; + + // Mint the ERC721 tokens to seller. These tokens will be auctioned. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // Auction tokens. + IEnglishAuctions.AuctionParameters memory auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + // Revoke ASSET_ROLE from token to list. + vm.startPrank(marketplaceDeployer); + assertEq(Permissions(marketplace).hasRole(keccak256("ASSET_ROLE"), address(0)), false); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(erc721)); + assertEq(Permissions(marketplace).hasRole(keccak256("ASSET_ROLE"), address(erc721)), false); + + vm.stopPrank(); + + vm.prank(seller); + vm.expectRevert("!ASSET_ROLE"); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + /*/////////////////////////////////////////////////////////////// + Cancel Auction + //////////////////////////////////////////////////////////////*/ + + function _setup_newAuction() private returns (uint256 auctionId) { + // Sample auction parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100; + uint64 endTimestamp = 200; + + // Mint the ERC721 tokens to seller. These tokens will be auctioned. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // Auction tokens. + IEnglishAuctions.AuctionParameters memory auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + vm.prank(seller); + auctionId = EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + function _setup_newAuction_nativeToken() private returns (uint256 auctionId) { + // Sample auction parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = NATIVE_TOKEN; + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100; + uint64 endTimestamp = 200; + + // Mint the ERC721 tokens to seller. These tokens will be auctioned. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // Auction tokens. + IEnglishAuctions.AuctionParameters memory auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + vm.prank(seller); + auctionId = EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + function test_state_cancelAuction() public { + uint256 auctionId = _setup_newAuction(); + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingAuction.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingAuction.assetContract, address(erc721)); + + vm.prank(seller); + EnglishAuctionsLogic(marketplace).cancelAuction(auctionId); + + // Test consequent states. + + // Seller is owner of token. + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Total auction count should include deleted auctions too + assertEq(EnglishAuctionsLogic(marketplace).totalAuctions(), 1); + + // status should be `CANCELLED` + IEnglishAuctions.Auction memory cancelledAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + assertTrue(cancelledAuction.status == IEnglishAuctions.Status.CANCELLED); + } + + function test_revert_cancelAuction_bidsAlreadyMade() public { + uint256 auctionId = _setup_newAuction(); + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingAuction.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingAuction.assetContract, address(erc721)); + + vm.warp(existingAuction.startTimestamp); + + // place bid + erc20.mint(buyer, 1 ether); + vm.startPrank(buyer); + erc20.approve(marketplace, 1 ether); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, 1 ether); + vm.stopPrank(); + + vm.prank(seller); + vm.expectRevert("Marketplace: bids already made."); + EnglishAuctionsLogic(marketplace).cancelAuction(auctionId); + } + + /*/////////////////////////////////////////////////////////////// + Bid In Auction + //////////////////////////////////////////////////////////////*/ + + function test_state_bidInAuction_firstBid() public { + uint256 auctionId = _setup_newAuction(); + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingAuction.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingAuction.assetContract, address(erc721)); + + vm.warp(existingAuction.startTimestamp); + + // place bid + erc20.mint(buyer, 1 ether); + vm.startPrank(buyer); + erc20.approve(marketplace, 1 ether); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, 1 ether); + vm.stopPrank(); + + (address bidder, address currency, uint256 bidAmount) = EnglishAuctionsLogic(marketplace).getWinningBid( + auctionId + ); + + // Test consequent states. + // Seller is owner of token. + assertIsOwnerERC721(address(erc721), marketplace, tokenIds); + assertEq(erc20.balanceOf(marketplace), 1 ether); + assertEq(erc20.balanceOf(buyer), 0); + assertEq(buyer, bidder); + assertEq(currency, address(erc20)); + assertEq(bidAmount, 1 ether); + } + + function test_state_bidInAuction_secondBid() public { + uint256 auctionId = _setup_newAuction(); + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingAuction.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingAuction.assetContract, address(erc721)); + + vm.warp(existingAuction.startTimestamp); + + // place first bid + erc20.mint(buyer, 1 ether); + vm.startPrank(buyer); + erc20.approve(marketplace, 1 ether); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, 1 ether); + vm.stopPrank(); + + (address bidder, address currency, uint256 bidAmount) = EnglishAuctionsLogic(marketplace).getWinningBid( + auctionId + ); + + // Test consequent states. + // Seller is owner of token. + assertIsOwnerERC721(address(erc721), marketplace, tokenIds); + assertEq(erc20.balanceOf(marketplace), 1 ether); + assertEq(erc20.balanceOf(buyer), 0); + assertEq(buyer, bidder); + assertEq(currency, address(erc20)); + assertEq(bidAmount, 1 ether); + + // place second winning bid + erc20.mint(address(0x345), 2 ether); + vm.startPrank(address(0x345)); + erc20.approve(marketplace, 2 ether); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, 2 ether); + vm.stopPrank(); + + (bidder, currency, bidAmount) = EnglishAuctionsLogic(marketplace).getWinningBid(auctionId); + + // Test consequent states. + // Seller is owner of token. + assertIsOwnerERC721(address(erc721), marketplace, tokenIds); + assertEq(erc20.balanceOf(marketplace), 2 ether); + assertEq(erc20.balanceOf(buyer), 1 ether); + assertEq(erc20.balanceOf(address(0x345)), 0); + assertEq(address(0x345), bidder); + assertEq(currency, address(erc20)); + assertEq(bidAmount, 2 ether); + } + + function test_state_bidInAuction_buyoutBid() public { + uint256 auctionId = _setup_newAuction(); + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingAuction.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingAuction.assetContract, address(erc721)); + + vm.warp(existingAuction.startTimestamp); + + // place first bid + erc20.mint(buyer, 1 ether); + vm.startPrank(buyer); + erc20.approve(marketplace, 1 ether); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, 1 ether); + vm.stopPrank(); + + (address bidder, address currency, uint256 bidAmount) = EnglishAuctionsLogic(marketplace).getWinningBid( + auctionId + ); + + // Test consequent states. + // Seller is owner of token. + assertIsOwnerERC721(address(erc721), marketplace, tokenIds); + assertEq(erc20.balanceOf(marketplace), 1 ether); + assertEq(erc20.balanceOf(buyer), 0); + assertEq(buyer, bidder); + assertEq(currency, address(erc20)); + assertEq(bidAmount, 1 ether); + + // place buyout bid + erc20.mint(address(0x345), 10 ether); + vm.startPrank(address(0x345)); + erc20.approve(marketplace, 10 ether); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, 10 ether); + vm.stopPrank(); + + (bidder, currency, bidAmount) = EnglishAuctionsLogic(marketplace).getWinningBid(auctionId); + + // Test consequent states. + // Seller is owner of token. + assertIsOwnerERC721(address(erc721), address(0x345), tokenIds); + assertEq(erc20.balanceOf(marketplace), 10 ether); + assertEq(erc20.balanceOf(buyer), 1 ether); + assertEq(erc20.balanceOf(address(0x345)), 0); + assertEq(address(0x345), bidder); + assertEq(currency, address(erc20)); + assertEq(bidAmount, 10 ether); + } + + function test_revert_bidInAuction_inactiveAuction() public { + uint256 auctionId = _setup_newAuction(); + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingAuction.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingAuction.assetContract, address(erc721)); + + // place bid before start-time + erc20.mint(buyer, 1 ether); + vm.startPrank(buyer); + erc20.approve(marketplace, 1 ether); + vm.expectRevert("Marketplace: inactive auction."); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, 1 ether); + vm.stopPrank(); + + // place bid after end-time + vm.warp(existingAuction.endTimestamp); + + erc20.mint(buyer, 1 ether); + vm.startPrank(buyer); + erc20.approve(marketplace, 1 ether); + vm.expectRevert("Marketplace: inactive auction."); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, 1 ether); + vm.stopPrank(); + } + + function test_revert_bidInAuction_notOwnerOfBidTokens() public { + uint256 auctionId = _setup_newAuction(); + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingAuction.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingAuction.assetContract, address(erc721)); + + vm.warp(existingAuction.startTimestamp); + + // place bid + vm.startPrank(buyer); + erc20.approve(marketplace, 1 ether); + vm.expectRevert("ERC20: transfer amount exceeds balance"); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, 1 ether); + vm.stopPrank(); + } + + function test_revert_bidInAuction_notApprovedMarketplaceToTransferToken() public { + uint256 auctionId = _setup_newAuction(); + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingAuction.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingAuction.assetContract, address(erc721)); + + vm.warp(existingAuction.startTimestamp); + + // place bid + erc20.mint(buyer, 1 ether); + vm.startPrank(buyer); + vm.expectRevert("ERC20: insufficient allowance"); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, 1 ether); + vm.stopPrank(); + } + + function test_revert_bidInAuction_notNewWinningBid_firstBid() public { + uint256 auctionId = _setup_newAuction(); + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingAuction.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingAuction.assetContract, address(erc721)); + + vm.warp(existingAuction.startTimestamp); + + // place first bid less than minimum bid amount + erc20.mint(buyer, 0.5 ether); + vm.startPrank(buyer); + erc20.approve(marketplace, 0.5 ether); + vm.expectRevert("Marketplace: not winning bid."); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, 0.5 ether); + vm.stopPrank(); + } + + function test_revert_bidInAuction_notNewWinningBid_secondBid() public { + uint256 auctionId = _setup_newAuction(); + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingAuction.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingAuction.assetContract, address(erc721)); + + vm.warp(existingAuction.startTimestamp); + + // place first bid + erc20.mint(buyer, 1 ether); + vm.startPrank(buyer); + erc20.approve(marketplace, 1 ether); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, 1 ether); + vm.stopPrank(); + + (address bidder, address currency, uint256 bidAmount) = EnglishAuctionsLogic(marketplace).getWinningBid( + auctionId + ); + + // Test consequent states. + // Seller is owner of token. + assertIsOwnerERC721(address(erc721), marketplace, tokenIds); + assertEq(erc20.balanceOf(marketplace), 1 ether); + assertEq(erc20.balanceOf(buyer), 0); + assertEq(buyer, bidder); + assertEq(currency, address(erc20)); + assertEq(bidAmount, 1 ether); + + // place second bid less-than/equal-to previous winning bid + erc20.mint(address(0x345), 1 ether); + vm.startPrank(address(0x345)); + erc20.approve(marketplace, 1 ether); + vm.expectRevert("Marketplace: not winning bid."); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, 1 ether); + vm.stopPrank(); + } + + function test_state_bidInAuction_nativeToken() public { + uint256 auctionId = _setup_newAuction_nativeToken(); + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingAuction.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingAuction.assetContract, address(erc721)); + + vm.warp(existingAuction.startTimestamp); + + // place bid + vm.deal(buyer, 10 ether); + vm.startPrank(buyer); + EnglishAuctionsLogic(marketplace).bidInAuction{ value: 1 ether }(auctionId, 1 ether); + vm.stopPrank(); + + (address bidder, address currency, uint256 bidAmount) = EnglishAuctionsLogic(marketplace).getWinningBid( + auctionId + ); + + // Test consequent states. + // Seller is owner of token. + assertIsOwnerERC721(address(erc721), marketplace, tokenIds); + assertEq(weth.balanceOf(marketplace), 1 ether); + assertEq(buyer.balance, 9 ether); + assertEq(buyer, bidder); + assertEq(currency, NATIVE_TOKEN); + assertEq(bidAmount, 1 ether); + } + + /*/////////////////////////////////////////////////////////////// + Collect Auction Payout + //////////////////////////////////////////////////////////////*/ + + function test_state_collectAuctionPayout_buyoutBid() public { + uint256 auctionId = _setup_newAuction(); + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingAuction.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingAuction.assetContract, address(erc721)); + + vm.warp(existingAuction.startTimestamp); + + // place buyout bid + erc20.mint(buyer, 10 ether); + vm.startPrank(buyer); + erc20.approve(marketplace, 10 ether); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, 10 ether); + vm.stopPrank(); + + (address bidder, address currency, uint256 bidAmount) = EnglishAuctionsLogic(marketplace).getWinningBid( + auctionId + ); + + // Test consequent states. + // Seller is owner of token. + assertIsOwnerERC721(address(erc721), buyer, tokenIds); + assertEq(erc20.balanceOf(marketplace), 10 ether); + assertEq(erc20.balanceOf(buyer), 0); + assertEq(buyer, bidder); + assertEq(currency, address(erc20)); + assertEq(bidAmount, 10 ether); + + uint256 defaultFee = (10 ether * 100) / 10_000; + + // collect auction payout + vm.prank(seller); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + + assertEq(erc20.balanceOf(marketplace), 0); + assertEq(erc20.balanceOf(seller), 10 ether - defaultFee); + assertEq(erc20.balanceOf(defaultFeeRecipient), defaultFee); + } + + function test_state_collectAuctionPayout_afterAuctionEnds() public { + uint256 auctionId = _setup_newAuction(); + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingAuction.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingAuction.assetContract, address(erc721)); + + vm.warp(existingAuction.startTimestamp); + + // place bid + erc20.mint(buyer, 5 ether); + vm.startPrank(buyer); + erc20.approve(marketplace, 5 ether); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, 5 ether); + vm.stopPrank(); + + (address bidder, address currency, uint256 bidAmount) = EnglishAuctionsLogic(marketplace).getWinningBid( + auctionId + ); + + // Test consequent states. + // Seller is owner of token. + assertIsOwnerERC721(address(erc721), marketplace, tokenIds); + assertEq(erc20.balanceOf(marketplace), 5 ether); + assertEq(erc20.balanceOf(buyer), 0); + assertEq(buyer, bidder); + assertEq(currency, address(erc20)); + assertEq(bidAmount, 5 ether); + + vm.warp(existingAuction.endTimestamp); + + // collect auction payout + vm.prank(seller); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + + uint256 defaultFee = (5 ether * 100) / 10_000; + + assertIsOwnerERC721(address(erc721), marketplace, tokenIds); + assertEq(erc20.balanceOf(marketplace), 0); + assertEq(erc20.balanceOf(seller), 5 ether - defaultFee); + assertEq(erc20.balanceOf(defaultFeeRecipient), defaultFee); + } + + function test_revert_collectAuctionPayout_auctionNotExpired() public { + uint256 auctionId = _setup_newAuction(); + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingAuction.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingAuction.assetContract, address(erc721)); + + vm.warp(existingAuction.startTimestamp); + + // place bid + erc20.mint(buyer, 5 ether); + vm.startPrank(buyer); + erc20.approve(marketplace, 5 ether); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, 5 ether); + vm.stopPrank(); + + // collect auction payout before auction has ended + vm.prank(seller); + vm.expectRevert("Marketplace: auction still active."); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + } + + function test_revert_collectAuctionPayout_noBidsInAuction() public { + uint256 auctionId = _setup_newAuction(); + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingAuction.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingAuction.assetContract, address(erc721)); + + vm.warp(existingAuction.endTimestamp); + + // collect auction payout without any bids made + vm.prank(seller); + vm.expectRevert("Marketplace: no bids were made."); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + } + + /*/////////////////////////////////////////////////////////////// + Collect Auction Tokens + //////////////////////////////////////////////////////////////*/ + + function test_state_collectAuctionTokens() public { + uint256 auctionId = _setup_newAuction(); + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingAuction.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingAuction.assetContract, address(erc721)); + + vm.warp(existingAuction.startTimestamp); + + // place bid + erc20.mint(buyer, 5 ether); + vm.startPrank(buyer); + erc20.approve(marketplace, 5 ether); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, 5 ether); + vm.stopPrank(); + + (address bidder, address currency, uint256 bidAmount) = EnglishAuctionsLogic(marketplace).getWinningBid( + auctionId + ); + + // Test consequent states. + // Seller is owner of token. + assertIsOwnerERC721(address(erc721), marketplace, tokenIds); + assertEq(erc20.balanceOf(marketplace), 5 ether); + assertEq(erc20.balanceOf(buyer), 0); + assertEq(buyer, bidder); + assertEq(currency, address(erc20)); + assertEq(bidAmount, 5 ether); + + vm.warp(existingAuction.endTimestamp); + + // collect auction tokens + vm.prank(buyer); + EnglishAuctionsLogic(marketplace).collectAuctionTokens(auctionId); + + assertIsOwnerERC721(address(erc721), buyer, tokenIds); + assertEq(erc20.balanceOf(marketplace), 5 ether); + } + + function test_revert_collectAuctionTokens_auctionNotExpired() public { + uint256 auctionId = _setup_newAuction(); + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingAuction.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingAuction.assetContract, address(erc721)); + + vm.warp(existingAuction.startTimestamp); + + // place bid + erc20.mint(buyer, 5 ether); + vm.startPrank(buyer); + erc20.approve(marketplace, 5 ether); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, 5 ether); + vm.stopPrank(); + + (address bidder, address currency, uint256 bidAmount) = EnglishAuctionsLogic(marketplace).getWinningBid( + auctionId + ); + + // Test consequent states. + // Seller is owner of token. + assertIsOwnerERC721(address(erc721), marketplace, tokenIds); + assertEq(erc20.balanceOf(marketplace), 5 ether); + assertEq(erc20.balanceOf(buyer), 0); + assertEq(buyer, bidder); + assertEq(currency, address(erc20)); + assertEq(bidAmount, 5 ether); + + // collect auction tokens before auction has ended + vm.prank(buyer); + vm.expectRevert("Marketplace: auction still active."); + EnglishAuctionsLogic(marketplace).collectAuctionTokens(auctionId); + } + + /*/////////////////////////////////////////////////////////////// + View functions + //////////////////////////////////////////////////////////////*/ + + function test_state_isNewWinningBid() public { + uint256 auctionId = _setup_newAuction(); + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingAuction.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingAuction.assetContract, address(erc721)); + + vm.warp(existingAuction.startTimestamp); + + // place bid + erc20.mint(buyer, 5 ether); + vm.startPrank(buyer); + erc20.approve(marketplace, 5 ether); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, 5 ether); + vm.stopPrank(); + + // check if new winning bid + assertTrue(EnglishAuctionsLogic(marketplace).isNewWinningBid(auctionId, 6 ether)); + assertFalse(EnglishAuctionsLogic(marketplace).isNewWinningBid(auctionId, 5 ether)); + assertFalse(EnglishAuctionsLogic(marketplace).isNewWinningBid(auctionId, 4 ether)); + } + + function test_revert_isNewWinningBid() public { + uint256 auctionId = _setup_newAuction(); + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingAuction.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingAuction.assetContract, address(erc721)); + + vm.warp(existingAuction.startTimestamp); + + // place bid + erc20.mint(buyer, 5 ether); + vm.startPrank(buyer); + erc20.approve(marketplace, 5 ether); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, 5 ether); + vm.stopPrank(); + + // check winning bid for a non-existent auction + vm.expectRevert("Marketplace: invalid auction."); + EnglishAuctionsLogic(marketplace).isNewWinningBid(auctionId + 1, 6 ether); + } + + function test_state_getAllAuctions() public { + // Mint the ERC721 tokens to seller. These tokens will be auctioned. + _setupERC721BalanceForSeller(seller, 6); + + uint256[] memory auctionIds = new uint256[](5); + uint256[] memory tokenIds = new uint256[](5); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // Sample auction parameters. + address assetContract = address(erc721); + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = uint64(block.timestamp); + uint64 endTimestamp = startTimestamp + 200; + + IEnglishAuctions.AuctionParameters memory auctionParams; + + for (uint256 i = 0; i < 5; i += 1) { + tokenIds[i] = i; + + // Auction tokens. + auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenIds[i], + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + vm.prank(seller); + auctionIds[i] = EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + IEnglishAuctions.Auction[] memory activeAuctions = EnglishAuctionsLogic(marketplace).getAllAuctions(0, 4); + assertEq(activeAuctions.length, 5); + + for (uint256 i = 0; i < 5; i += 1) { + assertEq(activeAuctions[i].auctionId, auctionIds[i]); + assertEq(activeAuctions[i].auctionCreator, seller); + assertEq(activeAuctions[i].assetContract, assetContract); + assertEq(activeAuctions[i].tokenId, tokenIds[i]); + assertEq(activeAuctions[i].quantity, quantity); + assertEq(activeAuctions[i].currency, currency); + assertEq(activeAuctions[i].minimumBidAmount, minimumBidAmount); + assertEq(activeAuctions[i].buyoutBidAmount, buyoutBidAmount); + assertEq(activeAuctions[i].timeBufferInSeconds, timeBufferInSeconds); + assertEq(activeAuctions[i].bidBufferBps, bidBufferBps); + assertEq(activeAuctions[i].startTimestamp, startTimestamp); + assertEq(activeAuctions[i].endTimestamp, endTimestamp); + assertEq(uint256(activeAuctions[i].tokenType), uint256(IEnglishAuctions.TokenType.ERC721)); + } + } + + function test_state_getAllValidAuctions() public { + // Mint the ERC721 tokens to seller. These tokens will be auctioned. + _setupERC721BalanceForSeller(seller, 6); + + uint256[] memory auctionIds = new uint256[](5); + uint256[] memory tokenIds = new uint256[](5); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // Sample auction parameters. + address assetContract = address(erc721); + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = uint64(block.timestamp); + uint64 endTimestamp = startTimestamp + 200; + + IEnglishAuctions.AuctionParameters memory auctionParams; + + for (uint256 i = 0; i < 5; i += 1) { + tokenIds[i] = i; + + // Auction tokens. + auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenIds[i], + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + vm.prank(seller); + auctionIds[i] = EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + IEnglishAuctions.Auction[] memory activeAuctions = EnglishAuctionsLogic(marketplace).getAllValidAuctions(0, 4); + assertEq(activeAuctions.length, 5); + + for (uint256 i = 0; i < 5; i += 1) { + assertEq(activeAuctions[i].auctionId, auctionIds[i]); + assertEq(activeAuctions[i].auctionCreator, seller); + assertEq(activeAuctions[i].assetContract, assetContract); + assertEq(activeAuctions[i].tokenId, tokenIds[i]); + assertEq(activeAuctions[i].quantity, quantity); + assertEq(activeAuctions[i].currency, currency); + assertEq(activeAuctions[i].minimumBidAmount, minimumBidAmount); + assertEq(activeAuctions[i].buyoutBidAmount, buyoutBidAmount); + assertEq(activeAuctions[i].timeBufferInSeconds, timeBufferInSeconds); + assertEq(activeAuctions[i].bidBufferBps, bidBufferBps); + assertEq(activeAuctions[i].startTimestamp, startTimestamp); + assertEq(activeAuctions[i].endTimestamp, endTimestamp); + assertEq(uint256(activeAuctions[i].tokenType), uint256(IEnglishAuctions.TokenType.ERC721)); + } + + // create an inactive auction, and check the auctions returned + auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + 5, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp + 100, + endTimestamp + ); + + vm.prank(seller); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + + activeAuctions = EnglishAuctionsLogic(marketplace).getAllValidAuctions(0, 5); + assertEq(activeAuctions.length, 5); + } + + function test_state_isAuctionExpired() public { + uint256 auctionId = _setup_newAuction(); + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + vm.warp(existingAuction.endTimestamp); + assertTrue(EnglishAuctionsLogic(marketplace).isAuctionExpired(auctionId)); + } + + function test_revert_isAuctionExpired() public { + uint256 auctionId = _setup_newAuction(); + + vm.expectRevert("Marketplace: invalid auction."); + EnglishAuctionsLogic(marketplace).isAuctionExpired(auctionId + 1); + } + + /*/////////////////////////////////////////////////////////////// + Audit POCs + //////////////////////////////////////////////////////////////*/ + + function test_state_collectAuctionPayout_buyoutBid_nativeToken() public { + uint256 auctionId = _setup_newAuction_nativeToken(); + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingAuction.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingAuction.assetContract, address(erc721)); + + vm.warp(existingAuction.startTimestamp); + + // place bid + vm.deal(buyer, 10 ether); + vm.startPrank(buyer); + EnglishAuctionsLogic(marketplace).bidInAuction{ value: 10 ether }(auctionId, 10 ether); + vm.stopPrank(); + + (address bidder, address currency, uint256 bidAmount) = EnglishAuctionsLogic(marketplace).getWinningBid( + auctionId + ); + + // Test consequent states. + // Seller is owner of token. + assertIsOwnerERC721(address(erc721), buyer, tokenIds); + assertEq(weth.balanceOf(marketplace), 10 ether); + assertEq(buyer.balance, 0 ether); + assertEq(buyer, bidder); + assertEq(currency, NATIVE_TOKEN); + assertEq(bidAmount, 10 ether); + + uint256 defaultFee = (10 ether * 100) / 10_000; + + vm.prank(seller); + // calls WETH.withdraw (which calls receive function of Marketplace) and sends native tokens to seller + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + assertEq(weth.balanceOf(marketplace), 0 ether); + assertEq(seller.balance, 10 ether - defaultFee); + assertEq(defaultFeeRecipient.balance, defaultFee); + + // sending eth directly should fail + vm.deal(address(this), 1 ether); + (bool success, ) = marketplace.call{ value: 1 ether }(""); + assertEq(success, false); + } + + function test_audit_native_tokens_locked() public { + uint256 auctionId = _setup_newAuction(); + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingAuction.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingAuction.assetContract, address(erc721)); + + vm.warp(existingAuction.startTimestamp); + + // place buyout bid + erc20.mint(buyer, 10 ether); + vm.deal(buyer, 1 ether); + + vm.startPrank(buyer); + erc20.approve(marketplace, 10 ether); + + vm.expectRevert("Marketplace: invalid native tokens sent."); + EnglishAuctionsLogic(marketplace).bidInAuction{ value: 1 ether }(auctionId, 10 ether); + vm.stopPrank(); + + // No ether is temporary locked in contract + assertEq(marketplace.balance, 0); + } + + function test_revert_collectAuctionPayout_buyoutBid_poc() public { + /*/////////////////////////////////////////////////////////////// + Initial State + //////////////////////////////////////////////////////////////*/ + + // consider that market place already has 200 ETH worth of tokens from all bids made + erc20.mint(marketplace, 200 ether); + + /*/////////////////////////////////////////////////////////////// + Create Auction + //////////////////////////////////////////////////////////////*/ + + // Buyout bid : 10 ETH + uint256 auctionId = _setup_newAuction(); + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingAuction.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingAuction.assetContract, address(erc721)); + + vm.warp(existingAuction.startTimestamp); + + /*/////////////////////////////////////////////////////////////// + BID + //////////////////////////////////////////////////////////////*/ + + // place bid : 200 ETH + erc20.mint(buyer, 200 ether); + vm.startPrank(buyer); + erc20.approve(marketplace, 200 ether); + + vm.expectRevert("Marketplace: Bidding above buyout price."); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, 200 ether); + vm.stopPrank(); + } + + function _setup_nativeTokenAuction() private returns (uint256 auctionId) { + // Sample auction parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = NATIVE_TOKEN; + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100; + uint64 endTimestamp = 200; + + // Mint the ERC721 tokens to seller. These tokens will be auctioned. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // Auction tokens. + IEnglishAuctions.AuctionParameters memory auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + vm.prank(seller); + auctionId = EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + function test_revert_collectAuctionPayout_buyoutBid_nativeTokens_poc() public { + /*/////////////////////////////////////////////////////////////// + Initial State + //////////////////////////////////////////////////////////////*/ + + // consider that market place already has 200 ETH worth of tokens from all bids made + vm.deal(address(marketplace), 200 ether); + + /*/////////////////////////////////////////////////////////////// + Create Auction + //////////////////////////////////////////////////////////////*/ + + // Buyout bid : 10 ETH + uint256 auctionId = _setup_nativeTokenAuction(); + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingAuction.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingAuction.assetContract, address(erc721)); + + vm.warp(existingAuction.startTimestamp); + + /*/////////////////////////////////////////////////////////////// + BID + //////////////////////////////////////////////////////////////*/ + + // place bid : 200 ETH + vm.deal(buyer, 200 ether); + vm.prank(buyer); + vm.expectRevert("Marketplace: Bidding above buyout price."); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, 200 ether); + } +} + +contract BreitwieserTheCreator is BaseTest, IERC721Receiver, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + address private defaultFeeRecipient; + + function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) { + return IERC721Receiver.onERC721Received.selector; + } + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles for seller and assets + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc721)); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc1155)); + + vm.stopPrank(); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(buyer, "Buyer"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `EnglishAuctions` + address englishAuctions = address(new EnglishAuctionsLogic(address(weth))); + defaultFeeRecipient = 0x1Af20C6B23373350aD464700B5965CE4B0D2aD94; + vm.label(englishAuctions, "EnglishAuctions_Extension"); + + // Extension: EnglishAuctionsLogic + Extension memory extension_englishAuctions; + extension_englishAuctions.metadata = ExtensionMetadata({ + name: "EnglishAuctionsLogic", + metadataURI: "ipfs://EnglishAuctions", + implementation: englishAuctions + }); + + extension_englishAuctions.functions = new ExtensionFunction[](12); + extension_englishAuctions.functions[0] = ExtensionFunction( + EnglishAuctionsLogic.totalAuctions.selector, + "totalAuctions()" + ); + extension_englishAuctions.functions[1] = ExtensionFunction( + EnglishAuctionsLogic.createAuction.selector, + "createAuction((address,uint256,uint256,address,uint256,uint256,uint64,uint64,uint64,uint64))" + ); + extension_englishAuctions.functions[2] = ExtensionFunction( + EnglishAuctionsLogic.cancelAuction.selector, + "cancelAuction(uint256)" + ); + extension_englishAuctions.functions[3] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionPayout.selector, + "collectAuctionPayout(uint256)" + ); + extension_englishAuctions.functions[4] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionTokens.selector, + "collectAuctionTokens(uint256)" + ); + extension_englishAuctions.functions[5] = ExtensionFunction( + EnglishAuctionsLogic.bidInAuction.selector, + "bidInAuction(uint256,uint256)" + ); + extension_englishAuctions.functions[6] = ExtensionFunction( + EnglishAuctionsLogic.isNewWinningBid.selector, + "isNewWinningBid(uint256,uint256)" + ); + extension_englishAuctions.functions[7] = ExtensionFunction( + EnglishAuctionsLogic.getAuction.selector, + "getAuction(uint256)" + ); + extension_englishAuctions.functions[8] = ExtensionFunction( + EnglishAuctionsLogic.getAllAuctions.selector, + "getAllAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[9] = ExtensionFunction( + EnglishAuctionsLogic.getAllValidAuctions.selector, + "getAllValidAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[10] = ExtensionFunction( + EnglishAuctionsLogic.getWinningBid.selector, + "getWinningBid(uint256)" + ); + extension_englishAuctions.functions[11] = ExtensionFunction( + EnglishAuctionsLogic.isAuctionExpired.selector, + "isAuctionExpired(uint256)" + ); + + extensions[0] = extension_englishAuctions; + } + + function _setupERC721BalanceForSeller(address _seller, uint256 _numOfTokens) private { + erc721.mint(_seller, _numOfTokens); + } + + function test_rob_as_creator() public { + ///////////////////////////// Setup: dummy NFT //////////////////////////// + + // Sample auction parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 50 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 0; + uint64 endTimestamp = 200; + + // Mint the ERC721 tokens to seller. These tokens will be auctioned. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + ////////////////////////////// Setup: auction tokens ////////////////////////////////// + IEnglishAuctions.AuctionParameters memory auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + vm.prank(seller); + uint256 auctionId = EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + + /////////////////////////// Setup: marketplace has currency ///////////// + uint256 mbalance = 100 ether; + erc20.mint(marketplace, mbalance); + + /////////////////////////// Attack: win to drain /////////////////////////////////////////// + + // 1. Buy out the token. + assertEq(erc20.balanceOf(seller), 0); + erc20.mint(seller, buyoutBidAmount); + assertEq(erc20.balanceOf(seller), buyoutBidAmount); + + vm.startPrank(seller); + + erc20.approve(marketplace, buyoutBidAmount); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, buyoutBidAmount); + + // 2. Collect their own bid. + uint256 defaultFee = (buyoutBidAmount * 100) / 10_000; + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + assertEq(erc20.balanceOf(seller), buyoutBidAmount - defaultFee); + assertEq(erc20.balanceOf(defaultFeeRecipient), defaultFee); + + // 3. Profit. (FIXED) + + vm.expectRevert("Marketplace: payout already completed."); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + // EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + // assertEq(erc20.balanceOf(seller), buyoutBidAmount + mbalance); + } +} + +contract BreitwieserTheBidder is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles for seller and assets + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc721)); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc1155)); + + vm.stopPrank(); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(buyer, "Buyer"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `EnglishAuctions` + address englishAuctions = address(new EnglishAuctionsLogic(address(weth))); + vm.label(englishAuctions, "EnglishAuctions_Extension"); + + // Extension: EnglishAuctionsLogic + Extension memory extension_englishAuctions; + extension_englishAuctions.metadata = ExtensionMetadata({ + name: "EnglishAuctionsLogic", + metadataURI: "ipfs://EnglishAuctions", + implementation: englishAuctions + }); + + extension_englishAuctions.functions = new ExtensionFunction[](12); + extension_englishAuctions.functions[0] = ExtensionFunction( + EnglishAuctionsLogic.totalAuctions.selector, + "totalAuctions()" + ); + extension_englishAuctions.functions[1] = ExtensionFunction( + EnglishAuctionsLogic.createAuction.selector, + "createAuction((address,uint256,uint256,address,uint256,uint256,uint64,uint64,uint64,uint64))" + ); + extension_englishAuctions.functions[2] = ExtensionFunction( + EnglishAuctionsLogic.cancelAuction.selector, + "cancelAuction(uint256)" + ); + extension_englishAuctions.functions[3] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionPayout.selector, + "collectAuctionPayout(uint256)" + ); + extension_englishAuctions.functions[4] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionTokens.selector, + "collectAuctionTokens(uint256)" + ); + extension_englishAuctions.functions[5] = ExtensionFunction( + EnglishAuctionsLogic.bidInAuction.selector, + "bidInAuction(uint256,uint256)" + ); + extension_englishAuctions.functions[6] = ExtensionFunction( + EnglishAuctionsLogic.isNewWinningBid.selector, + "isNewWinningBid(uint256,uint256)" + ); + extension_englishAuctions.functions[7] = ExtensionFunction( + EnglishAuctionsLogic.getAuction.selector, + "getAuction(uint256)" + ); + extension_englishAuctions.functions[8] = ExtensionFunction( + EnglishAuctionsLogic.getAllAuctions.selector, + "getAllAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[9] = ExtensionFunction( + EnglishAuctionsLogic.getAllValidAuctions.selector, + "getAllValidAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[10] = ExtensionFunction( + EnglishAuctionsLogic.getWinningBid.selector, + "getWinningBid(uint256)" + ); + extension_englishAuctions.functions[11] = ExtensionFunction( + EnglishAuctionsLogic.isAuctionExpired.selector, + "isAuctionExpired(uint256)" + ); + + extensions[0] = extension_englishAuctions; + } + + function _setupERC721BalanceForSeller(address _seller, uint256 _numOfTokens) private { + erc721.mint(_seller, _numOfTokens); + } + + function test_rob_as_bidder() public { + address attacker = address(0xbeef); + vm.prank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), attacker); + + // Condition: multiple copies in circulation and attacker has at least 1. + uint256 tokenId = 999; + // Victim. + erc1155.mint(seller, tokenId, 1); + erc1155.mint(attacker, tokenId, 1); + + ////////////////// Setup: auction 1 ////////////////// + + IEnglishAuctions.AuctionParameters memory auctionParams1; + { + address assetContract = address(erc1155); + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint256 qty = 1; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 0; + uint64 endTimestamp = 200; + auctionParams1 = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + qty, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + } + + vm.startPrank(seller); + + erc1155.setApprovalForAll(marketplace, true); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams1); + + assertEq(erc1155.balanceOf(marketplace, tokenId), 1, "Marketplace should have the token."); + + vm.stopPrank(); + + ////////////////// Attack: auction the 2nd and steal the 1st token ////////////////// + + // 1. Set up auction. + erc20.mint(attacker, 1); + + vm.startPrank(attacker); + + erc1155.setApprovalForAll(marketplace, true); + + IEnglishAuctions.AuctionParameters memory auctionParams2; + { + address assetContract = address(erc1155); + address currency = address(erc20); + uint256 minimumBidAmount = 1; + uint256 buyoutBidAmount = 1; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 0; + uint64 endTimestamp = 200; + auctionParams2 = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + 1, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + } + uint256 auctionId2 = EnglishAuctionsLogic(marketplace).createAuction(auctionParams2); + + assertEq(erc1155.balanceOf(marketplace, tokenId), 2, "Marketplace should have 2 tokens."); + + // 2. Bid and collect back token. + erc20.increaseAllowance(marketplace, 1); + // Bid a small amount: 1 wei. + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId2, 1); + + assertEq(erc1155.balanceOf(attacker, tokenId), 1, "Attack should have collected back their token."); + + // Note: Attacker does not collect payout, it sets auction quantity to 0 and prevent further token collections. + + // 3. Fixed: Profit. + assertEq(erc1155.balanceOf(marketplace, tokenId), 1); + + vm.expectRevert("Marketplace: payout already completed."); + EnglishAuctionsLogic(marketplace).collectAuctionTokens(auctionId2); + + // assertEq(erc1155.balanceOf(attacker, tokenId), 2, "Attacker should have collected the 2nd token for free."); + + vm.stopPrank(); + } +} + +contract IssueC3_MarketplaceEnglishAuctionsTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles for seller and assets + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc721)); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc1155)); + + vm.stopPrank(); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(buyer, "Buyer"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `EnglishAuctions` + address englishAuctions = address(new EnglishAuctionsLogic(address(weth))); + vm.label(englishAuctions, "EnglishAuctions_Extension"); + + // Extension: EnglishAuctionsLogic + Extension memory extension_englishAuctions; + extension_englishAuctions.metadata = ExtensionMetadata({ + name: "EnglishAuctionsLogic", + metadataURI: "ipfs://EnglishAuctions", + implementation: englishAuctions + }); + + extension_englishAuctions.functions = new ExtensionFunction[](12); + extension_englishAuctions.functions[0] = ExtensionFunction( + EnglishAuctionsLogic.totalAuctions.selector, + "totalAuctions()" + ); + extension_englishAuctions.functions[1] = ExtensionFunction( + EnglishAuctionsLogic.createAuction.selector, + "createAuction((address,uint256,uint256,address,uint256,uint256,uint64,uint64,uint64,uint64))" + ); + extension_englishAuctions.functions[2] = ExtensionFunction( + EnglishAuctionsLogic.cancelAuction.selector, + "cancelAuction(uint256)" + ); + extension_englishAuctions.functions[3] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionPayout.selector, + "collectAuctionPayout(uint256)" + ); + extension_englishAuctions.functions[4] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionTokens.selector, + "collectAuctionTokens(uint256)" + ); + extension_englishAuctions.functions[5] = ExtensionFunction( + EnglishAuctionsLogic.bidInAuction.selector, + "bidInAuction(uint256,uint256)" + ); + extension_englishAuctions.functions[6] = ExtensionFunction( + EnglishAuctionsLogic.isNewWinningBid.selector, + "isNewWinningBid(uint256,uint256)" + ); + extension_englishAuctions.functions[7] = ExtensionFunction( + EnglishAuctionsLogic.getAuction.selector, + "getAuction(uint256)" + ); + extension_englishAuctions.functions[8] = ExtensionFunction( + EnglishAuctionsLogic.getAllAuctions.selector, + "getAllAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[9] = ExtensionFunction( + EnglishAuctionsLogic.getAllValidAuctions.selector, + "getAllValidAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[10] = ExtensionFunction( + EnglishAuctionsLogic.getWinningBid.selector, + "getWinningBid(uint256)" + ); + extension_englishAuctions.functions[11] = ExtensionFunction( + EnglishAuctionsLogic.isAuctionExpired.selector, + "isAuctionExpired(uint256)" + ); + + extensions[0] = extension_englishAuctions; + } + + function _setupERC1155BalanceForSeller(address _seller, uint256 _numOfTokens) private { + erc1155.mint(_seller, 0, _numOfTokens); + } + + function _setup_newAuction_1155() private returns (uint256 auctionId) { + // Sample auction parameters. + address assetContract = address(erc1155); + uint256 tokenId = 0; + uint256 quantity = 2; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100; + uint64 endTimestamp = 200; + + // Mint the erc1155 tokens to seller. These tokens will be auctioned. + _setupERC1155BalanceForSeller(seller, 2); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc1155.setApprovalForAll(marketplace, true); + + // Auction tokens. + IEnglishAuctions.AuctionParameters memory auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + vm.prank(seller); + auctionId = EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + function test_state_collectAuctionTokens_afterAuctionPayout() public { + uint256 auctionId = _setup_newAuction_1155(); + IEnglishAuctions.Auction memory existingAuction = EnglishAuctionsLogic(marketplace).getAuction(auctionId); + + // Verify existing auction at `auctionId` + assertEq(existingAuction.assetContract, address(erc1155)); + + vm.warp(existingAuction.startTimestamp); + + // place bid + erc20.mint(buyer, 5 ether); + vm.startPrank(buyer); + erc20.approve(marketplace, 5 ether); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, 5 ether); + vm.stopPrank(); + + (address bidder, address currency, uint256 bidAmount) = EnglishAuctionsLogic(marketplace).getWinningBid( + auctionId + ); + + // Seller is owner of token. + assertEq(erc20.balanceOf(marketplace), 5 ether); + assertEq(erc20.balanceOf(buyer), 0); + assertEq(buyer, bidder); + assertEq(currency, address(erc20)); + assertEq(bidAmount, 5 ether); + + vm.warp(existingAuction.endTimestamp); + + // collect auction payout + vm.prank(seller); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + + // collect buyer token + vm.prank(buyer); + EnglishAuctionsLogic(marketplace).collectAuctionTokens(auctionId); + + // token is NOT stuck in the marketplace + assertEq(erc1155.balanceOf(marketplace, 0), 0); + assertEq(erc1155.balanceOf(buyer, 0), 2); + } +} diff --git a/src/test/marketplace/Offers.t.sol b/src/test/marketplace/Offers.t.sol new file mode 100644 index 000000000..7a982f98b --- /dev/null +++ b/src/test/marketplace/Offers.t.sol @@ -0,0 +1,1102 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test helper imports +import "../utils/BaseTest.sol"; + +// Test contracts and interfaces + +import { PluginMap, IPluginMap } from "contracts/extension/plugin/PluginMap.sol"; +import { RoyaltyPaymentsLogic } from "contracts/extension/plugin/RoyaltyPayments.sol"; +import { MarketplaceV3, IPlatformFee } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { OffersLogic } from "contracts/prebuilts/marketplace/offers/OffersLogic.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { ERC721Base } from "contracts/base/ERC721Base.sol"; +import { MockRoyaltyEngineV1 } from "../mocks/MockRoyaltyEngineV1.sol"; + +import { IOffers } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +contract MarketplaceOffersTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + address private defaultFeeRecipient; + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles for seller and assets + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc721)); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc1155)); + + vm.stopPrank(); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(buyer, "Buyer"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `Offers` + address offers = address(new OffersLogic()); + defaultFeeRecipient = 0x1Af20C6B23373350aD464700B5965CE4B0D2aD94; + vm.label(offers, "Offers_Extension"); + + // Extension: OffersLogic + Extension memory extension_offers; + extension_offers.metadata = ExtensionMetadata({ + name: "OffersLogic", + metadataURI: "ipfs://Offers", + implementation: offers + }); + + extension_offers.functions = new ExtensionFunction[](7); + extension_offers.functions[0] = ExtensionFunction(OffersLogic.totalOffers.selector, "totalOffers()"); + extension_offers.functions[1] = ExtensionFunction( + OffersLogic.makeOffer.selector, + "makeOffer((address,uint256,uint256,address,uint256,uint256))" + ); + extension_offers.functions[2] = ExtensionFunction(OffersLogic.cancelOffer.selector, "cancelOffer(uint256)"); + extension_offers.functions[3] = ExtensionFunction(OffersLogic.acceptOffer.selector, "acceptOffer(uint256)"); + extension_offers.functions[4] = ExtensionFunction( + OffersLogic.getAllValidOffers.selector, + "getAllValidOffers(uint256,uint256)" + ); + extension_offers.functions[5] = ExtensionFunction( + OffersLogic.getAllOffers.selector, + "getAllOffers(uint256,uint256)" + ); + extension_offers.functions[6] = ExtensionFunction(OffersLogic.getOffer.selector, "getOffer(uint256)"); + + extensions[0] = extension_offers; + } + + function test_state_initial() public { + uint256 totalOffers = OffersLogic(marketplace).totalOffers(); + assertEq(totalOffers, 0); + } + + /*/////////////////////////////////////////////////////////////// + Royalty Tests (incl Royalty Engine / Registry) + //////////////////////////////////////////////////////////////*/ + + function _setupRoyaltyEngine() + private + returns ( + MockRoyaltyEngineV1 royaltyEngine, + address payable[] memory mockRecipients, + uint256[] memory mockAmounts + ) + { + mockRecipients = new address payable[](2); + mockAmounts = new uint256[](2); + + mockRecipients[0] = payable(address(0x12345)); + mockRecipients[1] = payable(address(0x56789)); + + mockAmounts[0] = 10; + mockAmounts[1] = 15; + + royaltyEngine = new MockRoyaltyEngineV1(mockRecipients, mockAmounts); + } + + function _setupOfferForRoyaltyTests(address erc721TokenAddress) private returns (uint256 offerId) { + // Sample offer parameters. + address assetContract = erc721TokenAddress; + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 totalPrice = 1 ether; + uint256 expirationTimestamp = 200; + + // mint total-price to buyer + erc20.mint(buyer, totalPrice); + + // Approve Marketplace to transfer currency tokens. + vm.prank(buyer); + erc20.approve(marketplace, totalPrice); + + // Make offer. + IOffers.OfferParams memory offerParams = IOffers.OfferParams( + assetContract, + tokenId, + quantity, + currency, + totalPrice, + expirationTimestamp + ); + + vm.prank(buyer); + offerId = OffersLogic(marketplace).makeOffer(offerParams); + } + + function _acceptOfferForRoyaltyTests(uint256 offerId) private returns (uint256 totalPrice) { + IOffers.Offer memory offer = OffersLogic(marketplace).getOffer(offerId); + + totalPrice = offer.totalPrice; + + // Approve Marketplace to transfer token. + vm.prank(seller); + IERC721(offer.assetContract).setApprovalForAll(marketplace, true); + + // Accept offer + vm.prank(seller); + OffersLogic(marketplace).acceptOffer(offerId); + } + + function test_royaltyEngine_tokenWithCustomRoyalties() public { + ( + MockRoyaltyEngineV1 royaltyEngine, + address payable[] memory customRoyaltyRecipients, + uint256[] memory customRoyaltyAmounts + ) = _setupRoyaltyEngine(); + + // Add RoyaltyEngine to marketplace + vm.prank(marketplaceDeployer); + RoyaltyPaymentsLogic(marketplace).setRoyaltyEngine(address(royaltyEngine)); + + assertEq(RoyaltyPaymentsLogic(marketplace).getRoyaltyEngineAddress(), address(royaltyEngine)); + + // 1. ========= Make offer ========= + + uint256 offerId = _setupOfferForRoyaltyTests(address(erc721)); + + // 2. ========= Accept offer ========= + + // Mint the ERC721 tokens to seller. These tokens will be sold. + erc721.mint(seller, 1); + uint256 totalPrice = _acceptOfferForRoyaltyTests(offerId); + + // 3. ======== Check balances after royalty payments ======== + + { + uint256 defaultFee = (totalPrice * 100) / 10_000; + + // Royalty recipients receive correct amounts + assertBalERC20Eq(address(erc20), customRoyaltyRecipients[0], customRoyaltyAmounts[0]); + assertBalERC20Eq(address(erc20), customRoyaltyRecipients[1], customRoyaltyAmounts[1]); + + // Seller gets total price minus royalty amounts + assertBalERC20Eq( + address(erc20), + seller, + totalPrice - customRoyaltyAmounts[0] - customRoyaltyAmounts[1] - defaultFee + ); + assertBalERC20Eq(address(erc20), defaultFeeRecipient, defaultFee); + } + } + + function test_royaltyEngine_tokenWithERC2981() public { + (MockRoyaltyEngineV1 royaltyEngine, , ) = _setupRoyaltyEngine(); + + // Add RoyaltyEngine to marketplace + vm.prank(marketplaceDeployer); + RoyaltyPaymentsLogic(marketplace).setRoyaltyEngine(address(royaltyEngine)); + + assertEq(RoyaltyPaymentsLogic(marketplace).getRoyaltyEngineAddress(), address(royaltyEngine)); + + // create token with ERC2981 + address royaltyRecipient = address(0x12345); + uint128 royaltyBps = 10; + ERC721Base nft2981 = new ERC721Base(address(0x12345), "NFT 2981", "NFT2981", royaltyRecipient, royaltyBps); + // Mint the ERC721 tokens to seller. These tokens will be sold. + vm.prank(address(0x12345)); + nft2981.mintTo(seller, ""); + + vm.prank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(nft2981)); + + // 1. ========= Make offer ========= + + uint256 offerId = _setupOfferForRoyaltyTests(address(nft2981)); + + // 2. ========= Accept offer ========= + + uint256 totalPrice = _acceptOfferForRoyaltyTests(offerId); + + // 3. ======== Check balances after royalty payments ======== + + { + uint256 defaultFee = (totalPrice * 100) / 10_000; + uint256 royaltyAmount = (royaltyBps * totalPrice) / 10_000; + // Royalty recipient receives correct amounts + assertBalERC20Eq(address(erc20), royaltyRecipient, royaltyAmount); + + // Seller gets total price minus royalty amount + assertBalERC20Eq(address(erc20), seller, totalPrice - royaltyAmount - defaultFee); + assertBalERC20Eq(address(erc20), defaultFeeRecipient, defaultFee); + } + } + + function test_noRoyaltyEngine_defaultERC2981Token() public { + // create token with ERC2981 + address royaltyRecipient = address(0x12345); + uint128 royaltyBps = 10; + ERC721Base nft2981 = new ERC721Base(address(0x12345), "NFT 2981", "NFT2981", royaltyRecipient, royaltyBps); + vm.prank(address(0x12345)); + nft2981.mintTo(seller, ""); + + vm.prank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(nft2981)); + + // 1. ========= Make offer ========= + + uint256 offerId = _setupOfferForRoyaltyTests(address(nft2981)); + + // 2. ========= Accept offer ========= + + uint256 totalPrice = _acceptOfferForRoyaltyTests(offerId); + + // 3. ======== Check balances after royalty payments ======== + + { + uint256 defaultFee = (totalPrice * 100) / 10_000; + + uint256 royaltyAmount = (royaltyBps * totalPrice) / 10_000; + // Royalty recipient receives correct amounts + assertBalERC20Eq(address(erc20), royaltyRecipient, royaltyAmount); + + // Seller gets total price minus royalty amount + assertBalERC20Eq(address(erc20), seller, totalPrice - royaltyAmount - defaultFee); + assertBalERC20Eq(address(erc20), defaultFeeRecipient, defaultFee); + } + } + + function test_royaltyEngine_correctlyDistributeAllFees() public { + ( + MockRoyaltyEngineV1 royaltyEngine, + address payable[] memory customRoyaltyRecipients, + uint256[] memory customRoyaltyAmounts + ) = _setupRoyaltyEngine(); + + // Add RoyaltyEngine to marketplace + vm.prank(marketplaceDeployer); + RoyaltyPaymentsLogic(marketplace).setRoyaltyEngine(address(royaltyEngine)); + + assertEq(RoyaltyPaymentsLogic(marketplace).getRoyaltyEngineAddress(), address(royaltyEngine)); + + // Set platform fee on marketplace + address platformFeeRecipient = marketplaceDeployer; + uint128 platformFeeBps = 5; + vm.prank(marketplaceDeployer); + IPlatformFee(marketplace).setPlatformFeeInfo(platformFeeRecipient, platformFeeBps); + + // 1. ========= Make offer ========= + + uint256 offerId = _setupOfferForRoyaltyTests(address(erc721)); + + // 2. ========= Accept offer ========= + + // Mint the ERC721 tokens to seller. These tokens will be sold. + erc721.mint(seller, 1); + uint256 totalPrice = _acceptOfferForRoyaltyTests(offerId); + + // 3. ======== Check balances after royalty payments ======== + + { + uint256 defaultFee = (totalPrice * 100) / 10_000; + + // Royalty recipients receive correct amounts + assertBalERC20Eq(address(erc20), customRoyaltyRecipients[0], customRoyaltyAmounts[0]); + assertBalERC20Eq(address(erc20), customRoyaltyRecipients[1], customRoyaltyAmounts[1]); + + // Platform fee recipient + uint256 platformFeeAmount = (platformFeeBps * totalPrice) / 10_000; + assertBalERC20Eq(address(erc20), platformFeeRecipient, platformFeeAmount); + + // Seller gets total price minus royalty amounts + assertBalERC20Eq( + address(erc20), + seller, + totalPrice - customRoyaltyAmounts[0] - customRoyaltyAmounts[1] - platformFeeAmount - defaultFee + ); + + assertBalERC20Eq(address(erc20), defaultFeeRecipient, defaultFee); + } + } + + function test_revert_feesExceedTotalPrice() public { + (MockRoyaltyEngineV1 royaltyEngine, , ) = _setupRoyaltyEngine(); + + // Add RoyaltyEngine to marketplace + vm.prank(marketplaceDeployer); + RoyaltyPaymentsLogic(marketplace).setRoyaltyEngine(address(royaltyEngine)); + + assertEq(RoyaltyPaymentsLogic(marketplace).getRoyaltyEngineAddress(), address(royaltyEngine)); + + // Set platform fee on marketplace + address platformFeeRecipient = marketplaceDeployer; + uint128 platformFeeBps = 9900; // equal to max bps 10_000 or 100% with 100 bps default + vm.prank(marketplaceDeployer); + IPlatformFee(marketplace).setPlatformFeeInfo(platformFeeRecipient, platformFeeBps); + + // 1. ========= Make offer ========= + + uint256 offerId = _setupOfferForRoyaltyTests(address(erc721)); + + // 2. ========= Accept offer ========= + + // Mint the ERC721 tokens to seller. These tokens will be sold. + erc721.mint(seller, 1); + + IOffers.Offer memory offer = OffersLogic(marketplace).getOffer(offerId); + + // Approve Marketplace to transfer token. + vm.prank(seller); + IERC721(offer.assetContract).setApprovalForAll(marketplace, true); + + // Accept offer + vm.expectRevert("fees exceed the price"); + vm.prank(seller); + OffersLogic(marketplace).acceptOffer(offerId); + } + + /*/////////////////////////////////////////////////////////////// + Make Offer + //////////////////////////////////////////////////////////////*/ + + function test_state_makeOffer() public { + // Sample offer parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 totalPrice = 1 ether; + uint256 expirationTimestamp = 200; + + // mint total-price to buyer + erc20.mint(buyer, totalPrice); + + // Approve Marketplace to transfer currency tokens. + vm.prank(buyer); + erc20.approve(marketplace, totalPrice); + + // Make offer. + IOffers.OfferParams memory offerParams = IOffers.OfferParams( + assetContract, + tokenId, + quantity, + currency, + totalPrice, + expirationTimestamp + ); + + vm.prank(buyer); + uint256 offerId = OffersLogic(marketplace).makeOffer(offerParams); + + // Test consequent state of the contract. + + // Total offers incremented + assertEq(OffersLogic(marketplace).totalOffers(), 1); + + // Fetch listing and verify state. + IOffers.Offer memory offer = OffersLogic(marketplace).getOffer(offerId); + + assertEq(offer.offerId, offerId); + assertEq(offer.offeror, buyer); + assertEq(offer.assetContract, assetContract); + assertEq(offer.tokenId, tokenId); + assertEq(offer.quantity, quantity); + assertEq(offer.currency, currency); + assertEq(offer.totalPrice, totalPrice); + assertEq(offer.expirationTimestamp, expirationTimestamp); + assertEq(uint256(offer.tokenType), uint256(IOffers.TokenType.ERC721)); + } + + function test_revert_makeOffer_notOwnerOfOfferedTokens() public { + // Sample offer parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 totalPrice = 1 ether; + uint256 expirationTimestamp = 200; + + // Approve Marketplace to transfer currency tokens. (without owning) + vm.prank(buyer); + erc20.approve(marketplace, totalPrice); + + // Make offer. + IOffers.OfferParams memory offerParams = IOffers.OfferParams( + assetContract, + tokenId, + quantity, + currency, + totalPrice, + expirationTimestamp + ); + + vm.prank(buyer); + vm.expectRevert("Marketplace: insufficient currency balance."); + OffersLogic(marketplace).makeOffer(offerParams); + } + + function test_revert_makeOffer_notApprovedMarketplaceToTransferTokens() public { + // Sample offer parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 totalPrice = 1 ether; + uint256 expirationTimestamp = 200; + + // mint total-price to buyer, but not approved to marketplace + erc20.mint(buyer, totalPrice); + + // Make offer. + IOffers.OfferParams memory offerParams = IOffers.OfferParams( + assetContract, + tokenId, + quantity, + currency, + totalPrice, + expirationTimestamp + ); + + vm.prank(buyer); + vm.expectRevert("Marketplace: insufficient currency balance."); + OffersLogic(marketplace).makeOffer(offerParams); + } + + function test_revert_makeOffer_wantedZeroTokens() public { + // Sample offer parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 0; + address currency = address(erc20); + uint256 totalPrice = 1 ether; + uint256 expirationTimestamp = 200; + + // mint total-price to buyer + erc20.mint(buyer, totalPrice); + + // Approve Marketplace to transfer currency tokens. + vm.prank(buyer); + erc20.approve(marketplace, totalPrice); + + // Make offer. + IOffers.OfferParams memory offerParams = IOffers.OfferParams( + assetContract, + tokenId, + quantity, + currency, + totalPrice, + expirationTimestamp + ); + + vm.prank(buyer); + vm.expectRevert("Marketplace: wanted zero tokens."); + OffersLogic(marketplace).makeOffer(offerParams); + } + + function test_revert_makeOffer_invalidQuantity() public { + // Sample offer parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 2; // Asking for more than `1` quantity of erc721 tokenId + address currency = address(erc20); + uint256 totalPrice = 1 ether; + uint256 expirationTimestamp = 200; + + // mint total-price to buyer + erc20.mint(buyer, totalPrice); + + // Approve Marketplace to transfer currency tokens. + vm.prank(buyer); + erc20.approve(marketplace, totalPrice); + + // Make offer. + IOffers.OfferParams memory offerParams = IOffers.OfferParams( + assetContract, + tokenId, + quantity, + currency, + totalPrice, + expirationTimestamp + ); + + vm.prank(buyer); + vm.expectRevert("Marketplace: wanted invalid quantity."); + OffersLogic(marketplace).makeOffer(offerParams); + } + + function test_revert_makeOffer_invalidExpirationTimestamp() public { + uint256 blockTimestamp = 100 minutes; + // Set block.timestamp + vm.warp(blockTimestamp); + + // Sample offer parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 totalPrice = 1 ether; + uint256 expirationTimestamp = blockTimestamp - 61 minutes; + + // mint total-price to buyer + erc20.mint(buyer, totalPrice); + + // Approve Marketplace to transfer currency tokens. + vm.prank(buyer); + erc20.approve(marketplace, totalPrice); + + // Make offer. + IOffers.OfferParams memory offerParams = IOffers.OfferParams( + assetContract, + tokenId, + quantity, + currency, + totalPrice, + expirationTimestamp + ); + + vm.prank(buyer); + vm.expectRevert("Marketplace: invalid expiration timestamp."); + OffersLogic(marketplace).makeOffer(offerParams); + } + + function test_revert_makeOffer_invalidAssetContract() public { + // Sample offer parameters. + address assetContract = address(erc20); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 totalPrice = 1 ether; + uint256 expirationTimestamp = block.timestamp; + + // mint total-price to buyer + erc20.mint(buyer, totalPrice); + + // Approve Marketplace to transfer currency tokens. + vm.prank(buyer); + erc20.approve(marketplace, totalPrice); + + // Make offer. + IOffers.OfferParams memory offerParams = IOffers.OfferParams( + assetContract, + tokenId, + quantity, + currency, + totalPrice, + expirationTimestamp + ); + + // Grant ERC20 token asset role. + vm.prank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc20)); + + vm.prank(buyer); + vm.expectRevert("Marketplace: token must be ERC1155 or ERC721."); + OffersLogic(marketplace).makeOffer(offerParams); + } + + function test_revert_createListing_noAssetRoleWhenRestrictionsActive() public { + // Sample offer parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 totalPrice = 1 ether; + uint256 expirationTimestamp = block.timestamp; + + // mint total-price to buyer + erc20.mint(buyer, totalPrice); + + // Approve Marketplace to transfer currency tokens. + vm.prank(buyer); + erc20.approve(marketplace, totalPrice); + + // Make offer. + IOffers.OfferParams memory offerParams = IOffers.OfferParams( + assetContract, + tokenId, + quantity, + currency, + totalPrice, + expirationTimestamp + ); + + // Revoke ASSET_ROLE from token to list. + vm.startPrank(marketplaceDeployer); + assertEq(Permissions(marketplace).hasRole(keccak256("ASSET_ROLE"), address(0)), false); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(erc721)); + assertEq(Permissions(marketplace).hasRole(keccak256("ASSET_ROLE"), address(erc721)), false); + + vm.stopPrank(); + + vm.prank(buyer); + vm.expectRevert("!ASSET_ROLE"); + OffersLogic(marketplace).makeOffer(offerParams); + } + + /*/////////////////////////////////////////////////////////////// + Cancel Offer + //////////////////////////////////////////////////////////////*/ + + function test_state_cancelOffer() public { + // Sample offer parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 totalPrice = 1 ether; + uint256 expirationTimestamp = 200; + + // mint total-price to buyer + erc20.mint(buyer, totalPrice); + + // Approve Marketplace to transfer currency tokens. + vm.prank(buyer); + erc20.approve(marketplace, totalPrice); + + // Make offer. + IOffers.OfferParams memory offerParams = IOffers.OfferParams( + assetContract, + tokenId, + quantity, + currency, + totalPrice, + expirationTimestamp + ); + + vm.prank(buyer); + uint256 offerId = OffersLogic(marketplace).makeOffer(offerParams); + + IOffers.Offer memory offer = OffersLogic(marketplace).getOffer(offerId); + + assertEq(offer.offerId, offerId); + assertEq(offer.offeror, buyer); + assertEq(offer.assetContract, assetContract); + assertEq(offer.tokenId, tokenId); + assertEq(offer.quantity, quantity); + assertEq(offer.currency, currency); + assertEq(offer.totalPrice, totalPrice); + assertEq(offer.expirationTimestamp, expirationTimestamp); + assertEq(uint256(offer.tokenType), uint256(IOffers.TokenType.ERC721)); + + vm.prank(buyer); + OffersLogic(marketplace).cancelOffer(offerId); + + // Total offers count shouldn't change + assertEq(OffersLogic(marketplace).totalOffers(), 1); + + // status should be `CANCELLED` + IOffers.Offer memory cancelledOffer = OffersLogic(marketplace).getOffer(offerId); + assertTrue(cancelledOffer.status == IOffers.Status.CANCELLED); + } + + function test_revert_cancelOffer_callerNotOfferor() public { + // Sample offer parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 totalPrice = 1 ether; + uint256 expirationTimestamp = 200; + + // mint total-price to buyer + erc20.mint(buyer, totalPrice); + + // Approve Marketplace to transfer currency tokens. + vm.prank(buyer); + erc20.approve(marketplace, totalPrice); + + // Make offer. + IOffers.OfferParams memory offerParams = IOffers.OfferParams( + assetContract, + tokenId, + quantity, + currency, + totalPrice, + expirationTimestamp + ); + + vm.prank(buyer); + uint256 offerId = OffersLogic(marketplace).makeOffer(offerParams); + + vm.prank(address(0x345)); + vm.expectRevert("!Offeror"); + OffersLogic(marketplace).cancelOffer(offerId); + } + + /*/////////////////////////////////////////////////////////////// + Accept Offer + //////////////////////////////////////////////////////////////*/ + + function test_state_acceptOffer() public { + // set owner of NFT + erc721.mint(seller, 1); + + // Sample offer parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 totalPrice = 1 ether; + uint256 expirationTimestamp = 200; + + // mint total-price to buyer + erc20.mint(buyer, totalPrice); + + // Approve Marketplace to transfer currency tokens. + vm.prank(buyer); + erc20.approve(marketplace, totalPrice); + + // Make offer. + IOffers.OfferParams memory offerParams = IOffers.OfferParams( + assetContract, + tokenId, + quantity, + currency, + totalPrice, + expirationTimestamp + ); + + vm.prank(buyer); + uint256 offerId = OffersLogic(marketplace).makeOffer(offerParams); + + // accept offer + vm.startPrank(seller); + erc721.setApprovalForAll(marketplace, true); + OffersLogic(marketplace).acceptOffer(offerId); + vm.stopPrank(); + + // Total offers count shouldn't change + assertEq(OffersLogic(marketplace).totalOffers(), 1); + + // status should be `COMPLETED` + IOffers.Offer memory completedOffer = OffersLogic(marketplace).getOffer(offerId); + assertTrue(completedOffer.status == IOffers.Status.COMPLETED); + + uint256 defaultFee = (totalPrice * 100) / 10_000; + // check states after accepting offer + assertEq(erc721.ownerOf(tokenId), buyer); + assertEq(erc20.balanceOf(seller), totalPrice - defaultFee); + assertEq(erc20.balanceOf(defaultFeeRecipient), defaultFee); + assertEq(erc20.balanceOf(buyer), 0); + } + + function test_revert_acceptOffer_notOwnedRequiredTokens() public { + // set owner of NFT to address other than seller + erc721.mint(address(0x345), 1); + + // Sample offer parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 totalPrice = 1 ether; + uint256 expirationTimestamp = 200; + + // mint total-price to buyer + erc20.mint(buyer, totalPrice); + + // Approve Marketplace to transfer currency tokens. (but not owned) + vm.prank(buyer); + erc20.approve(marketplace, totalPrice); + + // Make offer. + IOffers.OfferParams memory offerParams = IOffers.OfferParams( + assetContract, + tokenId, + quantity, + currency, + totalPrice, + expirationTimestamp + ); + + vm.prank(buyer); + uint256 offerId = OffersLogic(marketplace).makeOffer(offerParams); + + // accept offer + vm.startPrank(seller); + erc721.setApprovalForAll(marketplace, true); + vm.expectRevert("Marketplace: not owner or approved tokens."); + OffersLogic(marketplace).acceptOffer(offerId); + vm.stopPrank(); + } + + function test_revert_acceptOffer_notApprovedMarketplaceToTransferOfferedTokens() public { + // set owner of NFT + erc721.mint(seller, 1); + + // Sample offer parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 totalPrice = 1 ether; + uint256 expirationTimestamp = 200; + + // mint total-price to buyer + erc20.mint(buyer, totalPrice); + + // Approve Marketplace to transfer currency tokens. (but not owned) + vm.prank(buyer); + erc20.approve(marketplace, totalPrice); + + // Make offer. + IOffers.OfferParams memory offerParams = IOffers.OfferParams( + assetContract, + tokenId, + quantity, + currency, + totalPrice, + expirationTimestamp + ); + + vm.prank(buyer); + uint256 offerId = OffersLogic(marketplace).makeOffer(offerParams); + + // accept offer, without approving NFT to marketplace + vm.startPrank(seller); + vm.expectRevert("Marketplace: not owner or approved tokens."); + OffersLogic(marketplace).acceptOffer(offerId); + vm.stopPrank(); + } + + function test_revert_acceptOffer_offerorBalanceLessThanPrice() public { + // set owner of NFT + erc721.mint(seller, 1); + + // Sample offer parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 totalPrice = 1 ether; + uint256 expirationTimestamp = 200; + + // mint total-price to buyer + erc20.mint(buyer, totalPrice); + + // Approve Marketplace to transfer currency tokens. (but not owned) + vm.prank(buyer); + erc20.approve(marketplace, totalPrice); + + // Make offer. + IOffers.OfferParams memory offerParams = IOffers.OfferParams( + assetContract, + tokenId, + quantity, + currency, + totalPrice, + expirationTimestamp + ); + + vm.prank(buyer); + uint256 offerId = OffersLogic(marketplace).makeOffer(offerParams); + + // reduce erc20 balance of buyer + vm.prank(buyer); + erc20.burn(totalPrice); + + // accept offer + vm.startPrank(seller); + erc721.setApprovalForAll(marketplace, true); + vm.expectRevert("Marketplace: insufficient currency balance."); + OffersLogic(marketplace).acceptOffer(offerId); + vm.stopPrank(); + } + + function test_revert_acceptOffer_notApprovedMarketplaceToTransferPrice() public { + // set owner of NFT + erc721.mint(seller, 1); + + // Sample offer parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 totalPrice = 1 ether; + uint256 expirationTimestamp = 200; + + // mint total-price to buyer + erc20.mint(buyer, totalPrice); + + // Approve Marketplace to transfer currency tokens. (but not owned) + vm.prank(buyer); + erc20.approve(marketplace, totalPrice); + + // Make offer. + IOffers.OfferParams memory offerParams = IOffers.OfferParams( + assetContract, + tokenId, + quantity, + currency, + totalPrice, + expirationTimestamp + ); + + vm.prank(buyer); + uint256 offerId = OffersLogic(marketplace).makeOffer(offerParams); + + // remove erc20 approval + vm.prank(buyer); + erc20.approve(marketplace, 0); + + // accept offer + vm.startPrank(seller); + erc721.setApprovalForAll(marketplace, true); + vm.expectRevert("Marketplace: insufficient currency balance."); + OffersLogic(marketplace).acceptOffer(offerId); + vm.stopPrank(); + } + + /*/////////////////////////////////////////////////////////////// + View functions + //////////////////////////////////////////////////////////////*/ + + function test_state_getAllOffers() public { + uint256[] memory offerIds = new uint256[](5); + uint256[] memory tokenIds = new uint256[](5); + + // mint total-price to buyer + erc20.mint(buyer, 1000 ether); + + // Approve Marketplace to transfer currency tokens. (but not owned) + vm.prank(buyer); + erc20.approve(marketplace, 1000 ether); + + // Sample offer parameters. + address assetContract = address(erc721); + uint256 quantity = 1; + address currency = address(erc20); + uint256 totalPrice = 1 ether; + uint256 expirationTimestamp = 200; + + IOffers.OfferParams memory offerParams; + + for (uint256 i = 0; i < 5; i += 1) { + tokenIds[i] = i; + + // make offer + offerParams = IOffers.OfferParams( + assetContract, + tokenIds[i], + quantity, + currency, + totalPrice, + expirationTimestamp + ); + + vm.prank(buyer); + offerIds[i] = OffersLogic(marketplace).makeOffer(offerParams); + } + + IOffers.Offer[] memory allOffers = OffersLogic(marketplace).getAllOffers(0, 4); + assertEq(allOffers.length, 5); + + for (uint256 i = 0; i < 5; i += 1) { + assertEq(allOffers[i].offerId, offerIds[i]); + assertEq(allOffers[i].offeror, buyer); + assertEq(allOffers[i].assetContract, assetContract); + assertEq(allOffers[i].tokenId, tokenIds[i]); + assertEq(allOffers[i].quantity, quantity); + assertEq(allOffers[i].currency, currency); + assertEq(allOffers[i].totalPrice, totalPrice); + assertEq(allOffers[i].expirationTimestamp, expirationTimestamp); + assertEq(uint256(allOffers[i].tokenType), uint256(IOffers.TokenType.ERC721)); + } + } + + function test_state_getAllValidOffers() public { + uint256[] memory offerIds = new uint256[](5); + uint256[] memory tokenIds = new uint256[](5); + + // mint total-price to buyer + erc20.mint(buyer, 5 ether); + + // Approve Marketplace to transfer currency tokens. (but not owned) + vm.prank(buyer); + erc20.approve(marketplace, 5 ether); + + // Sample offer parameters. + address assetContract = address(erc721); + uint256 quantity = 1; + address currency = address(erc20); + uint256 expirationTimestamp = 200; + + IOffers.OfferParams memory offerParams; + + for (uint256 i = 0; i < 5; i += 1) { + tokenIds[i] = i; + + // make offer, with total-price as i + offerParams = IOffers.OfferParams( + assetContract, + tokenIds[i], + quantity, + currency, + (i + 1) * 1 ether, + expirationTimestamp + ); + + vm.prank(buyer); + offerIds[i] = OffersLogic(marketplace).makeOffer(offerParams); + } + + vm.prank(buyer); + erc20.burn(2 ether); // reduce balance to make some offers invalid + + IOffers.Offer[] memory allOffers = OffersLogic(marketplace).getAllValidOffers(0, 4); + assertEq(allOffers.length, 3); + + for (uint256 i = 0; i < 3; i += 1) { + assertEq(allOffers[i].offerId, offerIds[i]); + assertEq(allOffers[i].offeror, buyer); + assertEq(allOffers[i].assetContract, assetContract); + assertEq(allOffers[i].tokenId, tokenIds[i]); + assertEq(allOffers[i].quantity, quantity); + assertEq(allOffers[i].currency, currency); + assertEq(allOffers[i].totalPrice, (i + 1) * 1 ether); + assertEq(allOffers[i].expirationTimestamp, expirationTimestamp); + assertEq(uint256(allOffers[i].tokenType), uint256(IOffers.TokenType.ERC721)); + } + + // create an offer, and check the offers returned post its expiry + offerParams = IOffers.OfferParams(assetContract, 5, quantity, currency, 10, 10); + + vm.prank(buyer); + OffersLogic(marketplace).makeOffer(offerParams); + + vm.warp(10); + allOffers = OffersLogic(marketplace).getAllValidOffers(0, 5); + assertEq(allOffers.length, 3); + } +} diff --git a/src/test/marketplace/direct-listings/_payout/_payout.t.sol b/src/test/marketplace/direct-listings/_payout/_payout.t.sol new file mode 100644 index 000000000..04d895607 --- /dev/null +++ b/src/test/marketplace/direct-listings/_payout/_payout.t.sol @@ -0,0 +1,355 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../../utils/BaseTest.sol"; +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { RoyaltyPaymentsLogic } from "contracts/extension/plugin/RoyaltyPayments.sol"; +import { PlatformFee } from "contracts/extension/PlatformFee.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { MarketplaceV3 } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { DirectListingsLogic } from "contracts/prebuilts/marketplace/direct-listings/DirectListingsLogic.sol"; +import { IDirectListings } from "contracts/prebuilts/marketplace/IMarketplace.sol"; +import { MockRoyaltyEngineV1 } from "../../../mocks/MockRoyaltyEngineV1.sol"; + +contract PayoutTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + address private defaultFeeRecipient; + + // Default listing parameters + IDirectListings.ListingParameters internal listingParams; + uint256 internal listingId = 0; + + // Events to test + + /// @notice Emitted when a listing is updated. + event UpdatedListing( + address indexed listingCreator, + uint256 indexed listingId, + address indexed assetContract, + IDirectListings.Listing listing + ); + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), platformFeeRecipient, uint16(platformFeeBps)) + ) + ) + ); + + // Setup listing params + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100 minutes; + uint128 endTimestamp = 200 minutes; + bool reserved = false; + + listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + // Mint 1 ERC721 NFT to seller + erc721.mint(seller, listingParams.quantity); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `DirectListings` + address directListings = address(new DirectListingsLogic(address(weth))); + defaultFeeRecipient = 0x1Af20C6B23373350aD464700B5965CE4B0D2aD94; + vm.label(directListings, "DirectListings_Extension"); + + // Extension: DirectListingsLogic + Extension memory extension_directListings; + extension_directListings.metadata = ExtensionMetadata({ + name: "DirectListingsLogic", + metadataURI: "ipfs://DirectListings", + implementation: directListings + }); + + extension_directListings.functions = new ExtensionFunction[](13); + extension_directListings.functions[0] = ExtensionFunction( + DirectListingsLogic.totalListings.selector, + "totalListings()" + ); + extension_directListings.functions[1] = ExtensionFunction( + DirectListingsLogic.isBuyerApprovedForListing.selector, + "isBuyerApprovedForListing(uint256,address)" + ); + extension_directListings.functions[2] = ExtensionFunction( + DirectListingsLogic.isCurrencyApprovedForListing.selector, + "isCurrencyApprovedForListing(uint256,address)" + ); + extension_directListings.functions[3] = ExtensionFunction( + DirectListingsLogic.currencyPriceForListing.selector, + "currencyPriceForListing(uint256,address)" + ); + extension_directListings.functions[4] = ExtensionFunction( + DirectListingsLogic.createListing.selector, + "createListing((address,uint256,uint256,address,uint256,uint128,uint128,bool))" + ); + extension_directListings.functions[5] = ExtensionFunction( + DirectListingsLogic.updateListing.selector, + "updateListing(uint256,(address,uint256,uint256,address,uint256,uint128,uint128,bool))" + ); + extension_directListings.functions[6] = ExtensionFunction( + DirectListingsLogic.cancelListing.selector, + "cancelListing(uint256)" + ); + extension_directListings.functions[7] = ExtensionFunction( + DirectListingsLogic.approveBuyerForListing.selector, + "approveBuyerForListing(uint256,address,bool)" + ); + extension_directListings.functions[8] = ExtensionFunction( + DirectListingsLogic.approveCurrencyForListing.selector, + "approveCurrencyForListing(uint256,address,uint256)" + ); + extension_directListings.functions[9] = ExtensionFunction( + DirectListingsLogic.buyFromListing.selector, + "buyFromListing(uint256,address,uint256,address,uint256)" + ); + extension_directListings.functions[10] = ExtensionFunction( + DirectListingsLogic.getAllListings.selector, + "getAllListings(uint256,uint256)" + ); + extension_directListings.functions[11] = ExtensionFunction( + DirectListingsLogic.getAllValidListings.selector, + "getAllValidListings(uint256,uint256)" + ); + extension_directListings.functions[12] = ExtensionFunction( + DirectListingsLogic.getListing.selector, + "getListing(uint256)" + ); + + extensions[0] = extension_directListings; + } + + address payable[] internal mockRecipients; + uint256[] internal mockAmounts; + MockRoyaltyEngineV1 internal royaltyEngine; + + function _setupRoyaltyEngine() private { + mockRecipients.push(payable(address(0x12345))); + mockRecipients.push(payable(address(0x56789))); + + mockAmounts.push(10 ether); + mockAmounts.push(15 ether); + + royaltyEngine = new MockRoyaltyEngineV1(mockRecipients, mockAmounts); + } + + function _setupListingForRoyaltyTests(address erc721TokenAddress) private returns (uint256 _listingId) { + // Sample listing parameters. + address assetContract = erc721TokenAddress; + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 100 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = false; + + // Approve Marketplace to transfer token. + vm.prank(seller); + IERC721(erc721TokenAddress).setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParameters = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + _listingId = DirectListingsLogic(marketplace).createListing(listingParameters); + } + + function _buyFromListingForRoyaltyTests(uint256 _listingId) private returns (uint256 totalPrice) { + IDirectListings.Listing memory listing = DirectListingsLogic(marketplace).getListing(_listingId); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + totalPrice = pricePerToken * quantityToBuy; + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + DirectListingsLogic(marketplace).buyFromListing(_listingId, buyFor, quantityToBuy, currency, totalPrice); + } + + function test_payout_whenZeroRoyaltyRecipients() public { + // 1. ========= Create listing ========= + vm.startPrank(seller); + erc721.setApprovalForAll(marketplace, true); + listingId = DirectListingsLogic(marketplace).createListing(listingParams); + vm.stopPrank(); + + // 2. ========= Buy from listing ========= + + uint256 totalPrice = listingParams.pricePerToken; + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listingParams.startTimestamp); + vm.prank(buyer); + DirectListingsLogic(marketplace).buyFromListing( + listingId, + buyer, + listingParams.quantity, + listingParams.currency, + totalPrice + ); + + // 3. ======== Check balances after royalty payments ======== + + uint256 platformFees = (totalPrice * platformFeeBps) / 10_000; + + { + uint256 defaultFee = (totalPrice * 100) / 10_000; + // Platform fee recipient receives correct amount + assertBalERC20Eq(address(erc20), platformFeeRecipient, platformFees); + + // Seller gets total price minus royalty amounts + assertBalERC20Eq(address(erc20), seller, totalPrice - platformFees - defaultFee); + + assertBalERC20Eq(address(erc20), defaultFeeRecipient, defaultFee); + } + } + + modifier whenNonZeroRoyaltyRecipients() { + _setupRoyaltyEngine(); + + // Add RoyaltyEngine to marketplace + vm.prank(marketplaceDeployer); + RoyaltyPaymentsLogic(marketplace).setRoyaltyEngine(address(royaltyEngine)); + + _; + } + + function test_payout_whenInsufficientFundsToPayRoyaltyAfterPlatformFeePayout() public whenNonZeroRoyaltyRecipients { + vm.prank(marketplaceDeployer); + PlatformFee(marketplace).setPlatformFeeInfo(platformFeeRecipient, 9899); // 99.99% fees with 100 bps default + + // Mint the ERC721 tokens to seller. These tokens will be listed. + erc721.mint(seller, 1); + listingId = _setupListingForRoyaltyTests(address(erc721)); + + IDirectListings.Listing memory listing = DirectListingsLogic(marketplace).getListing(listingId); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("fees exceed the price"); + DirectListingsLogic(marketplace).buyFromListing(listingId, buyFor, quantityToBuy, currency, totalPrice); + } + + function test_payout_whenSufficientFundsToPayRoyaltyAfterPlatformFeePayout() public whenNonZeroRoyaltyRecipients { + assertEq(RoyaltyPaymentsLogic(marketplace).getRoyaltyEngineAddress(), address(royaltyEngine)); + + // 1. ========= Create listing ========= + + // Mint the ERC721 tokens to seller. These tokens will be listed. + erc721.mint(seller, 1); + listingId = _setupListingForRoyaltyTests(address(erc721)); + + // 2. ========= Buy from listing ========= + + uint256 totalPrice = _buyFromListingForRoyaltyTests(listingId); + + // 3. ======== Check balances after royalty payments ======== + + uint256 platformFees = (totalPrice * platformFeeBps) / 10_000; + + { + uint256 defaultFee = (totalPrice * 100) / 10_000; + // Royalty recipients receive correct amounts + assertBalERC20Eq(address(erc20), mockRecipients[0], mockAmounts[0]); + assertBalERC20Eq(address(erc20), mockRecipients[1], mockAmounts[1]); + + // Platform fee recipient receives correct amount + assertBalERC20Eq(address(erc20), platformFeeRecipient, platformFees); + + // Seller gets total price minus royalty amounts + assertBalERC20Eq( + address(erc20), + seller, + totalPrice - mockAmounts[0] - mockAmounts[1] - platformFees - defaultFee + ); + assertBalERC20Eq(address(erc20), defaultFeeRecipient, defaultFee); + } + } +} diff --git a/src/test/marketplace/direct-listings/_payout/_payout.tree b/src/test/marketplace/direct-listings/_payout/_payout.tree new file mode 100644 index 000000000..3d09e5d13 --- /dev/null +++ b/src/test/marketplace/direct-listings/_payout/_payout.tree @@ -0,0 +1,17 @@ +function _payout( + address _payer, + address _payee, + address _currencyToUse, + uint256 _totalPayoutAmount, + Listing memory _listing +) +├── when there are zero royalty recipients ✅ +│ ├── it should transfer platform fee from payer to platform fee recipient +│ └── it should transfer remainder of currency from payer to payee +└── when there are non-zero royalty recipients + ├── when the total royalty payout exceeds remainder payout after having paid platform fee + │ └── it should revert ✅ + └── when the total royalty payout does not exceed remainder payout after having paid platform fee ✅ + ├── it should transfer platform fee from payer to platform fee recipient + ├── it should transfer royalty fee from payer to royalty recipients + └── it should transfer remainder of currency from payer to payee \ No newline at end of file diff --git a/src/test/marketplace/direct-listings/_transferListingTokens/_transferListingTokens.t.sol b/src/test/marketplace/direct-listings/_transferListingTokens/_transferListingTokens.t.sol new file mode 100644 index 000000000..3673ef854 --- /dev/null +++ b/src/test/marketplace/direct-listings/_transferListingTokens/_transferListingTokens.t.sol @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../../utils/BaseTest.sol"; +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { MarketplaceV3 } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { DirectListingsLogic } from "contracts/prebuilts/marketplace/direct-listings/DirectListingsLogic.sol"; +import { IDirectListings } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +contract MockTransferListingTokens is DirectListingsLogic { + constructor(address _nativeTokenWrapper) DirectListingsLogic(_nativeTokenWrapper) {} + + function transferListingTokens( + address _from, + address _to, + uint256 _quantity, + IDirectListings.Listing memory _listing + ) external { + _transferListingTokens(_from, _to, _quantity, _listing); + } +} + +contract TransferListingTokensTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public recipient; + + // Default listing parameters + IDirectListings.ListingParameters internal listingParams; + uint256 listingId_erc721 = 0; + uint256 listingId_erc1155 = 1; + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + recipient = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + + vm.stopPrank(); + + // Setup listing params + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + // Mint 1 ERC721 NFT to seller + erc721.mint(seller, listingParams.quantity); + // Mint 100 ERC1155 NFT to seller + erc1155.mint(seller, listingParams.tokenId, 100); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + + // Create listings + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc721)); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc1155)); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + vm.stopPrank(); + + vm.startPrank(seller); + + erc721.setApprovalForAll(marketplace, true); + erc1155.setApprovalForAll(marketplace, true); + + listingId_erc721 = DirectListingsLogic(marketplace).createListing(listingParams); + + listingParams.assetContract = address(erc1155); + listingParams.quantity = 100; + listingId_erc1155 = DirectListingsLogic(marketplace).createListing(listingParams); + + vm.stopPrank(); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `MockTransferListingTokens` + address directListings = address(new MockTransferListingTokens(address(weth))); + vm.label(directListings, "DirectListings_Extension"); + + // Extension: DirectListingsLogic + Extension memory extension_directListings; + extension_directListings.metadata = ExtensionMetadata({ + name: "MockTransferListingTokens", + metadataURI: "ipfs://MockTransferListingTokens", + implementation: directListings + }); + + extension_directListings.functions = new ExtensionFunction[](3); + extension_directListings.functions[0] = ExtensionFunction( + MockTransferListingTokens.transferListingTokens.selector, + "transferListingTokens(address,address,uint256,(uint256,uint256,uint256,uint256,uint128,uint128,address,address,address,uint8,uint8,bool))" + ); + extension_directListings.functions[1] = ExtensionFunction( + DirectListingsLogic.createListing.selector, + "createListing((address,uint256,uint256,address,uint256,uint128,uint128,bool))" + ); + extension_directListings.functions[2] = ExtensionFunction( + DirectListingsLogic.getListing.selector, + "getListing(uint256)" + ); + extensions[0] = extension_directListings; + } + + function test_transferListingTokens_erc1155() public { + IDirectListings.Listing memory listing = DirectListingsLogic(marketplace).getListing(listingId_erc1155); + + assertEq(erc1155.balanceOf(seller, listing.tokenId), 100); + assertEq(erc1155.balanceOf(recipient, listing.tokenId), 0); + + MockTransferListingTokens(marketplace).transferListingTokens(seller, recipient, 100, listing); + + assertEq(erc1155.balanceOf(seller, listing.tokenId), 0); + assertEq(erc1155.balanceOf(recipient, listing.tokenId), 100); + } + + function test_transferListingTokens_erc721() public { + IDirectListings.Listing memory listing = DirectListingsLogic(marketplace).getListing(listingId_erc721); + + assertEq(erc721.ownerOf(listing.tokenId), seller); + + MockTransferListingTokens(marketplace).transferListingTokens(seller, recipient, 1, listing); + + assertEq(erc721.ownerOf(listing.tokenId), recipient); + } +} diff --git a/src/test/marketplace/direct-listings/_transferListingTokens/_transferListingTokens.tree b/src/test/marketplace/direct-listings/_transferListingTokens/_transferListingTokens.tree new file mode 100644 index 000000000..02204ec44 --- /dev/null +++ b/src/test/marketplace/direct-listings/_transferListingTokens/_transferListingTokens.tree @@ -0,0 +1,10 @@ +function _transferListingTokens( + address _from, + address _to, + uint256 _quantity, + Listing memory _listing +) +├── when the token is ERC1155 +│ └── it should transfer ERC1155 tokens from the specified owner to recipient +└── when the token is ERC721 + └── it should transfer ERC721 tokens from the specified owner to recipient \ No newline at end of file diff --git a/src/test/marketplace/direct-listings/_validateERC20BalAndAllowance/_validateERC20BalAndAllowance.t.sol b/src/test/marketplace/direct-listings/_validateERC20BalAndAllowance/_validateERC20BalAndAllowance.t.sol new file mode 100644 index 000000000..f57643ca0 --- /dev/null +++ b/src/test/marketplace/direct-listings/_validateERC20BalAndAllowance/_validateERC20BalAndAllowance.t.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../../utils/BaseTest.sol"; +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { MarketplaceV3 } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { DirectListingsLogic } from "contracts/prebuilts/marketplace/direct-listings/DirectListingsLogic.sol"; +import { IDirectListings } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +contract MockValidateERC20BalAndAllowance is DirectListingsLogic { + constructor(address _nativeTokenWrapper) DirectListingsLogic(_nativeTokenWrapper) {} + + function validateERC20BalAndAllowance( + address _tokenOwner, + address _currency, + uint256 _amount + ) external returns (bool) { + _validateERC20BalAndAllowance(_tokenOwner, _currency, _amount); + return true; + } +} + +contract ValidateERC20BalAndAllowanceTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + + // Default listing parameters + IDirectListings.ListingParameters internal listingParams; + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + + vm.stopPrank(); + + // Setup listing params + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + // Mint 1 ERC721 NFT to seller + erc721.mint(seller, listingParams.quantity); + // Mint 100 ERC1155 NFT to seller + erc1155.mint(seller, listingParams.tokenId, 100); + // Mint some ERC20 tokens to seller + erc20.mint(seller, 100 ether); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `MockValidateERC20BalAndAllowance` + address directListings = address(new MockValidateERC20BalAndAllowance(address(weth))); + vm.label(directListings, "DirectListings_Extension"); + + // Extension: DirectListingsLogic + Extension memory extension_directListings; + extension_directListings.metadata = ExtensionMetadata({ + name: "MockValidateERC20BalAndAllowance", + metadataURI: "ipfs://MockValidateERC20BalAndAllowance", + implementation: directListings + }); + + extension_directListings.functions = new ExtensionFunction[](1); + extension_directListings.functions[0] = ExtensionFunction( + MockValidateERC20BalAndAllowance.validateERC20BalAndAllowance.selector, + "validateERC20BalAndAllowance(address,address,uint256)" + ); + extensions[0] = extension_directListings; + } + + function test_validateERC20BalAndAllowance_whenInsufficientTokensOwned() public { + vm.startPrank(seller); + + erc20.approve(marketplace, 100 ether); + erc20.burn(1 ether); + + vm.stopPrank(); + + vm.expectRevert("!BAL20"); + MockValidateERC20BalAndAllowance(marketplace).validateERC20BalAndAllowance(seller, address(erc20), 100 ether); + } + + function test_validateERC20BalAndAllowance_whenTokensNotApprovedToTransfer() public { + vm.startPrank(seller); + erc20.approve(marketplace, 0); + vm.stopPrank(); + + vm.expectRevert("!BAL20"); + MockValidateERC20BalAndAllowance(marketplace).validateERC20BalAndAllowance(seller, address(erc20), 100 ether); + } + + function test_validateERC20BalAndAllowance_whenTokensOwnedAndApproved() public { + vm.prank(seller); + erc20.approve(marketplace, 100 ether); + + bool result = MockValidateERC20BalAndAllowance(marketplace).validateERC20BalAndAllowance( + seller, + address(erc20), + 100 ether + ); + assertEq(result, true); + } +} diff --git a/src/test/marketplace/direct-listings/_validateERC20BalAndAllowance/_validateERC20BalAndAllowance.tree b/src/test/marketplace/direct-listings/_validateERC20BalAndAllowance/_validateERC20BalAndAllowance.tree new file mode 100644 index 000000000..04b6010d4 --- /dev/null +++ b/src/test/marketplace/direct-listings/_validateERC20BalAndAllowance/_validateERC20BalAndAllowance.tree @@ -0,0 +1,11 @@ +function _validateERC20BalAndAllowance( + address _tokenOwner, + address _currency, + uint256 _amount +) +├── when the balance of token owner is less than expected _amount +│ └── it should revert ✅ +├── when marketplace is not approved to spend token owner's token +│ └── it should revert ✅ +└── when the balance of token owner is greater than or equal to expected _amount and marketplace is approved to spend token owner's token + └── it should return ✅ \ No newline at end of file diff --git a/src/test/marketplace/direct-listings/_validateNewListing/_validateNewListing.t.sol b/src/test/marketplace/direct-listings/_validateNewListing/_validateNewListing.t.sol new file mode 100644 index 000000000..51b681e34 --- /dev/null +++ b/src/test/marketplace/direct-listings/_validateNewListing/_validateNewListing.t.sol @@ -0,0 +1,292 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../../utils/BaseTest.sol"; +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { MarketplaceV3 } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { DirectListingsLogic } from "contracts/prebuilts/marketplace/direct-listings/DirectListingsLogic.sol"; +import { IDirectListings } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +contract MockValidateListing is DirectListingsLogic { + constructor(address _nativeTokenWrapper) DirectListingsLogic(_nativeTokenWrapper) {} + + function validateNewListing(ListingParameters memory _params, TokenType _tokenType) external returns (bool) { + _validateNewListing(_params, _tokenType); + return true; + } +} + +contract ValidateNewListingTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + + // Default listing parameters + IDirectListings.ListingParameters internal listingParams; + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + + vm.stopPrank(); + + // Setup listing params + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + // Mint 1 ERC721 NFT to seller + erc721.mint(seller, listingParams.quantity); + // Mint 100 ERC1155 NFT to seller + erc1155.mint(seller, listingParams.tokenId, 100); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `MockValidateListing` + address directListings = address(new MockValidateListing(address(weth))); + vm.label(directListings, "DirectListings_Extension"); + + // Extension: DirectListingsLogic + Extension memory extension_directListings; + extension_directListings.metadata = ExtensionMetadata({ + name: "MockValidateListing", + metadataURI: "ipfs://MockValidateListing", + implementation: directListings + }); + + extension_directListings.functions = new ExtensionFunction[](1); + extension_directListings.functions[0] = ExtensionFunction( + MockValidateListing.validateNewListing.selector, + "validateNewListing((address,uint256,uint256,address,uint256,uint128,uint128,bool),uint8)" + ); + extensions[0] = extension_directListings; + } + + function test_validateNewListing_whenQuantityIsZero() public { + listingParams.quantity = 0; + + vm.expectRevert("Marketplace: listing zero quantity."); + MockValidateListing(marketplace).validateNewListing(listingParams, IDirectListings.TokenType.ERC721); + } + + modifier whenQuantityIsOne() { + listingParams.quantity = 1; + _; + } + + modifier whenQuantityIsGtOne() { + listingParams.quantity = 2; + _; + } + + modifier whenTokenIsERC721() { + listingParams.assetContract = address(erc721); + _; + } + + modifier whenTokenIsERC1155() { + listingParams.assetContract = address(erc1155); + _; + } + + function test_validateNewListing_whenTokenIsERC721() public whenQuantityIsGtOne { + vm.expectRevert("Marketplace: listing invalid quantity."); + MockValidateListing(marketplace).validateNewListing(listingParams, IDirectListings.TokenType.ERC721); + } + + function test_validateNewListing_whenTokenOwnerDoesntOwnSufficientTokens_1() + public + whenQuantityIsGtOne + whenTokenIsERC1155 + { + vm.startPrank(seller); + erc1155.setApprovalForAll(marketplace, true); + erc1155.burn(seller, listingParams.tokenId, 100); + vm.stopPrank(); + + vm.prank(seller); + vm.expectRevert("Marketplace: not owner or approved tokens."); + MockValidateListing(marketplace).validateNewListing(listingParams, IDirectListings.TokenType.ERC1155); + } + + modifier whenTokenOwnerOwnsSufficientTokens() { + _; + } + + function test_validateNewListing_whenTokensNotApprovedForTransfer_1() + public + whenQuantityIsGtOne + whenTokenIsERC1155 + whenTokenOwnerOwnsSufficientTokens + { + vm.prank(seller); + erc1155.setApprovalForAll(marketplace, false); + + vm.prank(seller); + vm.expectRevert("Marketplace: not owner or approved tokens."); + MockValidateListing(marketplace).validateNewListing(listingParams, IDirectListings.TokenType.ERC1155); + } + + modifier whenTokensApprovedForTransfer(IDirectListings.TokenType tokenType) { + vm.prank(seller); + if (tokenType == IDirectListings.TokenType.ERC721) { + erc721.setApprovalForAll(marketplace, true); + } else { + erc1155.setApprovalForAll(marketplace, true); + } + _; + } + + function test_validateNewListing_whenTokensOwnedAndApproved_1() + public + whenQuantityIsGtOne + whenTokenIsERC1155 + whenTokenOwnerOwnsSufficientTokens + whenTokensApprovedForTransfer(IDirectListings.TokenType.ERC1155) + { + vm.prank(seller); + assertEq( + MockValidateListing(marketplace).validateNewListing(listingParams, IDirectListings.TokenType.ERC1155), + true + ); + } + + function test_validateNewListing_whenTokenOwnerDoesntOwnSufficientTokens_2a() + public + whenQuantityIsOne + whenTokenIsERC1155 + { + vm.startPrank(seller); + erc1155.setApprovalForAll(marketplace, true); + erc1155.burn(seller, listingParams.tokenId, 100); + vm.stopPrank(); + + vm.prank(seller); + vm.expectRevert("Marketplace: not owner or approved tokens."); + MockValidateListing(marketplace).validateNewListing(listingParams, IDirectListings.TokenType.ERC1155); + } + + function test_validateNewListing_whenTokenOwnerDoesntOwnSufficientTokens_2b() + public + whenQuantityIsOne + whenTokenIsERC721 + { + vm.startPrank(seller); + erc721.setApprovalForAll(marketplace, true); + erc721.burn(listingParams.tokenId); + vm.stopPrank(); + + vm.prank(seller); + vm.expectRevert("Marketplace: not owner or approved tokens."); + MockValidateListing(marketplace).validateNewListing(listingParams, IDirectListings.TokenType.ERC721); + } + + function test_validateNewListing_whenTokensNotApprovedForTransfer_2a() + public + whenQuantityIsOne + whenTokenIsERC721 + whenTokenOwnerOwnsSufficientTokens + { + vm.prank(seller); + erc721.setApprovalForAll(marketplace, false); + + vm.prank(seller); + vm.expectRevert("Marketplace: not owner or approved tokens."); + MockValidateListing(marketplace).validateNewListing(listingParams, IDirectListings.TokenType.ERC721); + } + + function test_validateNewListing_whenTokensNotApprovedForTransfer_2b() + public + whenQuantityIsOne + whenTokenIsERC1155 + whenTokenOwnerOwnsSufficientTokens + { + vm.prank(seller); + erc1155.setApprovalForAll(marketplace, false); + + vm.prank(seller); + vm.expectRevert("Marketplace: not owner or approved tokens."); + MockValidateListing(marketplace).validateNewListing(listingParams, IDirectListings.TokenType.ERC1155); + } + + function test_validateNewListing_whenTokensOwnedAndApproved_2a() + public + whenQuantityIsOne + whenTokenIsERC1155 + whenTokenOwnerOwnsSufficientTokens + whenTokensApprovedForTransfer(IDirectListings.TokenType.ERC1155) + { + vm.prank(seller); + assertEq( + MockValidateListing(marketplace).validateNewListing(listingParams, IDirectListings.TokenType.ERC1155), + true + ); + } + + function test_validateNewListing_whenTokensOwnedAndApproved_2b() + public + whenQuantityIsOne + whenTokenIsERC721 + whenTokenOwnerOwnsSufficientTokens + whenTokensApprovedForTransfer(IDirectListings.TokenType.ERC721) + { + vm.prank(seller); + assertEq( + MockValidateListing(marketplace).validateNewListing(listingParams, IDirectListings.TokenType.ERC721), + true + ); + } +} diff --git a/src/test/marketplace/direct-listings/_validateNewListing/_validateNewListing.tree b/src/test/marketplace/direct-listings/_validateNewListing/_validateNewListing.tree new file mode 100644 index 000000000..a2520cf27 --- /dev/null +++ b/src/test/marketplace/direct-listings/_validateNewListing/_validateNewListing.tree @@ -0,0 +1,23 @@ +function _validateNewListing(ListingParameters memory _params, TokenType _tokenType) +├── when quantity is zero +│ └── it should revert ✅ +└── when quantity is non zero + ├── when quantity is greater than one + │ ├── when token type is ERC721 + │ │ └── it should revert ✅ + │ └── when the token type is ERC1155 + │ ├── when the token owner owns less than quantity to list + │ │ └── it should revert ✅ + │ └── when the token owner owns sufficient quantity + │ ├── when the marketplace is not approved to transfer tokens + │ │ └── it should revert ✅ + │ └── when the marketplace is approved to transfer tokens + │ └── it should return ✅ + └── when the quantity is one + ├── when the token owner owns less than quantity to list + │ └── it should revert ✅ + └── when the token owner owns sufficient quantity + ├── when the marketplace is not approved to transfer tokens + │ └── it should revert ✅ + └── when the marketplace is approved to transfer tokens + └── it should return ✅ \ No newline at end of file diff --git a/src/test/marketplace/direct-listings/_validateOwnershipAndApproval/_validateOwnershipAndApproval.t.sol b/src/test/marketplace/direct-listings/_validateOwnershipAndApproval/_validateOwnershipAndApproval.t.sol new file mode 100644 index 000000000..5436d89f7 --- /dev/null +++ b/src/test/marketplace/direct-listings/_validateOwnershipAndApproval/_validateOwnershipAndApproval.t.sol @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../../utils/BaseTest.sol"; +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { MarketplaceV3 } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { DirectListingsLogic } from "contracts/prebuilts/marketplace/direct-listings/DirectListingsLogic.sol"; +import { IDirectListings } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +contract MockValidateOwnershipAndApproval is DirectListingsLogic { + constructor(address _nativeTokenWrapper) DirectListingsLogic(_nativeTokenWrapper) {} + + function validateOwnershipAndApproval( + address _tokenOwner, + address _assetContract, + uint256 _tokenId, + uint256 _quantity, + TokenType _tokenType + ) external view returns (bool) { + return _validateOwnershipAndApproval(_tokenOwner, _assetContract, _tokenId, _quantity, _tokenType); + } +} + +contract ValidateOwnershipAndApprovalTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + + // Default listing parameters + IDirectListings.ListingParameters internal listingParams; + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + + vm.stopPrank(); + + // Setup listing params + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + // Mint 1 ERC721 NFT to seller + erc721.mint(seller, listingParams.quantity); + // Mint 100 ERC1155 NFT to seller + erc1155.mint(seller, listingParams.tokenId, 100); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `MockValidateListing` + address directListings = address(new MockValidateOwnershipAndApproval(address(weth))); + vm.label(directListings, "DirectListings_Extension"); + + // Extension: DirectListingsLogic + Extension memory extension_directListings; + extension_directListings.metadata = ExtensionMetadata({ + name: "MockValidateOwnershipAndApproval", + metadataURI: "ipfs://MockValidateOwnershipAndApproval", + implementation: directListings + }); + + extension_directListings.functions = new ExtensionFunction[](1); + extension_directListings.functions[0] = ExtensionFunction( + MockValidateOwnershipAndApproval.validateOwnershipAndApproval.selector, + "validateOwnershipAndApproval(address,address,uint256,uint256,uint8)" + ); + extensions[0] = extension_directListings; + } + + modifier whenTokenIsERC1155() { + listingParams.assetContract = address(erc1155); + listingParams.quantity = 100; + _; + } + + modifier whenTokenIsERC721() { + listingParams.assetContract = address(erc721); + listingParams.quantity = 1; + _; + } + + function test_validateOwnershipAndApproval_whenInsufficientTokensOwned_erc1155() public whenTokenIsERC1155 { + vm.prank(seller); + erc1155.setApprovalForAll(marketplace, true); + + vm.prank(seller); + erc1155.burn(seller, listingParams.tokenId, 100); + + bool result = MockValidateOwnershipAndApproval(marketplace).validateOwnershipAndApproval( + seller, + listingParams.assetContract, + listingParams.tokenId, + listingParams.quantity, + IDirectListings.TokenType.ERC1155 + ); + assertEq(result, false); + } + + function test_validateOwnershipAndApproval_whenInsufficientTokensOwned_erc721() public whenTokenIsERC721 { + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + vm.prank(seller); + erc721.burn(listingParams.tokenId); + + bool result = MockValidateOwnershipAndApproval(marketplace).validateOwnershipAndApproval( + seller, + listingParams.assetContract, + listingParams.tokenId, + listingParams.quantity, + IDirectListings.TokenType.ERC721 + ); + assertEq(result, false); + } + + modifier whenSufficientTokensOwned() { + _; + } + + function test_validateOwnershipAndApproval_whenTokensNotApprovedToTransfer_erc1155() + public + whenTokenIsERC1155 + whenSufficientTokensOwned + { + assertEq(erc1155.balanceOf(seller, listingParams.tokenId), listingParams.quantity); + + vm.prank(seller); + erc1155.setApprovalForAll(marketplace, false); + + bool result = MockValidateOwnershipAndApproval(marketplace).validateOwnershipAndApproval( + seller, + listingParams.assetContract, + listingParams.tokenId, + listingParams.quantity, + IDirectListings.TokenType.ERC1155 + ); + assertEq(result, false); + } + + function test_validateOwnershipAndApproval_whenTokensNotApprovedToTransfer_erc721() + public + whenTokenIsERC721 + whenSufficientTokensOwned + { + assertEq(erc721.ownerOf(listingParams.tokenId), seller); + + vm.prank(seller); + erc721.setApprovalForAll(marketplace, false); + + bool result = MockValidateOwnershipAndApproval(marketplace).validateOwnershipAndApproval( + seller, + listingParams.assetContract, + listingParams.tokenId, + listingParams.quantity, + IDirectListings.TokenType.ERC721 + ); + assertEq(result, false); + } + + modifier whenTokensApprovedForTransfer(IDirectListings.TokenType tokenType) { + vm.prank(seller); + if (tokenType == IDirectListings.TokenType.ERC1155) { + erc1155.setApprovalForAll(marketplace, true); + } else { + erc721.setApprovalForAll(marketplace, true); + } + _; + } + + function test_validateOwnershipAndApproval_whenTokensOwnedAndApproved_erc1155() + public + whenTokenIsERC1155 + whenSufficientTokensOwned + whenTokensApprovedForTransfer(IDirectListings.TokenType.ERC1155) + { + bool result = MockValidateOwnershipAndApproval(marketplace).validateOwnershipAndApproval( + seller, + listingParams.assetContract, + listingParams.tokenId, + listingParams.quantity, + IDirectListings.TokenType.ERC1155 + ); + assertEq(result, true); + } + + function test_validateOwnershipAndApproval_whenTokensOwnedAndApproved_erc721() + public + whenTokenIsERC721 + whenSufficientTokensOwned + whenTokensApprovedForTransfer(IDirectListings.TokenType.ERC721) + { + bool result = MockValidateOwnershipAndApproval(marketplace).validateOwnershipAndApproval( + seller, + listingParams.assetContract, + listingParams.tokenId, + listingParams.quantity, + IDirectListings.TokenType.ERC721 + ); + assertEq(result, true); + } +} diff --git a/src/test/marketplace/direct-listings/_validateOwnershipAndApproval/_validateOwnershipAndApproval.tree b/src/test/marketplace/direct-listings/_validateOwnershipAndApproval/_validateOwnershipAndApproval.tree new file mode 100644 index 000000000..2ee82b493 --- /dev/null +++ b/src/test/marketplace/direct-listings/_validateOwnershipAndApproval/_validateOwnershipAndApproval.tree @@ -0,0 +1,21 @@ +function _validateOwnershipAndApproval( + address _tokenOwner, + address _assetContract, + uint256 _tokenId, + uint256 _quantity, + TokenType _tokenType +) +├── when token type is ERC1155 +│ ├── when token balance of owner is less than expected quantity +│ │ └── it should return false ✅ +│ ├── when marketplace is not approved to transfer tokens +│ │ └── it should return false ✅ +│ └── when token balance of owner is gte expected quantity and marketplace is approved to transfer tokens +│ └── it should return true ✅ +└── when token type is ERC721 + ├── when token owner is not the expected owner of the token + │ └── it should return false ✅ + ├── when marketplace is not approved to transfer tokens + │ └── it should return false ✅ + └── when token owner is the expected owner of the token and marketplace is approved to transfer tokens + └── it should return true ✅ diff --git a/src/test/marketplace/direct-listings/approveBuyerForListing/approveBuyerForListing.t.sol b/src/test/marketplace/direct-listings/approveBuyerForListing/approveBuyerForListing.t.sol new file mode 100644 index 000000000..bdd8dbae8 --- /dev/null +++ b/src/test/marketplace/direct-listings/approveBuyerForListing/approveBuyerForListing.t.sol @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../../utils/BaseTest.sol"; +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { MarketplaceV3 } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { DirectListingsLogic } from "contracts/prebuilts/marketplace/direct-listings/DirectListingsLogic.sol"; +import { IDirectListings } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +contract ApproveBuyerForListingTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + // Default listing parameters + IDirectListings.ListingParameters internal listingParams; + uint256 internal listingId = 0; + + // Events to test + + /// @notice Emitted when a buyer is approved to buy from a reserved listing. + event BuyerApprovedForListing(uint256 indexed listingId, address indexed buyer, bool approved); + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + + vm.stopPrank(); + + // Setup listing params + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100 minutes; + uint128 endTimestamp = 200 minutes; + bool reserved = false; + + listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + // Mint 1 ERC721 NFT to seller + erc721.mint(seller, listingParams.quantity); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `DirectListings` + address directListings = address(new DirectListingsLogic(address(weth))); + vm.label(directListings, "DirectListings_Extension"); + + // Extension: DirectListingsLogic + Extension memory extension_directListings; + extension_directListings.metadata = ExtensionMetadata({ + name: "DirectListingsLogic", + metadataURI: "ipfs://DirectListings", + implementation: directListings + }); + + extension_directListings.functions = new ExtensionFunction[](13); + extension_directListings.functions[0] = ExtensionFunction( + DirectListingsLogic.totalListings.selector, + "totalListings()" + ); + extension_directListings.functions[1] = ExtensionFunction( + DirectListingsLogic.isBuyerApprovedForListing.selector, + "isBuyerApprovedForListing(uint256,address)" + ); + extension_directListings.functions[2] = ExtensionFunction( + DirectListingsLogic.isCurrencyApprovedForListing.selector, + "isCurrencyApprovedForListing(uint256,address)" + ); + extension_directListings.functions[3] = ExtensionFunction( + DirectListingsLogic.currencyPriceForListing.selector, + "currencyPriceForListing(uint256,address)" + ); + extension_directListings.functions[4] = ExtensionFunction( + DirectListingsLogic.createListing.selector, + "createListing((address,uint256,uint256,address,uint256,uint128,uint128,bool))" + ); + extension_directListings.functions[5] = ExtensionFunction( + DirectListingsLogic.updateListing.selector, + "updateListing(uint256,(address,uint256,uint256,address,uint256,uint128,uint128,bool))" + ); + extension_directListings.functions[6] = ExtensionFunction( + DirectListingsLogic.cancelListing.selector, + "cancelListing(uint256)" + ); + extension_directListings.functions[7] = ExtensionFunction( + DirectListingsLogic.approveBuyerForListing.selector, + "approveBuyerForListing(uint256,address,bool)" + ); + extension_directListings.functions[8] = ExtensionFunction( + DirectListingsLogic.approveCurrencyForListing.selector, + "approveCurrencyForListing(uint256,address,uint256)" + ); + extension_directListings.functions[9] = ExtensionFunction( + DirectListingsLogic.buyFromListing.selector, + "buyFromListing(uint256,address,uint256,address,uint256)" + ); + extension_directListings.functions[10] = ExtensionFunction( + DirectListingsLogic.getAllListings.selector, + "getAllListings(uint256,uint256)" + ); + extension_directListings.functions[11] = ExtensionFunction( + DirectListingsLogic.getAllValidListings.selector, + "getAllValidListings(uint256,uint256)" + ); + extension_directListings.functions[12] = ExtensionFunction( + DirectListingsLogic.getListing.selector, + "getListing(uint256)" + ); + + extensions[0] = extension_directListings; + } + + function test_approveBuyerForListing_listingDoesntExist() public { + vm.prank(seller); + vm.expectRevert("Marketplace: invalid listing."); + DirectListingsLogic(marketplace).approveBuyerForListing(listingId, buyer, true); + } + + modifier whenListingExists() { + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc721)); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + vm.stopPrank(); + + vm.startPrank(seller); + erc721.setApprovalForAll(marketplace, true); + listingId = DirectListingsLogic(marketplace).createListing(listingParams); + vm.stopPrank(); + _; + } + + function test_approveBuyerForListing_whenCallerNotListingCreator() public whenListingExists { + vm.prank(address(0x4353)); + vm.expectRevert("Marketplace: not listing creator."); + DirectListingsLogic(marketplace).approveBuyerForListing(listingId, buyer, true); + } + + modifier whenCallerIsListingCreator() { + _; + } + + function test_approveBuyerForListing_whenListingNotReserved() public whenListingExists whenCallerIsListingCreator { + vm.prank(seller); + vm.expectRevert("Marketplace: listing not reserved."); + DirectListingsLogic(marketplace).approveBuyerForListing(listingId, buyer, true); + } + + modifier whenListingIsReserved() { + listingParams.reserved = true; + + vm.prank(seller); + DirectListingsLogic(marketplace).updateListing(listingId, listingParams); + _; + } + + function test_approveBuyerForListing_whenListingIsReserved() + public + whenListingExists + whenCallerIsListingCreator + whenListingIsReserved + { + assertEq(DirectListingsLogic(marketplace).isBuyerApprovedForListing(listingId, buyer), false); + + vm.prank(seller); + vm.expectEmit(true, true, true, false); + emit BuyerApprovedForListing(listingId, buyer, true); + DirectListingsLogic(marketplace).approveBuyerForListing(listingId, buyer, true); + + assertEq(DirectListingsLogic(marketplace).isBuyerApprovedForListing(listingId, buyer), true); + } +} diff --git a/src/test/marketplace/direct-listings/approveBuyerForListing/approveBuyerForListing.tree b/src/test/marketplace/direct-listings/approveBuyerForListing/approveBuyerForListing.tree new file mode 100644 index 000000000..5b7aeff2a --- /dev/null +++ b/src/test/marketplace/direct-listings/approveBuyerForListing/approveBuyerForListing.tree @@ -0,0 +1,15 @@ +function approveBuyerForListing( + uint256 _listingId, + address _buyer, + bool _toApprove +) +├── when the lisitng does not exist +│ └── it should revert ✅ +└── when the listing exists + ├── when the caller is not listing creator + │ └── it should revert ✅ + └── when the caller is listing creator + ├── when the listing is not reserved + │ └── it should revert ✅ + └── when the listing is reserved + └── it should set the intended approval status for buyer ✅ \ No newline at end of file diff --git a/src/test/marketplace/direct-listings/approveCurrencyForListing/approveCurrencyForListing.t.sol b/src/test/marketplace/direct-listings/approveCurrencyForListing/approveCurrencyForListing.t.sol new file mode 100644 index 000000000..e4e70007d --- /dev/null +++ b/src/test/marketplace/direct-listings/approveCurrencyForListing/approveCurrencyForListing.t.sol @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../../utils/BaseTest.sol"; +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { MarketplaceV3 } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { DirectListingsLogic } from "contracts/prebuilts/marketplace/direct-listings/DirectListingsLogic.sol"; +import { IDirectListings } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +contract ApproveCurrencyForListingTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + + // Default listing parameters + IDirectListings.ListingParameters internal listingParams; + uint256 internal listingId = 0; + + // Events to test + + /// @notice Emitted when a currency is approved as a form of payment for the listing. + event CurrencyApprovedForListing(uint256 indexed listingId, address indexed currency, uint256 pricePerToken); + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + + vm.stopPrank(); + + // Setup listing params + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100 minutes; + uint128 endTimestamp = 200 minutes; + bool reserved = true; + + listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + // Mint 1 ERC721 NFT to seller + erc721.mint(seller, listingParams.quantity); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `DirectListings` + address directListings = address(new DirectListingsLogic(address(weth))); + vm.label(directListings, "DirectListings_Extension"); + + // Extension: DirectListingsLogic + Extension memory extension_directListings; + extension_directListings.metadata = ExtensionMetadata({ + name: "DirectListingsLogic", + metadataURI: "ipfs://DirectListings", + implementation: directListings + }); + + extension_directListings.functions = new ExtensionFunction[](13); + extension_directListings.functions[0] = ExtensionFunction( + DirectListingsLogic.totalListings.selector, + "totalListings()" + ); + extension_directListings.functions[1] = ExtensionFunction( + DirectListingsLogic.isBuyerApprovedForListing.selector, + "isBuyerApprovedForListing(uint256,address)" + ); + extension_directListings.functions[2] = ExtensionFunction( + DirectListingsLogic.isCurrencyApprovedForListing.selector, + "isCurrencyApprovedForListing(uint256,address)" + ); + extension_directListings.functions[3] = ExtensionFunction( + DirectListingsLogic.currencyPriceForListing.selector, + "currencyPriceForListing(uint256,address)" + ); + extension_directListings.functions[4] = ExtensionFunction( + DirectListingsLogic.createListing.selector, + "createListing((address,uint256,uint256,address,uint256,uint128,uint128,bool))" + ); + extension_directListings.functions[5] = ExtensionFunction( + DirectListingsLogic.updateListing.selector, + "updateListing(uint256,(address,uint256,uint256,address,uint256,uint128,uint128,bool))" + ); + extension_directListings.functions[6] = ExtensionFunction( + DirectListingsLogic.cancelListing.selector, + "cancelListing(uint256)" + ); + extension_directListings.functions[7] = ExtensionFunction( + DirectListingsLogic.approveBuyerForListing.selector, + "approveBuyerForListing(uint256,address,bool)" + ); + extension_directListings.functions[8] = ExtensionFunction( + DirectListingsLogic.approveCurrencyForListing.selector, + "approveCurrencyForListing(uint256,address,uint256)" + ); + extension_directListings.functions[9] = ExtensionFunction( + DirectListingsLogic.buyFromListing.selector, + "buyFromListing(uint256,address,uint256,address,uint256)" + ); + extension_directListings.functions[10] = ExtensionFunction( + DirectListingsLogic.getAllListings.selector, + "getAllListings(uint256,uint256)" + ); + extension_directListings.functions[11] = ExtensionFunction( + DirectListingsLogic.getAllValidListings.selector, + "getAllValidListings(uint256,uint256)" + ); + extension_directListings.functions[12] = ExtensionFunction( + DirectListingsLogic.getListing.selector, + "getListing(uint256)" + ); + + extensions[0] = extension_directListings; + } + + function test_approveCurrencyForListing_listingDoesntExist() public { + vm.prank(seller); + vm.expectRevert("Marketplace: invalid listing."); + DirectListingsLogic(marketplace).approveCurrencyForListing(listingId, address(weth), 1 ether); + } + + modifier whenListingExists() { + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc721)); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + vm.stopPrank(); + + vm.startPrank(seller); + erc721.setApprovalForAll(marketplace, true); + listingId = DirectListingsLogic(marketplace).createListing(listingParams); + erc721.setApprovalForAll(marketplace, false); + vm.stopPrank(); + + vm.prank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(erc721)); + _; + } + + function test_approveCurrencyForListing_whenCallerNotListingCreator() public whenListingExists { + vm.prank(address(0x4353)); + vm.expectRevert("Marketplace: not listing creator."); + DirectListingsLogic(marketplace).approveCurrencyForListing(listingId, address(weth), 1 ether); + } + + modifier whenCallerIsListingCreator() { + _; + } + + function test_approveCurrencyForListing_whenApprovingDifferentPriceForListedCurrency() + public + whenListingExists + whenCallerIsListingCreator + { + vm.prank(seller); + vm.expectRevert("Marketplace: approving listing currency with different price."); + DirectListingsLogic(marketplace).approveCurrencyForListing( + listingId, + listingParams.currency, + listingParams.pricePerToken + 1 + ); + } + + function test_approveCurrencyForListing_whenPriceToApproveIsAlreadyApproved() + public + whenListingExists + whenCallerIsListingCreator + { + vm.prank(seller); + DirectListingsLogic(marketplace).approveCurrencyForListing(listingId, address(weth), 1 ether); + + vm.prank(seller); + vm.expectRevert("Marketplace: price unchanged."); + DirectListingsLogic(marketplace).approveCurrencyForListing(listingId, address(weth), 1 ether); + } + + function test_approveCurrencyForListing_whenApprovedPriceForCurrencyIsDifferentThanIncumbent() + public + whenListingExists + whenCallerIsListingCreator + { + vm.expectRevert("Currency not approved for listing"); + DirectListingsLogic(marketplace).currencyPriceForListing(listingId, address(weth)); + + vm.prank(seller); + vm.expectEmit(true, true, true, true); + emit CurrencyApprovedForListing(listingId, address(weth), 1 ether); + DirectListingsLogic(marketplace).approveCurrencyForListing(listingId, address(weth), 1 ether); + + assertEq(DirectListingsLogic(marketplace).currencyPriceForListing(listingId, address(weth)), 1 ether); + } +} diff --git a/src/test/marketplace/direct-listings/approveCurrencyForListing/approveCurrencyForListing.tree b/src/test/marketplace/direct-listings/approveCurrencyForListing/approveCurrencyForListing.tree new file mode 100644 index 000000000..1c7912edc --- /dev/null +++ b/src/test/marketplace/direct-listings/approveCurrencyForListing/approveCurrencyForListing.tree @@ -0,0 +1,19 @@ +function approveCurrencyForListing( + uint256 _listingId, + address _currency, + uint256 _pricePerTokenInCurrency +) +├── when listing does not exist +│ └── it should revert ✅ +└── when the listing exists + ├── when the caller is not listing creator + │ └── it should revert ✅ + └── when the caller is listing creator + ├── when approving different price for listed currency + │ └── it should revert ✅ + └── when not approving different price for listed currency + ├── when prive to approve for currency is already approved + │ └── it should revert ✅ + └── when approving a new price for currency ✅ + ├── it should update the approved price for currency + └── it should emit CurrencyApprovedForListing event with the listing ID, currency and approved price \ No newline at end of file diff --git a/src/test/marketplace/direct-listings/buyFromListing/buyFromListing.t.sol b/src/test/marketplace/direct-listings/buyFromListing/buyFromListing.t.sol new file mode 100644 index 000000000..8c11e6540 --- /dev/null +++ b/src/test/marketplace/direct-listings/buyFromListing/buyFromListing.t.sol @@ -0,0 +1,636 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../../utils/BaseTest.sol"; +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { RoyaltyPaymentsLogic } from "contracts/extension/plugin/RoyaltyPayments.sol"; +import { PlatformFee } from "contracts/extension/PlatformFee.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { MarketplaceV3 } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { DirectListingsLogic } from "contracts/prebuilts/marketplace/direct-listings/DirectListingsLogic.sol"; +import { IDirectListings } from "contracts/prebuilts/marketplace/IMarketplace.sol"; +import { MockRoyaltyEngineV1 } from "../../../mocks/MockRoyaltyEngineV1.sol"; +import { ERC1155Holder } from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; + +contract ReentrantRecipient is ERC1155Holder { + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes memory data + ) public virtual override returns (bytes4) { + DirectListingsLogic(msg.sender).buyFromListing(0, address(this), 1, address(0), 0); + return super.onERC1155Received(operator, from, id, value, data); + } +} + +contract BuyFromListingTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + // Default listing parameters + IDirectListings.ListingParameters internal listingParams; + + uint256 internal listingId = type(uint256).max; + uint256 internal listingId_native_noSpecialPrice = 0; + uint256 internal listingId_native_specialPrice = 1; + uint256 internal listingId_erc20_noSpecialPrice = 2; + uint256 internal listingId_erc20_specialPrice = 3; + + // Events to test + + /// @notice Emitted when NFTs are bought from a listing. + event NewSale( + address indexed listingCreator, + uint256 indexed listingId, + address indexed assetContract, + uint256 tokenId, + address buyer, + uint256 quantityBought, + uint256 totalPricePaid + ); + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), platformFeeRecipient, uint16(platformFeeBps)) + ) + ) + ); + + // Setup listing params + address assetContract = address(erc1155); + uint256 tokenId = 0; + uint256 quantity = 10; + address currency = NATIVE_TOKEN; + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100 minutes; + uint128 endTimestamp = 200 minutes; + bool reserved = false; + + listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + // Mint currency to buyer + vm.deal(buyer, 100 ether); + erc20.mint(buyer, 100 ether); + + // Mint an ERC721 NFTs to seller + erc1155.mint(seller, 0, 100); + vm.prank(seller); + erc1155.setApprovalForAll(marketplace, true); + + // Create 4 listings + vm.startPrank(seller); + + // 1. Native token, no special price + listingParams.currency = NATIVE_TOKEN; + listingId_native_noSpecialPrice = DirectListingsLogic(marketplace).createListing(listingParams); + + // 2. Native token, special price + listingParams.currency = address(erc20); + listingId_native_specialPrice = DirectListingsLogic(marketplace).createListing(listingParams); + DirectListingsLogic(marketplace).approveCurrencyForListing( + listingId_native_specialPrice, + NATIVE_TOKEN, + 2 ether + ); + + // 3. ERC20 token, no special price + listingParams.currency = address(erc20); + listingId_erc20_noSpecialPrice = DirectListingsLogic(marketplace).createListing(listingParams); + + // 4. ERC20 token, special price + listingParams.currency = NATIVE_TOKEN; + listingId_erc20_specialPrice = DirectListingsLogic(marketplace).createListing(listingParams); + DirectListingsLogic(marketplace).approveCurrencyForListing( + listingId_erc20_specialPrice, + address(erc20), + 2 ether + ); + + vm.stopPrank(); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `DirectListings` + address directListings = address(new DirectListingsLogic(address(weth))); + vm.label(directListings, "DirectListings_Extension"); + + // Extension: DirectListingsLogic + Extension memory extension_directListings; + extension_directListings.metadata = ExtensionMetadata({ + name: "DirectListingsLogic", + metadataURI: "ipfs://DirectListings", + implementation: directListings + }); + + extension_directListings.functions = new ExtensionFunction[](13); + extension_directListings.functions[0] = ExtensionFunction( + DirectListingsLogic.totalListings.selector, + "totalListings()" + ); + extension_directListings.functions[1] = ExtensionFunction( + DirectListingsLogic.isBuyerApprovedForListing.selector, + "isBuyerApprovedForListing(uint256,address)" + ); + extension_directListings.functions[2] = ExtensionFunction( + DirectListingsLogic.isCurrencyApprovedForListing.selector, + "isCurrencyApprovedForListing(uint256,address)" + ); + extension_directListings.functions[3] = ExtensionFunction( + DirectListingsLogic.currencyPriceForListing.selector, + "currencyPriceForListing(uint256,address)" + ); + extension_directListings.functions[4] = ExtensionFunction( + DirectListingsLogic.createListing.selector, + "createListing((address,uint256,uint256,address,uint256,uint128,uint128,bool))" + ); + extension_directListings.functions[5] = ExtensionFunction( + DirectListingsLogic.updateListing.selector, + "updateListing(uint256,(address,uint256,uint256,address,uint256,uint128,uint128,bool))" + ); + extension_directListings.functions[6] = ExtensionFunction( + DirectListingsLogic.cancelListing.selector, + "cancelListing(uint256)" + ); + extension_directListings.functions[7] = ExtensionFunction( + DirectListingsLogic.approveBuyerForListing.selector, + "approveBuyerForListing(uint256,address,bool)" + ); + extension_directListings.functions[8] = ExtensionFunction( + DirectListingsLogic.approveCurrencyForListing.selector, + "approveCurrencyForListing(uint256,address,uint256)" + ); + extension_directListings.functions[9] = ExtensionFunction( + DirectListingsLogic.buyFromListing.selector, + "buyFromListing(uint256,address,uint256,address,uint256)" + ); + extension_directListings.functions[10] = ExtensionFunction( + DirectListingsLogic.getAllListings.selector, + "getAllListings(uint256,uint256)" + ); + extension_directListings.functions[11] = ExtensionFunction( + DirectListingsLogic.getAllValidListings.selector, + "getAllValidListings(uint256,uint256)" + ); + extension_directListings.functions[12] = ExtensionFunction( + DirectListingsLogic.getListing.selector, + "getListing(uint256)" + ); + + extensions[0] = extension_directListings; + } + + modifier whenListingCurrencyIsNativeToken() { + listingId = listingId_native_noSpecialPrice; + listingParams.currency = NATIVE_TOKEN; + _; + } + + modifier whenListingHasSpecialPriceNativeToken() { + listingId = listingId_native_specialPrice; + _; + } + + modifier whenListingCurrencyIsERC20Token() { + listingId = listingId_erc20_noSpecialPrice; + _; + } + + modifier whenListingHasSpecialPriceERC20Token() { + listingId = listingId_erc20_specialPrice; + _; + } + + //////////// ASSUME NATIVE_TOKEN && SPECIAL_PRICE //////////// + + function test_buyFromListing_whenCallIsReentrant() public whenListingHasSpecialPriceNativeToken { + vm.warp(listingParams.startTimestamp); + address reentrantRecipient = address(new ReentrantRecipient()); + + vm.prank(buyer); + vm.expectRevert(); + DirectListingsLogic(marketplace).buyFromListing{ value: 2 ether }( + listingId, + reentrantRecipient, + 1, + NATIVE_TOKEN, + 2 ether + ); + } + + modifier whenCallIsNotReentrant() { + _; + } + + function test_buyFromListing_whenListingDoesNotExist() + public + whenListingHasSpecialPriceNativeToken + whenCallIsNotReentrant + { + vm.warp(listingParams.startTimestamp); + vm.prank(buyer); + vm.expectRevert("Marketplace: invalid listing."); + DirectListingsLogic(marketplace).buyFromListing{ value: 2 ether }(100, buyer, 1, NATIVE_TOKEN, 2 ether); + } + + modifier whenListingExists() { + _; + } + + function test_buyFromListing_whenBuyerIsNotApprovedForListing() + public + whenListingHasSpecialPriceNativeToken + whenCallIsNotReentrant + whenListingExists + { + listingParams.reserved = true; + listingParams.currency = address(erc20); + + vm.prank(seller); + DirectListingsLogic(marketplace).updateListing(listingId, listingParams); + + vm.warp(listingParams.startTimestamp); + + vm.prank(buyer); + vm.expectRevert("buyer not approved"); + DirectListingsLogic(marketplace).buyFromListing{ value: 2 ether }(listingId, buyer, 1, NATIVE_TOKEN, 2 ether); + } + + modifier whenBuyerIsApprovedForListing(address _currency) { + listingParams.reserved = true; + listingParams.currency = _currency; + + vm.prank(seller); + DirectListingsLogic(marketplace).updateListing(listingId, listingParams); + + vm.prank(seller); + DirectListingsLogic(marketplace).approveBuyerForListing(listingId, buyer, true); + _; + } + + function test_buyFromListing_whenQuantityToBuyIsInvalid() + public + whenListingHasSpecialPriceNativeToken + whenCallIsNotReentrant + whenListingExists + whenBuyerIsApprovedForListing(address(erc20)) + { + vm.warp(listingParams.startTimestamp); + + vm.prank(buyer); + vm.expectRevert("Buying invalid quantity"); + DirectListingsLogic(marketplace).buyFromListing{ value: 2 ether }(listingId, buyer, 0, NATIVE_TOKEN, 2 ether); + } + + modifier whenQuantityToBuyIsValid() { + _; + } + + function test_buyFromListing_whenListingIsInactive() + public + whenListingHasSpecialPriceNativeToken + whenCallIsNotReentrant + whenListingExists + whenBuyerIsApprovedForListing(address(erc20)) + whenQuantityToBuyIsValid + { + vm.prank(buyer); + vm.expectRevert("not within sale window."); + DirectListingsLogic(marketplace).buyFromListing{ value: 2 ether }(listingId, buyer, 1, NATIVE_TOKEN, 2 ether); + } + + modifier whenListingIsActive() { + _; + } + + function test_buyFromListing_whenListedAssetNotOwnedOrApprovedToTransfer() + public + whenListingHasSpecialPriceNativeToken + whenCallIsNotReentrant + whenListingExists + whenBuyerIsApprovedForListing(address(erc20)) + whenQuantityToBuyIsValid + whenListingIsActive + { + vm.prank(seller); + erc1155.setApprovalForAll(marketplace, false); + + vm.warp(listingParams.startTimestamp); + + vm.prank(buyer); + vm.expectRevert("Marketplace: not owner or approved tokens."); + DirectListingsLogic(marketplace).buyFromListing{ value: 2 ether }(listingId, buyer, 1, NATIVE_TOKEN, 2 ether); + } + + modifier whenListedAssetOwnedAndApproved() { + _; + } + + function test_buyFromListing_whenExpectedPriceNotActualPrice() + public + whenListingHasSpecialPriceNativeToken + whenCallIsNotReentrant + whenListingExists + whenBuyerIsApprovedForListing(address(erc20)) + whenQuantityToBuyIsValid + whenListingIsActive + whenListedAssetOwnedAndApproved + { + vm.warp(listingParams.startTimestamp); + + vm.prank(buyer); + vm.expectRevert("Unexpected total price"); + DirectListingsLogic(marketplace).buyFromListing{ value: 2 ether }(listingId, buyer, 1, NATIVE_TOKEN, 1 ether); + } + + modifier whenExpectedPriceIsActualPrice() { + _; + } + + function test_buyFromListing_whenMsgValueNotEqTotalPrice() + public + whenListingHasSpecialPriceNativeToken + whenCallIsNotReentrant + whenListingExists + whenBuyerIsApprovedForListing(address(erc20)) + whenQuantityToBuyIsValid + whenListingIsActive + whenListedAssetOwnedAndApproved + whenExpectedPriceIsActualPrice + { + vm.warp(listingParams.startTimestamp); + + vm.prank(buyer); + vm.expectRevert("Marketplace: msg.value must exactly be the total price."); + DirectListingsLogic(marketplace).buyFromListing{ value: 1 ether }(listingId, buyer, 1, NATIVE_TOKEN, 2 ether); + } + + modifier whenMsgValueEqTotalPrice() { + _; + } + + function test_buyFromListing_whenAllRemainingQtyIsBought_nativeToken() + public + whenListingHasSpecialPriceNativeToken + whenCallIsNotReentrant + whenListingExists + whenBuyerIsApprovedForListing(address(erc20)) + whenQuantityToBuyIsValid + whenListingIsActive + whenListedAssetOwnedAndApproved + whenExpectedPriceIsActualPrice + whenMsgValueEqTotalPrice + { + assertEq(erc1155.balanceOf(seller, listingParams.tokenId), 100); + assertEq(erc1155.balanceOf(buyer, listingParams.tokenId), 0); + assertEq( + uint8(DirectListingsLogic(marketplace).getListing(listingId).status), + uint8(IDirectListings.Status.CREATED) + ); + + vm.warp(listingParams.startTimestamp); + vm.prank(buyer); + DirectListingsLogic(marketplace).buyFromListing{ value: 2 ether * listingParams.quantity }( + listingId, + buyer, + listingParams.quantity, + NATIVE_TOKEN, + 2 ether * listingParams.quantity + ); + + assertEq(erc1155.balanceOf(seller, listingParams.tokenId), 100 - listingParams.quantity); + assertEq(erc1155.balanceOf(buyer, listingParams.tokenId), listingParams.quantity); + assertEq( + uint8(DirectListingsLogic(marketplace).getListing(listingId).status), + uint8(IDirectListings.Status.COMPLETED) + ); + } + + function test_buyFromListing_whenSomeRemainingQtyIsBought_nativeToken() + public + whenListingHasSpecialPriceNativeToken + whenCallIsNotReentrant + whenListingExists + whenBuyerIsApprovedForListing(address(erc20)) + whenQuantityToBuyIsValid + whenListingIsActive + whenListedAssetOwnedAndApproved + whenExpectedPriceIsActualPrice + whenMsgValueEqTotalPrice + { + assertEq(erc1155.balanceOf(seller, listingParams.tokenId), 100); + assertEq(erc1155.balanceOf(buyer, listingParams.tokenId), 0); + assertEq( + uint8(DirectListingsLogic(marketplace).getListing(listingId).status), + uint8(IDirectListings.Status.CREATED) + ); + + vm.warp(listingParams.startTimestamp); + vm.prank(buyer); + DirectListingsLogic(marketplace).buyFromListing{ value: 2 ether }(listingId, buyer, 1, NATIVE_TOKEN, 2 ether); + + assertEq(erc1155.balanceOf(seller, listingParams.tokenId), 99); + assertEq(erc1155.balanceOf(buyer, listingParams.tokenId), 1); + assertEq( + uint8(DirectListingsLogic(marketplace).getListing(listingId).status), + uint8(IDirectListings.Status.CREATED) + ); + } + + //////////// ASSUME NATIVE_TOKEN && NO_SPECIAL_PRICE //////////// + + function test_buyFromListing_whenCurrencyToUseNotListedCurrency() + public + whenListingCurrencyIsNativeToken + whenCallIsNotReentrant + whenListingExists + whenBuyerIsApprovedForListing(NATIVE_TOKEN) + whenQuantityToBuyIsValid + whenListingIsActive + whenListedAssetOwnedAndApproved + { + vm.warp(listingParams.startTimestamp); + + vm.prank(buyer); + vm.expectRevert("Paying in invalid currency."); + DirectListingsLogic(marketplace).buyFromListing{ value: 2 ether * listingParams.quantity }( + listingId, + buyer, + listingParams.quantity, + address(erc20), + 2 ether * listingParams.quantity + ); + } + + //////////// ASSUME ERC20 && NO_SPECIAL_PRICE //////////// + + function test_buyFromListing_whenInsufficientTokenBalanceOrAllowance() + public + whenListingCurrencyIsERC20Token + whenCallIsNotReentrant + whenListingExists + whenBuyerIsApprovedForListing(address(erc20)) + whenQuantityToBuyIsValid + whenListingIsActive + whenListedAssetOwnedAndApproved + whenExpectedPriceIsActualPrice + { + vm.warp(listingParams.startTimestamp); + + vm.prank(buyer); + vm.expectRevert("!BAL20"); + DirectListingsLogic(marketplace).buyFromListing( + listingId, + buyer, + listingParams.quantity, + address(erc20), + 1 ether * listingParams.quantity + ); + } + + modifier whenSufficientTokenBalanceOrAllowance() { + vm.prank(buyer); + erc20.approve(marketplace, 100 ether); + _; + } + + function test_buyFromListing_whenMsgValueNotZero() + public + whenListingCurrencyIsERC20Token + whenCallIsNotReentrant + whenListingExists + whenBuyerIsApprovedForListing(address(erc20)) + whenQuantityToBuyIsValid + whenListingIsActive + whenListedAssetOwnedAndApproved + whenExpectedPriceIsActualPrice + whenSufficientTokenBalanceOrAllowance + { + vm.warp(listingParams.startTimestamp); + + vm.prank(buyer); + vm.expectRevert("Marketplace: invalid native tokens sent."); + DirectListingsLogic(marketplace).buyFromListing{ value: 1 ether }( + listingId, + buyer, + listingParams.quantity, + address(erc20), + 1 ether * listingParams.quantity + ); + } + + modifier whenMsgValueIsZero() { + _; + } + + function test_buyFromListing_whenAllRemainingQtyIsBought_erc20() + public + whenListingCurrencyIsERC20Token + whenCallIsNotReentrant + whenListingExists + whenBuyerIsApprovedForListing(address(erc20)) + whenQuantityToBuyIsValid + whenListingIsActive + whenListedAssetOwnedAndApproved + whenExpectedPriceIsActualPrice + whenSufficientTokenBalanceOrAllowance + whenMsgValueIsZero + { + assertEq(erc1155.balanceOf(seller, listingParams.tokenId), 100); + assertEq(erc1155.balanceOf(buyer, listingParams.tokenId), 0); + assertEq( + uint8(DirectListingsLogic(marketplace).getListing(listingId).status), + uint8(IDirectListings.Status.CREATED) + ); + + vm.warp(listingParams.startTimestamp); + vm.prank(buyer); + DirectListingsLogic(marketplace).buyFromListing( + listingId, + buyer, + listingParams.quantity, + address(erc20), + 1 ether * listingParams.quantity + ); + + assertEq(erc1155.balanceOf(seller, listingParams.tokenId), 100 - listingParams.quantity); + assertEq(erc1155.balanceOf(buyer, listingParams.tokenId), listingParams.quantity); + assertEq( + uint8(DirectListingsLogic(marketplace).getListing(listingId).status), + uint8(IDirectListings.Status.COMPLETED) + ); + } + + function test_buyFromListing_whenSomeRemainingQtyIsBought_erc20() + public + whenListingCurrencyIsERC20Token + whenCallIsNotReentrant + whenListingExists + whenBuyerIsApprovedForListing(address(erc20)) + whenQuantityToBuyIsValid + whenListingIsActive + whenListedAssetOwnedAndApproved + whenExpectedPriceIsActualPrice + whenSufficientTokenBalanceOrAllowance + whenMsgValueIsZero + { + assertEq(erc1155.balanceOf(seller, listingParams.tokenId), 100); + assertEq(erc1155.balanceOf(buyer, listingParams.tokenId), 0); + assertEq( + uint8(DirectListingsLogic(marketplace).getListing(listingId).status), + uint8(IDirectListings.Status.CREATED) + ); + + vm.warp(listingParams.startTimestamp); + vm.prank(buyer); + DirectListingsLogic(marketplace).buyFromListing(listingId, buyer, 1, address(erc20), 1 ether); + + assertEq(erc1155.balanceOf(seller, listingParams.tokenId), 99); + assertEq(erc1155.balanceOf(buyer, listingParams.tokenId), 1); + assertEq( + uint8(DirectListingsLogic(marketplace).getListing(listingId).status), + uint8(IDirectListings.Status.CREATED) + ); + } +} diff --git a/src/test/marketplace/direct-listings/buyFromListing/buyFromListing.tree b/src/test/marketplace/direct-listings/buyFromListing/buyFromListing.tree new file mode 100644 index 000000000..0b7ae81fa --- /dev/null +++ b/src/test/marketplace/direct-listings/buyFromListing/buyFromListing.tree @@ -0,0 +1,172 @@ +function buyFromListing( + uint256 _listingId, + address _buyFor, + uint256 _quantity, + address _currency, + uint256 _expectedTotalPrice +) + +// ASSUME NATIVE_TOKEN && SPECIAL_PRICE +. +├── when the call is reentrant +│ └── it should revert +└── when the call is not reentrant + ├── when no listing with the given listing ID exists + │ └── it should revert + └── when listing with the given listing ID exists + ├── when the listing is reserved and caller is not approved for listing + │ └── it should revert + └── when the listing is not reserved OR caller is approved for listing + ├── when quantity to buy is invalid i.e. zero OR exceeds the listing quantity + │ └── it should revert + └── when quantity to buy is valid i.e. non-zero and does not exceed the listing quantity + ├── when the listing is inactive + │ └── it should revert + └── when the listing is active + ├── when the asset is not owned by listing creator or marketplace is not approved for transfer + │ └── it should revert + └── when the asset is owned by listing creator and marketplace is approved for transfer + ├── when the calculated total price is not equal to the expected total price + │ └── it should revert + └── when the calculated total price is equal to the expected total price + ├── when msg.value is not equal to the calculated total price + │ └── it should revert + └── when msg.value is equal to the calculated total price + ├── when the quantity bought is the total remaining listing quantity + │ ├── it should set the status of the lisitng as complete + │ ├── it should subtract the quantity to buy from the listing quantity + │ ├── it should payout platform fees and royalty fees to respective recipients + │ ├── it should transfer the bought tokens from the listing creator to the appropriate recipient + │ └── it should emit NewSale event with the correct total price to paid + └── when the quantity bought is not the total remaining listing quantity + ├── it should subtract the quantity to buy from the listing quantity + ├── it should payout platform fees and royalty fees to respective recipients + ├── it should transfer the bought tokens from the listing creator to the appropriate recipient + └── it should emit NewSale event with the correct total price to paid + +// ASSUME NATIVE_TOKEN && NO_SPECIAL_PRICE +. +├── when the call is reentrant +│ └── it should revert +└── when the call is not reentrant + ├── when no listing with the given listing ID exists + │ └── it should revert + └── when listing with the given listing ID exists + ├── when the listing is reserved and caller is not approved for listing + │ └── it should revert + └── when the listing is not reserved OR caller is approved for listing + ├── when quantity to buy is invalid i.e. zero OR exceeds the listing quantity + │ └── it should revert + └── when quantity to buy is valid i.e. non-zero and does not exceed the listing quantity + ├── when the listing is inactive + │ └── it should revert + └── when the listing is active + ├── when the asset is not owned by listing creator or marketplace is not approved for transfer + │ └── it should revert + └── when the asset is owned by listing creator and marketplace is approved for transfer + ├── when the currency to pay in is not the listing's accepted currency + │ └── it should revert + └── when the currency to pay in is the listing's accepted currency + ├── when the calculated total price is not equal to the expected total price + │ └── it should revert + └── when the calculated total price is equal to the expected total price + ├── when the msg.value is not equal to the calculated total price + │ └── it should revert + └── when the msg.value is equal to the calculated total price + ├── when the quantity bought is the total remaining listing quantity + │ ├── it should set the status of the lisitng as complete + │ ├── it should subtract the quantity to buy from the listing quantity + │ ├── it should payout platform fees and royalty fees to respective recipients + │ ├── it should transfer the bought tokens from the listing creator to the appropriate recipient + │ └── it should emit NewSale event with the correct total price to paid + └── when the quantity bought is not the total remaining listing quantity + ├── it should subtract the quantity to buy from the listing quantity + ├── it should payout platform fees and royalty fees to respective recipients + ├── it should transfer the bought tokens from the listing creator to the appropriate recipient + └── it should emit NewSale event with the correct total price to paid + +// ASSUME ERC20 && NO_SPECIAL_PRICE +. +├── when the call is reentrant +│ └── it should revert +└── when the call is not reentrant + ├── when no listing with the given listing ID exists + │ └── it should revert + └── when listing with the given listing ID exists + ├── when the listing is reserved and caller is not approved for listing + │ └── it should revert + └── when the listing is not reserved OR caller is approved for listing + ├── when quantity to buy is invalid i.e. zero OR exceeds the listing quantity + │ └── it should revert + └── when quantity to buy is valid i.e. non-zero and does not exceed the listing quantity + ├── when the listing is inactive + │ └── it should revert + └── when the listing is active + ├── when the asset is not owned by listing creator or marketplace is not approved for transfer + │ └── it should revert + └── when the asset is owned by listing creator and marketplace is approved for transfer + ├── when the currency to pay in is not the listing's accepted currency + │ └── it should revert + └── when the currency to pay in is the listing's accepted currency + ├── when the calculated total price is not equal to the expected total price + │ └── it should revert + └── when the calculated total price is equal to the expected total price + └── when ERC20 balance and allowance is invalid + ├── it should revert + └── when ERC20 balance and allowance is valid + ├── when msg.value is not zero + │ └── it should revert + └── when msg.value is zero + ├── when the quantity bought is the total remaining listing quantity + │ ├── it should set the status of the lisitng as complete + │ ├── it should subtract the quantity to buy from the listing quantity + │ ├── it should payout platform fees and royalty fees to respective recipients + │ ├── it should transfer the bought tokens from the listing creator to the appropriate recipient + │ └── it should emit NewSale event with the correct total price to paid + └── when the quantity bought is not the total remaining listing quantity + ├── it should subtract the quantity to buy from the listing quantity + ├── it should payout platform fees and royalty fees to respective recipients + ├── it should transfer the bought tokens from the listing creator to the appropriate recipient + └── it should emit NewSale event with the correct total price to paid + + +// ASSUME ERC20 && SPECIAL_PRICE +. +├── when the call is reentrant +│ └── it should revert +└── when the call is not reentrant + ├── when no listing with the given listing ID exists + │ └── it should revert + └── when listing with the given listing ID exists + ├── when the listing is reserved and caller is not approved for listing + │ └── it should revert + └── when the listing is not reserved OR caller is approved for listing + ├── when quantity to buy is invalid i.e. zero OR exceeds the listing quantity + │ └── it should revert + └── when quantity to buy is valid i.e. non-zero and does not exceed the listing quantity + ├── when the listing is inactive + │ └── it should revert + └── when the listing is active + ├── when the asset is not owned by listing creator or marketplace is not approved for transfer + │ └── it should revert + └── when the asset is owned by listing creator and marketplace is approved for transfer + ├── when the calculated total price is not equal to the expected total price + │ └── it should revert + └── when the calculated total price is equal to the expected total price + ├── when ERC20 balance and allowance is invalid + │ └── it should revert + └── when ERC20 balance and allowance is valid + ├── when msg.value is not zero + │ └── it should revert + └── when msg.value is zero + ├── when the quantity bought is the total remaining listing quantity + │ ├── it should set the status of the lisitng as complete + │ ├── it should subtract the quantity to buy from the listing quantity + │ ├── it should payout platform fees and royalty fees to respective recipients + │ ├── it should transfer the bought tokens from the listing creator to the appropriate recipient + │ └── it should emit NewSale event with the correct total price to paid + └── when the quantity bought is not the total remaining listing quantity + ├── it should subtract the quantity to buy from the listing quantity + ├── it should payout platform fees and royalty fees to respective recipients + ├── it should transfer the bought tokens from the listing creator to the appropriate recipient + └── it should emit NewSale event with the correct total price to paid diff --git a/src/test/marketplace/direct-listings/cancelListing/cancelListing.t.sol b/src/test/marketplace/direct-listings/cancelListing/cancelListing.t.sol new file mode 100644 index 000000000..6e77c4fe0 --- /dev/null +++ b/src/test/marketplace/direct-listings/cancelListing/cancelListing.t.sol @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../../utils/BaseTest.sol"; +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { MarketplaceV3 } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { DirectListingsLogic } from "contracts/prebuilts/marketplace/direct-listings/DirectListingsLogic.sol"; +import { IDirectListings } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +contract CancelListingTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + + // Default listing parameters + IDirectListings.ListingParameters internal listingParams; + uint256 internal listingId = 0; + + // Events to test + + /// @notice Emitted when a listing is updated. + event CancelledListing(address indexed listingCreator, uint256 indexed listingId); + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + + vm.stopPrank(); + + // Setup listing params + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100 minutes; + uint128 endTimestamp = 200 minutes; + bool reserved = true; + + listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + // Mint 1 ERC721 NFT to seller + erc721.mint(seller, listingParams.quantity); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `DirectListings` + address directListings = address(new DirectListingsLogic(address(weth))); + vm.label(directListings, "DirectListings_Extension"); + + // Extension: DirectListingsLogic + Extension memory extension_directListings; + extension_directListings.metadata = ExtensionMetadata({ + name: "DirectListingsLogic", + metadataURI: "ipfs://DirectListings", + implementation: directListings + }); + + extension_directListings.functions = new ExtensionFunction[](13); + extension_directListings.functions[0] = ExtensionFunction( + DirectListingsLogic.totalListings.selector, + "totalListings()" + ); + extension_directListings.functions[1] = ExtensionFunction( + DirectListingsLogic.isBuyerApprovedForListing.selector, + "isBuyerApprovedForListing(uint256,address)" + ); + extension_directListings.functions[2] = ExtensionFunction( + DirectListingsLogic.isCurrencyApprovedForListing.selector, + "isCurrencyApprovedForListing(uint256,address)" + ); + extension_directListings.functions[3] = ExtensionFunction( + DirectListingsLogic.currencyPriceForListing.selector, + "currencyPriceForListing(uint256,address)" + ); + extension_directListings.functions[4] = ExtensionFunction( + DirectListingsLogic.createListing.selector, + "createListing((address,uint256,uint256,address,uint256,uint128,uint128,bool))" + ); + extension_directListings.functions[5] = ExtensionFunction( + DirectListingsLogic.updateListing.selector, + "updateListing(uint256,(address,uint256,uint256,address,uint256,uint128,uint128,bool))" + ); + extension_directListings.functions[6] = ExtensionFunction( + DirectListingsLogic.cancelListing.selector, + "cancelListing(uint256)" + ); + extension_directListings.functions[7] = ExtensionFunction( + DirectListingsLogic.approveBuyerForListing.selector, + "approveBuyerForListing(uint256,address,bool)" + ); + extension_directListings.functions[8] = ExtensionFunction( + DirectListingsLogic.approveCurrencyForListing.selector, + "approveCurrencyForListing(uint256,address,uint256)" + ); + extension_directListings.functions[9] = ExtensionFunction( + DirectListingsLogic.buyFromListing.selector, + "buyFromListing(uint256,address,uint256,address,uint256)" + ); + extension_directListings.functions[10] = ExtensionFunction( + DirectListingsLogic.getAllListings.selector, + "getAllListings(uint256,uint256)" + ); + extension_directListings.functions[11] = ExtensionFunction( + DirectListingsLogic.getAllValidListings.selector, + "getAllValidListings(uint256,uint256)" + ); + extension_directListings.functions[12] = ExtensionFunction( + DirectListingsLogic.getListing.selector, + "getListing(uint256)" + ); + + extensions[0] = extension_directListings; + } + + function test_cancelListing_whenListingDoesntExist() public { + vm.prank(seller); + vm.expectRevert("Marketplace: invalid listing."); + DirectListingsLogic(marketplace).cancelListing(listingId); + } + + modifier whenListingExists() { + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc721)); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + vm.stopPrank(); + + vm.startPrank(seller); + erc721.setApprovalForAll(marketplace, true); + listingId = DirectListingsLogic(marketplace).createListing(listingParams); + erc721.setApprovalForAll(marketplace, false); + vm.stopPrank(); + + vm.prank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(erc721)); + _; + } + + function test_cancelListing_whenCallerNotListingCreator() public whenListingExists { + vm.prank(address(0x4567)); + vm.expectRevert("Marketplace: not listing creator."); + DirectListingsLogic(marketplace).cancelListing(listingId); + } + + modifier whenCallerIsListingCreator() { + _; + } + + function test_cancelListing_success() public whenListingExists whenCallerIsListingCreator { + vm.warp(listingParams.startTimestamp + 1); + + assertEq(uint8(DirectListingsLogic(marketplace).getListing(listingId).status), uint8(1)); // CREATED + + vm.prank(seller); + vm.expectEmit(true, true, true, true); + emit CancelledListing(seller, listingId); + DirectListingsLogic(marketplace).cancelListing(listingId); + + assertEq(uint8(DirectListingsLogic(marketplace).getListing(listingId).status), uint8(3)); // CANCELLED + } +} diff --git a/src/test/marketplace/direct-listings/cancelListing/cancelListing.tree b/src/test/marketplace/direct-listings/cancelListing/cancelListing.tree new file mode 100644 index 000000000..dd34eb23d --- /dev/null +++ b/src/test/marketplace/direct-listings/cancelListing/cancelListing.tree @@ -0,0 +1,9 @@ +function cancelListing(uint256 _listingId) +├── when no listing with the given listing ID exists +│ └── it should revert ✅ +└── when listing with the given listing ID exists + ├── when the caller is not listing creator + │ └── it should revert ✅ + └── when the caller is listing creator ✅ + ├── it should set status of listing as cancelled + └── it should emit CancelledListing event \ No newline at end of file diff --git a/src/test/marketplace/direct-listings/createListing/createListing.t.sol b/src/test/marketplace/direct-listings/createListing/createListing.t.sol new file mode 100644 index 000000000..415a1b71d --- /dev/null +++ b/src/test/marketplace/direct-listings/createListing/createListing.t.sol @@ -0,0 +1,363 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../../utils/BaseTest.sol"; +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { MarketplaceV3 } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { DirectListingsLogic } from "contracts/prebuilts/marketplace/direct-listings/DirectListingsLogic.sol"; +import { IDirectListings } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +contract CreateListingTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + + // Default listing parameters + IDirectListings.ListingParameters internal listingParams; + + // Events to test + + /// @notice Emitted when a new listing is created. + event NewListing( + address indexed listingCreator, + uint256 indexed listingId, + address indexed assetContract, + IDirectListings.Listing listing + ); + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + + vm.stopPrank(); + + // Setup listing params + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + // Mint 1 ERC721 NFT to seller + erc721.mint(seller, listingParams.quantity); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `DirectListings` + address directListings = address(new DirectListingsLogic(address(weth))); + vm.label(directListings, "DirectListings_Extension"); + + // Extension: DirectListingsLogic + Extension memory extension_directListings; + extension_directListings.metadata = ExtensionMetadata({ + name: "DirectListingsLogic", + metadataURI: "ipfs://DirectListings", + implementation: directListings + }); + + extension_directListings.functions = new ExtensionFunction[](13); + extension_directListings.functions[0] = ExtensionFunction( + DirectListingsLogic.totalListings.selector, + "totalListings()" + ); + extension_directListings.functions[1] = ExtensionFunction( + DirectListingsLogic.isBuyerApprovedForListing.selector, + "isBuyerApprovedForListing(uint256,address)" + ); + extension_directListings.functions[2] = ExtensionFunction( + DirectListingsLogic.isCurrencyApprovedForListing.selector, + "isCurrencyApprovedForListing(uint256,address)" + ); + extension_directListings.functions[3] = ExtensionFunction( + DirectListingsLogic.currencyPriceForListing.selector, + "currencyPriceForListing(uint256,address)" + ); + extension_directListings.functions[4] = ExtensionFunction( + DirectListingsLogic.createListing.selector, + "createListing((address,uint256,uint256,address,uint256,uint128,uint128,bool))" + ); + extension_directListings.functions[5] = ExtensionFunction( + DirectListingsLogic.updateListing.selector, + "updateListing(uint256,(address,uint256,uint256,address,uint256,uint128,uint128,bool))" + ); + extension_directListings.functions[6] = ExtensionFunction( + DirectListingsLogic.cancelListing.selector, + "cancelListing(uint256)" + ); + extension_directListings.functions[7] = ExtensionFunction( + DirectListingsLogic.approveBuyerForListing.selector, + "approveBuyerForListing(uint256,address,bool)" + ); + extension_directListings.functions[8] = ExtensionFunction( + DirectListingsLogic.approveCurrencyForListing.selector, + "approveCurrencyForListing(uint256,address,uint256)" + ); + extension_directListings.functions[9] = ExtensionFunction( + DirectListingsLogic.buyFromListing.selector, + "buyFromListing(uint256,address,uint256,address,uint256)" + ); + extension_directListings.functions[10] = ExtensionFunction( + DirectListingsLogic.getAllListings.selector, + "getAllListings(uint256,uint256)" + ); + extension_directListings.functions[11] = ExtensionFunction( + DirectListingsLogic.getAllValidListings.selector, + "getAllValidListings(uint256,uint256)" + ); + extension_directListings.functions[12] = ExtensionFunction( + DirectListingsLogic.getListing.selector, + "getListing(uint256)" + ); + + extensions[0] = extension_directListings; + } + + function test_createListing_whenCallerDoesNotHaveListerRole() public { + bytes32 role = keccak256("LISTER_ROLE"); + assertEq(Permissions(marketplace).hasRole(role, seller), false); + + vm.prank(seller); + vm.expectRevert("!LISTER_ROLE"); + DirectListingsLogic(marketplace).createListing(listingParams); + } + + modifier whenCallerHasListerRole() { + vm.prank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + _; + } + + function test_createListing_whenAssetDoesNotHaveAssetRole() public whenCallerHasListerRole { + bytes32 role = keccak256("ASSET_ROLE"); + assertEq(Permissions(marketplace).hasRole(role, listingParams.assetContract), false); + + vm.prank(seller); + vm.expectRevert("!ASSET_ROLE"); + DirectListingsLogic(marketplace).createListing(listingParams); + } + + modifier whenAssetHasAssetRole() { + vm.prank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), listingParams.assetContract); + _; + } + + function test_createListing_startTimeGteEndTime() public whenCallerHasListerRole whenAssetHasAssetRole { + listingParams.startTimestamp = 200; + listingParams.endTimestamp = 100; + + vm.prank(seller); + vm.expectRevert("Marketplace: endTimestamp not greater than startTimestamp."); + DirectListingsLogic(marketplace).createListing(listingParams); + + listingParams.endTimestamp = 200; + + vm.prank(seller); + vm.expectRevert("Marketplace: endTimestamp not greater than startTimestamp."); + DirectListingsLogic(marketplace).createListing(listingParams); + } + + modifier whenStartTimeLtEndTime() { + listingParams.startTimestamp = 100; + listingParams.endTimestamp = 200; + _; + } + + modifier whenStartTimeLtBlockTimestamp() { + // This warp has no effect on subsequent tests since they include a vm.warp in their own test body. + vm.warp(listingParams.startTimestamp + 1); + _; + } + + function test_createListing_whenStartTimeMoreThanHourBeforeBlockTimestamp() + public + whenCallerHasListerRole + whenAssetHasAssetRole + whenStartTimeLtEndTime + whenStartTimeLtBlockTimestamp + { + vm.warp(listingParams.startTimestamp + (60 minutes + 1)); + + vm.prank(seller); + vm.expectRevert("Marketplace: invalid startTimestamp."); + DirectListingsLogic(marketplace).createListing(listingParams); + } + + modifier whenStartTimeWithinHourOfBlockTimestamp() { + vm.warp(listingParams.startTimestamp + 59 minutes); + _; + } + + function test_createListing_whenListingParamsAreInvalid_1() + public + whenCallerHasListerRole + whenAssetHasAssetRole + whenStartTimeLtEndTime + whenStartTimeLtBlockTimestamp + whenStartTimeWithinHourOfBlockTimestamp + { + // This is one of the ways in which params are considered invalid. + // We've written separate BTT tests for `_validateNewListing` + listingParams.quantity = 0; + + vm.prank(seller); + vm.expectRevert("Marketplace: listing zero quantity."); + DirectListingsLogic(marketplace).createListing(listingParams); + } + + modifier whenListingParamsAreValid() { + // Approve marketplace to transfer tokens -- else listing params are considered invalid. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + _; + } + + function test_createListing_whenListingParamsAreValid_1() + public + whenCallerHasListerRole + whenAssetHasAssetRole + whenStartTimeLtEndTime + whenStartTimeLtBlockTimestamp + whenStartTimeWithinHourOfBlockTimestamp + whenListingParamsAreValid + { + uint256 expectedListingId = 0; + + assertEq(DirectListingsLogic(marketplace).totalListings(), 0); + assertEq(DirectListingsLogic(marketplace).getListing(expectedListingId).assetContract, address(0)); + + IDirectListings.Listing memory listing; + + vm.prank(seller); + vm.expectEmit(true, true, true, false); + emit NewListing(seller, expectedListingId, listingParams.assetContract, listing); + DirectListingsLogic(marketplace).createListing(listingParams); + + listing = DirectListingsLogic(marketplace).getListing(expectedListingId); + assertEq(listing.assetContract, listingParams.assetContract); + assertEq(listing.tokenId, listingParams.tokenId); + assertEq(listing.quantity, listingParams.quantity); + assertEq(listing.currency, listingParams.currency); + assertEq(listing.pricePerToken, listingParams.pricePerToken); + assertEq(listing.endTimestamp, block.timestamp + (listingParams.endTimestamp - listingParams.startTimestamp)); + assertEq(listing.startTimestamp, block.timestamp); + assertEq(listing.listingCreator, seller); + assertEq(listing.reserved, true); + assertEq(uint256(listing.status), 1); // Status.CREATED + assertEq(uint256(listing.tokenType), 0); // TokenType.ERC721 + + assertEq(DirectListingsLogic(marketplace).totalListings(), 1); + assertEq(DirectListingsLogic(marketplace).getAllListings(0, 0).length, 1); + assertEq(DirectListingsLogic(marketplace).getAllValidListings(0, 0).length, 1); + } + + modifier whenStartTimeGteBlockTimestamp() { + vm.warp(listingParams.startTimestamp - 1 minutes); + _; + } + + function test_createListing_whenListingParamsAreInvalid_2() + public + whenCallerHasListerRole + whenAssetHasAssetRole + whenStartTimeLtEndTime + whenStartTimeGteBlockTimestamp + { + // This is one of the ways in which params are considered invalid. + // We've written separate BTT tests for `_validateNewListing` + listingParams.quantity = 0; + + vm.prank(seller); + vm.expectRevert("Marketplace: listing zero quantity."); + DirectListingsLogic(marketplace).createListing(listingParams); + } + + function test_createListing_whenListingParamsAreValid_2() + public + whenCallerHasListerRole + whenAssetHasAssetRole + whenStartTimeLtEndTime + whenStartTimeGteBlockTimestamp + whenListingParamsAreValid + { + uint256 expectedListingId = 0; + + assertEq(DirectListingsLogic(marketplace).totalListings(), 0); + assertEq(DirectListingsLogic(marketplace).getListing(expectedListingId).assetContract, address(0)); + + IDirectListings.Listing memory listing; + + vm.prank(seller); + vm.expectEmit(true, true, true, false); + emit NewListing(seller, expectedListingId, listingParams.assetContract, listing); + DirectListingsLogic(marketplace).createListing(listingParams); + + listing = DirectListingsLogic(marketplace).getListing(expectedListingId); + assertEq(listing.assetContract, listingParams.assetContract); + assertEq(listing.tokenId, listingParams.tokenId); + assertEq(listing.quantity, listingParams.quantity); + assertEq(listing.currency, listingParams.currency); + assertEq(listing.pricePerToken, listingParams.pricePerToken); + assertEq(listing.endTimestamp, listingParams.endTimestamp); + assertEq(listing.startTimestamp, listingParams.startTimestamp); + assertEq(listing.listingCreator, seller); + assertEq(listing.reserved, true); + assertEq(uint256(listing.status), 1); // Status.CREATED + assertEq(uint256(listing.tokenType), 0); // TokenType.ERC721 + + assertEq(DirectListingsLogic(marketplace).totalListings(), 1); + assertEq(DirectListingsLogic(marketplace).getAllListings(0, 0).length, 1); + assertEq(DirectListingsLogic(marketplace).getAllValidListings(0, 0).length, 0); + } +} diff --git a/src/test/marketplace/direct-listings/createListing/createListing.tree b/src/test/marketplace/direct-listings/createListing/createListing.tree new file mode 100644 index 000000000..8964c7798 --- /dev/null +++ b/src/test/marketplace/direct-listings/createListing/createListing.tree @@ -0,0 +1,27 @@ +function createListing(ListingParameters calldata _params) +├── when caller does not have LISTER_ROLE +│ └── it should revert +└── when the caller has lister LISTER_ROLE + ├── when the asset to list does not have ASSET_ROLE + │ └── it should revert + └── when the asset to list has ASSET_ROLE + ├── when the start time is greater i.e. after the end time + │ └── it should revert + └── when the start time is less than i.e. before the end time + ├── when the start time is less than i.e. before block timestamp + │ ├── when the start time is more than 60 minutes before block timestamp + │ │ └── it should revert + │ └── when the start time is less than or equal to 60 minutes before block timestamp + │ ├── when the listing params are invalid + │ │ └── it should revert + │ └── when the listing params are valid + │ ├── it should store the listing at a new listing ID + │ ├── it should return the listing ID + │ └── it should emit NewListing event with listing creator, listing ID, and listing data + └── when the start time is greater than i.e. after, or equal to block timestamp + ├── when the listing params are invalid + │ └── it should revert + └── when the listing params are valid + ├── it should store the listing at a new listing ID + ├── it should return the listing ID + └── it should emit NewListing event with listing creator, listing ID, and listing data \ No newline at end of file diff --git a/src/test/marketplace/direct-listings/updateListing/updateListing.t.sol b/src/test/marketplace/direct-listings/updateListing/updateListing.t.sol new file mode 100644 index 000000000..6b35615ea --- /dev/null +++ b/src/test/marketplace/direct-listings/updateListing/updateListing.t.sol @@ -0,0 +1,502 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../../utils/BaseTest.sol"; +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { MarketplaceV3 } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { DirectListingsLogic } from "contracts/prebuilts/marketplace/direct-listings/DirectListingsLogic.sol"; +import { IDirectListings } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +contract UpdateListingTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + + // Default listing parameters + IDirectListings.ListingParameters internal listingParams; + uint256 internal listingId = 0; + + // Events to test + + /// @notice Emitted when a listing is updated. + event UpdatedListing( + address indexed listingCreator, + uint256 indexed listingId, + address indexed assetContract, + IDirectListings.Listing listing + ); + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + + vm.stopPrank(); + + // Setup listing params + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100 minutes; + uint128 endTimestamp = 200 minutes; + bool reserved = true; + + listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + // Mint 1 ERC721 NFT to seller + erc721.mint(seller, listingParams.quantity); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `DirectListings` + address directListings = address(new DirectListingsLogic(address(weth))); + vm.label(directListings, "DirectListings_Extension"); + + // Extension: DirectListingsLogic + Extension memory extension_directListings; + extension_directListings.metadata = ExtensionMetadata({ + name: "DirectListingsLogic", + metadataURI: "ipfs://DirectListings", + implementation: directListings + }); + + extension_directListings.functions = new ExtensionFunction[](13); + extension_directListings.functions[0] = ExtensionFunction( + DirectListingsLogic.totalListings.selector, + "totalListings()" + ); + extension_directListings.functions[1] = ExtensionFunction( + DirectListingsLogic.isBuyerApprovedForListing.selector, + "isBuyerApprovedForListing(uint256,address)" + ); + extension_directListings.functions[2] = ExtensionFunction( + DirectListingsLogic.isCurrencyApprovedForListing.selector, + "isCurrencyApprovedForListing(uint256,address)" + ); + extension_directListings.functions[3] = ExtensionFunction( + DirectListingsLogic.currencyPriceForListing.selector, + "currencyPriceForListing(uint256,address)" + ); + extension_directListings.functions[4] = ExtensionFunction( + DirectListingsLogic.createListing.selector, + "createListing((address,uint256,uint256,address,uint256,uint128,uint128,bool))" + ); + extension_directListings.functions[5] = ExtensionFunction( + DirectListingsLogic.updateListing.selector, + "updateListing(uint256,(address,uint256,uint256,address,uint256,uint128,uint128,bool))" + ); + extension_directListings.functions[6] = ExtensionFunction( + DirectListingsLogic.cancelListing.selector, + "cancelListing(uint256)" + ); + extension_directListings.functions[7] = ExtensionFunction( + DirectListingsLogic.approveBuyerForListing.selector, + "approveBuyerForListing(uint256,address,bool)" + ); + extension_directListings.functions[8] = ExtensionFunction( + DirectListingsLogic.approveCurrencyForListing.selector, + "approveCurrencyForListing(uint256,address,uint256)" + ); + extension_directListings.functions[9] = ExtensionFunction( + DirectListingsLogic.buyFromListing.selector, + "buyFromListing(uint256,address,uint256,address,uint256)" + ); + extension_directListings.functions[10] = ExtensionFunction( + DirectListingsLogic.getAllListings.selector, + "getAllListings(uint256,uint256)" + ); + extension_directListings.functions[11] = ExtensionFunction( + DirectListingsLogic.getAllValidListings.selector, + "getAllValidListings(uint256,uint256)" + ); + extension_directListings.functions[12] = ExtensionFunction( + DirectListingsLogic.getListing.selector, + "getListing(uint256)" + ); + + extensions[0] = extension_directListings; + } + + function test_updateListing_whenListingDoesNotExist() public { + vm.prank(seller); + vm.expectRevert("Marketplace: invalid listing."); + DirectListingsLogic(marketplace).updateListing(listingId, listingParams); + } + + modifier whenListingExists() { + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc721)); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + vm.stopPrank(); + + vm.startPrank(seller); + erc721.setApprovalForAll(marketplace, true); + listingId = DirectListingsLogic(marketplace).createListing(listingParams); + erc721.setApprovalForAll(marketplace, false); + vm.stopPrank(); + + vm.prank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(erc721)); + _; + } + + function test_updateListing_whenAssetDoesntHaveAssetRole() public whenListingExists { + vm.prank(seller); + vm.expectRevert("!ASSET_ROLE"); + DirectListingsLogic(marketplace).updateListing(listingId, listingParams); + } + + modifier whenAssetHasAssetRole() { + vm.prank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc721)); + _; + } + + function test_updateListing_whenCallerIsNotListingCreator() public whenListingExists whenAssetHasAssetRole { + vm.prank(address(0x4567)); + vm.expectRevert("Marketplace: not listing creator."); + DirectListingsLogic(marketplace).updateListing(listingId, listingParams); + } + + modifier whenCallerIsListingCreator() { + _; + } + + function test_updateListing_whenListingHasExpired() + public + whenListingExists + whenAssetHasAssetRole + whenCallerIsListingCreator + { + vm.warp(listingParams.endTimestamp + 1); + + vm.prank(seller); + vm.expectRevert("Marketplace: listing expired."); + DirectListingsLogic(marketplace).updateListing(listingId, listingParams); + } + + modifier whenListingNotExpired() { + vm.warp(0); + _; + } + + function test_updateListing_whenUpdatedAssetIsDifferent() + public + whenListingExists + whenAssetHasAssetRole + whenCallerIsListingCreator + whenListingNotExpired + { + vm.prank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc1155)); + listingParams.assetContract = address(erc1155); + + vm.prank(seller); + vm.expectRevert("Marketplace: cannot update what token is listed."); + DirectListingsLogic(marketplace).updateListing(listingId, listingParams); + + listingParams.assetContract = address(erc721); + listingParams.tokenId = 10; + + vm.prank(seller); + vm.expectRevert("Marketplace: cannot update what token is listed."); + DirectListingsLogic(marketplace).updateListing(listingId, listingParams); + } + + modifier whenUpdatedAssetIsSame() { + _; + } + + function test_updateListing_whenUpdatedStartTimeGteEndTime() + public + whenListingExists + whenAssetHasAssetRole + whenCallerIsListingCreator + whenListingNotExpired + whenUpdatedAssetIsSame + { + listingParams.startTimestamp = 200; + listingParams.endTimestamp = 100; + + vm.prank(seller); + vm.expectRevert("Marketplace: endTimestamp not greater than startTimestamp."); + DirectListingsLogic(marketplace).updateListing(listingId, listingParams); + } + + modifier whenUpdatedStartTimeLtUpdatedEndTime() { + _; + } + + function test_updateListing_whenUpdateMakesActiveListingInactive() + public + whenListingExists + whenAssetHasAssetRole + whenCallerIsListingCreator + whenListingNotExpired + whenUpdatedAssetIsSame + whenUpdatedStartTimeLtUpdatedEndTime + { + vm.warp(listingParams.startTimestamp + 1); + + listingParams.startTimestamp += 50; + + vm.prank(seller); + vm.expectRevert("Marketplace: listing already active."); + DirectListingsLogic(marketplace).updateListing(listingId, listingParams); + } + + modifier whenUpdateDoesntMakeActiveListingInactive() { + _; + } + + modifier whenUpdatedStartIsDiffAndInPast() { + vm.warp(listingParams.startTimestamp - 1 minutes); + listingParams.startTimestamp -= 2 minutes; + _; + } + + function test_updateListing_whenUpdatedStartIsMoreThanHourInPast() + public + whenListingExists + whenAssetHasAssetRole + whenCallerIsListingCreator + whenListingNotExpired + whenUpdatedAssetIsSame + whenUpdatedStartTimeLtUpdatedEndTime + whenUpdateDoesntMakeActiveListingInactive + whenUpdatedStartIsDiffAndInPast + { + listingParams.startTimestamp = 30 minutes; + + vm.prank(seller); + vm.expectRevert("Marketplace: invalid startTimestamp."); + DirectListingsLogic(marketplace).updateListing(listingId, listingParams); + } + + modifier whenUpdatedStartIsWithinPastHour() { + listingParams.startTimestamp = 90 minutes; + _; + } + + function test_updateListing_whenUpdatedPriceIsDifferentFromApprovedPrice_1() + public + whenListingExists + whenAssetHasAssetRole + whenCallerIsListingCreator + whenListingNotExpired + whenUpdatedAssetIsSame + whenUpdatedStartTimeLtUpdatedEndTime + whenUpdateDoesntMakeActiveListingInactive + whenUpdatedStartIsDiffAndInPast + whenUpdatedStartIsWithinPastHour + { + vm.prank(seller); + DirectListingsLogic(marketplace).approveCurrencyForListing(listingId, address(weth), 2 ether); + + listingParams.currency = address(weth); + + vm.prank(seller); + vm.expectRevert("Marketplace: price different from approved price"); + DirectListingsLogic(marketplace).updateListing(listingId, listingParams); + } + + modifier whenUpdatedPriceIsSameAsApprovedPrice() { + _; + } + + function test_updateListing_whenListingParamsAreInvalid_1() + public + whenListingExists + whenAssetHasAssetRole + whenCallerIsListingCreator + whenListingNotExpired + whenUpdatedAssetIsSame + whenUpdatedStartTimeLtUpdatedEndTime + whenUpdateDoesntMakeActiveListingInactive + whenUpdatedStartIsDiffAndInPast + whenUpdatedStartIsWithinPastHour + whenUpdatedPriceIsSameAsApprovedPrice + { + // This is one of the ways in which params can be invalid. + // Separate tests for `_validateNewListingParams` + listingParams.quantity = 0; + + vm.prank(seller); + vm.expectRevert("Marketplace: listing zero quantity."); + DirectListingsLogic(marketplace).updateListing(listingId, listingParams); + } + + modifier whenListingParamsAreValid() { + _; + } + + function test_updateListing_whenListingParamsAreValid_1() + public + whenListingExists + whenAssetHasAssetRole + whenCallerIsListingCreator + whenListingNotExpired + whenUpdatedAssetIsSame + whenUpdatedStartTimeLtUpdatedEndTime + whenUpdateDoesntMakeActiveListingInactive + whenUpdatedStartIsDiffAndInPast + whenUpdatedStartIsWithinPastHour + whenUpdatedPriceIsSameAsApprovedPrice + whenListingParamsAreValid + { + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + IDirectListings.Listing memory listing; + + vm.prank(seller); + vm.expectEmit(true, true, true, false); + emit UpdatedListing(seller, listingId, listingParams.assetContract, listing); + DirectListingsLogic(marketplace).updateListing(listingId, listingParams); + + IDirectListings.Listing memory updatedListing = DirectListingsLogic(marketplace).getListing(listingId); + + assertEq(updatedListing.assetContract, listingParams.assetContract); + assertEq(updatedListing.tokenId, listingParams.tokenId); + assertEq(updatedListing.quantity, listingParams.quantity); + assertEq(updatedListing.currency, listingParams.currency); + assertEq(updatedListing.pricePerToken, listingParams.pricePerToken); + assertEq(updatedListing.endTimestamp, listingParams.endTimestamp); + assertEq(updatedListing.startTimestamp, block.timestamp); + assertEq(updatedListing.listingCreator, seller); + assertEq(updatedListing.reserved, true); + assertEq(uint256(updatedListing.status), 1); // Status.CREATED + assertEq(uint256(updatedListing.tokenType), 0); // TokenType.ERC721 + } + + modifier whenUpdatedStartIsSameAsCurrentStart() { + _; + } + + function test_updateListing_whenUpdatedPriceIsDifferentFromApprovedPrice_2() + public + whenListingExists + whenAssetHasAssetRole + whenCallerIsListingCreator + whenListingNotExpired + whenUpdatedAssetIsSame + whenUpdatedStartTimeLtUpdatedEndTime + whenUpdateDoesntMakeActiveListingInactive + whenUpdatedStartIsSameAsCurrentStart + { + vm.prank(seller); + DirectListingsLogic(marketplace).approveCurrencyForListing(listingId, address(weth), 2 ether); + + listingParams.currency = address(weth); + + vm.prank(seller); + vm.expectRevert("Marketplace: price different from approved price"); + DirectListingsLogic(marketplace).updateListing(listingId, listingParams); + } + + function test_updateListing_whenListingParamsAreInvalid_2() + public + whenListingExists + whenAssetHasAssetRole + whenCallerIsListingCreator + whenListingNotExpired + whenUpdatedAssetIsSame + whenUpdatedStartTimeLtUpdatedEndTime + whenUpdateDoesntMakeActiveListingInactive + whenUpdatedStartIsSameAsCurrentStart + whenUpdatedPriceIsSameAsApprovedPrice + { + // This is one of the ways in which params can be invalid. + // Separate tests for `_validateNewListingParams` + listingParams.quantity = 0; + + vm.prank(seller); + vm.expectRevert("Marketplace: listing zero quantity."); + DirectListingsLogic(marketplace).updateListing(listingId, listingParams); + } + + function test_updateListing_whenListingParamsAreValid_2() + public + whenListingExists + whenAssetHasAssetRole + whenCallerIsListingCreator + whenListingNotExpired + whenUpdatedAssetIsSame + whenUpdatedStartTimeLtUpdatedEndTime + whenUpdateDoesntMakeActiveListingInactive + whenUpdatedStartIsSameAsCurrentStart + whenUpdatedPriceIsSameAsApprovedPrice + whenListingParamsAreValid + { + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + IDirectListings.Listing memory listing; + + vm.prank(seller); + vm.expectEmit(true, true, true, false); + emit UpdatedListing(seller, listingId, listingParams.assetContract, listing); + DirectListingsLogic(marketplace).updateListing(listingId, listingParams); + + IDirectListings.Listing memory updatedListing = DirectListingsLogic(marketplace).getListing(listingId); + + assertEq(updatedListing.assetContract, listingParams.assetContract); + assertEq(updatedListing.tokenId, listingParams.tokenId); + assertEq(updatedListing.quantity, listingParams.quantity); + assertEq(updatedListing.currency, listingParams.currency); + assertEq(updatedListing.pricePerToken, listingParams.pricePerToken); + assertEq(updatedListing.endTimestamp, listingParams.endTimestamp); + assertEq(updatedListing.startTimestamp, listingParams.startTimestamp); + assertEq(updatedListing.listingCreator, seller); + assertEq(updatedListing.reserved, true); + assertEq(uint256(updatedListing.status), 1); // Status.CREATED + assertEq(uint256(updatedListing.tokenType), 0); // TokenType.ERC721 + } +} diff --git a/src/test/marketplace/direct-listings/updateListing/updateListing.tree b/src/test/marketplace/direct-listings/updateListing/updateListing.tree new file mode 100644 index 000000000..982ed1076 --- /dev/null +++ b/src/test/marketplace/direct-listings/updateListing/updateListing.tree @@ -0,0 +1,43 @@ +function updateListing(uint256 _listingId, ListingParameters memory _params) +├── when the listing does not exist +│ └── it should revert ✅ +└── when listing exists + ├── when asset does not have ASSET_ROLE + │ └── it should revert ✅ + └── when asset has ASSET_ROLE + ├── when caller is not listing creator + │ └── it should revert ✅ + └── when caller is listing creator + ├── when listing has expired + │ └── it should revert ✅ + └── when listing has not expired + ├── when the updated asset is different from the listed asset + │ └── it should revert ✅ + └── when the updated asset is the same as the listed asset + ├── when the updated start time is greater or equal to than the updated end time + │ └── it should revert ✅ + └── when the updated start time is less than the updated end time + ├── when update makes active listing inactive + │ └── it should revert ✅ + └── when update does not make active listing inactive + ├── when the updated start time is in the past and different from the listed start time + │ ├── when the updated start time is more than 60 minutes before block timestamp + │ │ └── it should revert ✅ + │ └── when the updated start time is within 60 minutes past block timestamp + │ ├── when updated price in updated currency different from approved price for updated currency + │ │ └── it should revert ✅ + │ └── when updated price in updated currency is same as approved price for updated currency + │ ├── when updated listing params are invalid + │ │ └── it should revert ✅ + │ └── when updated listing params are valid ✅ + │ ├── it should store updated listing at the same listing ID + │ └── it should emit UpdatedListing event with listing creator, listing ID, updated asset contract and listing data + └── when the updated start time is same as listed start time + ├── when updated price in updated currency different from approved price for updated currency + │ └── it should revert ✅ + └── when updated price in updated currency is same as approved price for updated currency + ├── when updated listing params are invalid + │ └── it should revert ✅ + └── when updated listing params are valid ✅ + ├── it should store updated listing at the same listing ID + └── it should emit UpdatedListing event with listing creator, listing ID, updated asset contract and listing data \ No newline at end of file diff --git a/src/test/marketplace/english-auctions/_payout/_payout.t.sol b/src/test/marketplace/english-auctions/_payout/_payout.t.sol new file mode 100644 index 000000000..822bf4bda --- /dev/null +++ b/src/test/marketplace/english-auctions/_payout/_payout.t.sol @@ -0,0 +1,312 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test helper imports +import "../../../utils/BaseTest.sol"; + +// Test contracts and interfaces +import { RoyaltyPaymentsLogic } from "contracts/extension/plugin/RoyaltyPayments.sol"; +import { MarketplaceV3, IPlatformFee } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { EnglishAuctionsLogic } from "contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsLogic.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { ERC721Base } from "contracts/base/ERC721Base.sol"; +import { MockRoyaltyEngineV1 } from "../../../mocks/MockRoyaltyEngineV1.sol"; +import { PlatformFee } from "contracts/extension/PlatformFee.sol"; + +import { IEnglishAuctions } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +contract ReentrantRecipient is ERC1155Holder { + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes memory data + ) public virtual override returns (bytes4) { + uint256 auctionId = 0; + uint256 bidAmount = 10 ether; + EnglishAuctionsLogic(msg.sender).bidInAuction(auctionId, bidAmount); + return super.onERC1155Received(operator, from, id, value, data); + } +} + +contract EnglishAuctionsPayoutTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + address private defaultFeeRecipient; + + // Auction parameters + uint256 internal auctionId; + uint256 internal bidAmount; + address internal winningBidder = address(0x123); + IEnglishAuctions.AuctionParameters internal auctionParams; + + // Events + event NewBid( + uint256 indexed auctionId, + address indexed bidder, + address indexed assetContract, + uint256 bidAmount, + IEnglishAuctions.Auction auction + ); + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), platformFeeRecipient, uint16(platformFeeBps)) + ) + ) + ); + + // Setup roles for seller and assets + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc1155)); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + vm.stopPrank(); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(buyer, "Buyer"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + + // Sample auction parameters. + address assetContract = address(erc1155); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 100 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100 minutes; + uint64 endTimestamp = 200 minutes; + + // Auction tokens. + auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + // Set bidAmount + bidAmount = auctionParams.minimumBidAmount; + + // Mint NFT to seller. + erc721.mint(seller, 1); // to, amount + erc1155.mint(seller, 0, 100); // to, id, amount + + // Create auction + vm.startPrank(seller); + erc721.setApprovalForAll(marketplace, true); + erc1155.setApprovalForAll(marketplace, true); + auctionId = EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + vm.stopPrank(); + + // Mint currency to bidder. + erc20.mint(buyer, 10_000 ether); + + vm.prank(buyer); + erc20.approve(marketplace, 100 ether); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `EnglishAuctions` + address englishAuctions = address(new EnglishAuctionsLogic(address(weth))); + defaultFeeRecipient = 0x1Af20C6B23373350aD464700B5965CE4B0D2aD94; + vm.label(englishAuctions, "EnglishAuctions_Extension"); + + // Extension: EnglishAuctionsLogic + Extension memory extension_englishAuctions; + extension_englishAuctions.metadata = ExtensionMetadata({ + name: "EnglishAuctionsLogic", + metadataURI: "ipfs://EnglishAuctions", + implementation: englishAuctions + }); + + extension_englishAuctions.functions = new ExtensionFunction[](12); + extension_englishAuctions.functions[0] = ExtensionFunction( + EnglishAuctionsLogic.totalAuctions.selector, + "totalAuctions()" + ); + extension_englishAuctions.functions[1] = ExtensionFunction( + EnglishAuctionsLogic.createAuction.selector, + "createAuction((address,uint256,uint256,address,uint256,uint256,uint64,uint64,uint64,uint64))" + ); + extension_englishAuctions.functions[2] = ExtensionFunction( + EnglishAuctionsLogic.cancelAuction.selector, + "cancelAuction(uint256)" + ); + extension_englishAuctions.functions[3] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionPayout.selector, + "collectAuctionPayout(uint256)" + ); + extension_englishAuctions.functions[4] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionTokens.selector, + "collectAuctionTokens(uint256)" + ); + extension_englishAuctions.functions[5] = ExtensionFunction( + EnglishAuctionsLogic.bidInAuction.selector, + "bidInAuction(uint256,uint256)" + ); + extension_englishAuctions.functions[6] = ExtensionFunction( + EnglishAuctionsLogic.isNewWinningBid.selector, + "isNewWinningBid(uint256,uint256)" + ); + extension_englishAuctions.functions[7] = ExtensionFunction( + EnglishAuctionsLogic.getAuction.selector, + "getAuction(uint256)" + ); + extension_englishAuctions.functions[8] = ExtensionFunction( + EnglishAuctionsLogic.getAllAuctions.selector, + "getAllAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[9] = ExtensionFunction( + EnglishAuctionsLogic.getAllValidAuctions.selector, + "getAllValidAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[10] = ExtensionFunction( + EnglishAuctionsLogic.getWinningBid.selector, + "getWinningBid(uint256)" + ); + extension_englishAuctions.functions[11] = ExtensionFunction( + EnglishAuctionsLogic.isAuctionExpired.selector, + "isAuctionExpired(uint256)" + ); + + extensions[0] = extension_englishAuctions; + } + + address payable[] internal mockRecipients; + uint256[] internal mockAmounts; + MockRoyaltyEngineV1 internal royaltyEngine; + + function _setupRoyaltyEngine() private { + mockRecipients.push(payable(address(0x12345))); + mockRecipients.push(payable(address(0x56789))); + + mockAmounts.push(10 ether); + mockAmounts.push(15 ether); + + royaltyEngine = new MockRoyaltyEngineV1(mockRecipients, mockAmounts); + } + + function test_payout_whenZeroRoyaltyRecipients() public { + vm.warp(auctionParams.startTimestamp); + vm.prank(buyer); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, auctionParams.buyoutBidAmount); + + vm.prank(seller); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + + uint256 totalPrice = auctionParams.buyoutBidAmount; + + uint256 platformFees = (totalPrice * platformFeeBps) / 10_000; + + { + uint256 defaultFee = (totalPrice * 100) / 10_000; + + // Platform fee recipient receives correct amount + assertBalERC20Eq(address(erc20), platformFeeRecipient, platformFees); + + // Seller gets total price minus royalty amounts + assertBalERC20Eq(address(erc20), seller, totalPrice - platformFees - defaultFee); + + assertBalERC20Eq(address(erc20), defaultFeeRecipient, defaultFee); + } + } + + modifier whenNonZeroRoyaltyRecipients() { + _setupRoyaltyEngine(); + + // Add RoyaltyEngine to marketplace + vm.prank(marketplaceDeployer); + RoyaltyPaymentsLogic(marketplace).setRoyaltyEngine(address(royaltyEngine)); + + _; + } + + function test_payout_whenInsufficientFundsToPayRoyaltyAfterPlatformFeePayout() public whenNonZeroRoyaltyRecipients { + vm.prank(marketplaceDeployer); + PlatformFee(marketplace).setPlatformFeeInfo(platformFeeRecipient, 9899); // 99.99% fees with 100 bps default; + + // Buy tokens from listing. + vm.warp(auctionParams.startTimestamp); + vm.prank(buyer); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, auctionParams.buyoutBidAmount); + + vm.prank(seller); + vm.expectRevert("fees exceed the price"); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + } + + function test_payout_whenSufficientFundsToPayRoyaltyAfterPlatformFeePayout() public whenNonZeroRoyaltyRecipients { + assertEq(RoyaltyPaymentsLogic(marketplace).getRoyaltyEngineAddress(), address(royaltyEngine)); + + vm.warp(auctionParams.startTimestamp); + vm.prank(buyer); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, auctionParams.buyoutBidAmount); + + vm.prank(seller); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + + uint256 totalPrice = auctionParams.buyoutBidAmount; + uint256 platformFees = (totalPrice * platformFeeBps) / 10_000; + + { + uint256 defaultFee = (totalPrice * 100) / 10_000; + + // Royalty recipients receive correct amounts + assertBalERC20Eq(address(erc20), mockRecipients[0], mockAmounts[0]); + assertBalERC20Eq(address(erc20), mockRecipients[1], mockAmounts[1]); + + // Platform fee recipient receives correct amount + assertBalERC20Eq(address(erc20), platformFeeRecipient, platformFees); + + // Seller gets total price minus royalty amounts + assertBalERC20Eq( + address(erc20), + seller, + totalPrice - mockAmounts[0] - mockAmounts[1] - platformFees - defaultFee + ); + + assertBalERC20Eq(address(erc20), defaultFeeRecipient, defaultFee); + } + } +} diff --git a/src/test/marketplace/english-auctions/_payout/_payout.tree b/src/test/marketplace/english-auctions/_payout/_payout.tree new file mode 100644 index 000000000..36b930e11 --- /dev/null +++ b/src/test/marketplace/english-auctions/_payout/_payout.tree @@ -0,0 +1,17 @@ +function _payout( + address _payer, + address _payee, + address _currencyToUse, + uint256 _totalPayoutAmount, + Auction memory _targetAuction +) +├── when there are zero royalty recipients ✅ +│ ├── it should transfer platform fee from payer to platform fee recipient +│ └── it should transfer remainder of currency from payer to payee +└── when there are non-zero royalty recipients + ├── when the total royalty payout exceeds remainder payout after having paid platform fee + │ └── it should revert ✅ + └── when the total royalty payout does not exceed remainder payout after having paid platform fee ✅ + ├── it should transfer platform fee from payer to platform fee recipient + ├── it should transfer royalty fee from payer to royalty recipients + └── it should transfer remainder of currency from payer to payeew \ No newline at end of file diff --git a/src/test/marketplace/english-auctions/_transferAuctionTokens/_transferAuctionTokens.t.sol b/src/test/marketplace/english-auctions/_transferAuctionTokens/_transferAuctionTokens.t.sol new file mode 100644 index 000000000..5c36a091d --- /dev/null +++ b/src/test/marketplace/english-auctions/_transferAuctionTokens/_transferAuctionTokens.t.sol @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test helper imports +import "../../../utils/BaseTest.sol"; + +// Test contracts and interfaces +import { RoyaltyPaymentsLogic } from "contracts/extension/plugin/RoyaltyPayments.sol"; +import { MarketplaceV3, IPlatformFee } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { EnglishAuctionsLogic } from "contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsLogic.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { ERC721Base } from "contracts/base/ERC721Base.sol"; +import { MockRoyaltyEngineV1 } from "../../../mocks/MockRoyaltyEngineV1.sol"; +import { PlatformFee } from "contracts/extension/PlatformFee.sol"; + +import { IEnglishAuctions } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +contract MockTransferAuctionTokens is EnglishAuctionsLogic { + constructor(address _nativeTokenWrapper) EnglishAuctionsLogic(_nativeTokenWrapper) {} + + function transferAuctionTokens(address _from, address _to, Auction memory _auction) external { + _transferAuctionTokens(_from, _to, _auction); + } +} + +contract TransferAuctionTokensTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + // Auction parameters + uint256 internal auctionId_erc1155; + uint256 internal auctionId_erc721; + uint256 internal bidAmount; + address internal winningBidder = address(0x123); + IEnglishAuctions.AuctionParameters internal auctionParams; + + // Events + event NewBid( + uint256 indexed auctionId, + address indexed bidder, + address indexed assetContract, + uint256 bidAmount, + IEnglishAuctions.Auction auction + ); + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), platformFeeRecipient, uint16(platformFeeBps)) + ) + ) + ); + + // Setup roles for seller and assets + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc1155)); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc721)); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + vm.stopPrank(); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(buyer, "Buyer"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + + // Sample auction parameters. + address assetContract = address(erc1155); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 100 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100 minutes; + uint64 endTimestamp = 200 minutes; + + // Auction tokens. + auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + // Set bidAmount + bidAmount = auctionParams.minimumBidAmount; + + // Mint NFT to seller. + erc721.mint(seller, 1); // to, amount + erc1155.mint(seller, 0, 100); // to, id, amount + + // Create auction + vm.startPrank(seller); + erc721.setApprovalForAll(marketplace, true); + erc1155.setApprovalForAll(marketplace, true); + + auctionId_erc1155 = EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + + auctionParams.assetContract = address(erc721); + auctionId_erc721 = EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + + vm.stopPrank(); + + // Mint currency to bidder. + erc20.mint(buyer, 10_000 ether); + + vm.prank(buyer); + erc20.approve(marketplace, 100 ether); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `EnglishAuctions` + address englishAuctions = address(new MockTransferAuctionTokens(address(weth))); + vm.label(englishAuctions, "EnglishAuctions_Extension"); + + // Extension: EnglishAuctionsLogic + Extension memory extension_englishAuctions; + extension_englishAuctions.metadata = ExtensionMetadata({ + name: "EnglishAuctionsLogic", + metadataURI: "ipfs://EnglishAuctions", + implementation: englishAuctions + }); + + extension_englishAuctions.functions = new ExtensionFunction[](3); + extension_englishAuctions.functions[0] = ExtensionFunction( + MockTransferAuctionTokens.transferAuctionTokens.selector, + "transferAuctionTokens(address,address,(uint256,uint256,uint256,uint256,uint256,uint64,uint64,uint64,uint64,address,address,address,uint8,uint8))" + ); + extension_englishAuctions.functions[1] = ExtensionFunction( + EnglishAuctionsLogic.createAuction.selector, + "createAuction((address,uint256,uint256,address,uint256,uint256,uint64,uint64,uint64,uint64))" + ); + extension_englishAuctions.functions[2] = ExtensionFunction( + EnglishAuctionsLogic.getAuction.selector, + "getAuction(uint256)" + ); + + extensions[0] = extension_englishAuctions; + } + + function test_transferAuctionTokens_erc1155() public { + IEnglishAuctions.Auction memory auction = EnglishAuctionsLogic(marketplace).getAuction(auctionId_erc1155); + + assertEq(erc1155.balanceOf(address(marketplace), auction.tokenId), 1); + assertEq(erc1155.balanceOf(buyer, auction.tokenId), 0); + + MockTransferAuctionTokens(marketplace).transferAuctionTokens(address(marketplace), buyer, auction); + + assertEq(erc1155.balanceOf(address(marketplace), auction.tokenId), 0); + assertEq(erc1155.balanceOf(buyer, auction.tokenId), 1); + } + + function test_transferAuctionTokens_erc721() public { + IEnglishAuctions.Auction memory auction = EnglishAuctionsLogic(marketplace).getAuction(auctionId_erc721); + + assertEq(erc721.ownerOf(auction.tokenId), address(marketplace)); + + MockTransferAuctionTokens(marketplace).transferAuctionTokens(address(marketplace), buyer, auction); + + assertEq(erc721.ownerOf(auction.tokenId), buyer); + } +} diff --git a/src/test/marketplace/english-auctions/_transferAuctionTokens/_transferAuctionTokens.tree b/src/test/marketplace/english-auctions/_transferAuctionTokens/_transferAuctionTokens.tree new file mode 100644 index 000000000..c44dc8c55 --- /dev/null +++ b/src/test/marketplace/english-auctions/_transferAuctionTokens/_transferAuctionTokens.tree @@ -0,0 +1,9 @@ +function _transferAuctionTokens( + address _from, + address _to, + Auction memory _auction +) +├── when the token is ERC1155 +│ └── it should transfer ERC1155 tokens from the specified owner to recipient +└── when the token is ERC721 + └── it should transfer ERC721 tokens from the specified owner to recipient \ No newline at end of file diff --git a/src/test/marketplace/english-auctions/_validateNewAuction/_validateNewAuction.t.sol b/src/test/marketplace/english-auctions/_validateNewAuction/_validateNewAuction.t.sol new file mode 100644 index 000000000..5f716875f --- /dev/null +++ b/src/test/marketplace/english-auctions/_validateNewAuction/_validateNewAuction.t.sol @@ -0,0 +1,304 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test helper imports +import "../../../utils/BaseTest.sol"; + +// Test contracts and interfaces +import { RoyaltyPaymentsLogic } from "contracts/extension/plugin/RoyaltyPayments.sol"; +import { MarketplaceV3, IPlatformFee } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { EnglishAuctionsLogic } from "contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsLogic.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { ERC721Base } from "contracts/base/ERC721Base.sol"; + +import { IEnglishAuctions } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +contract InvalidToken { + function supportsInterface(bytes4) public pure returns (bool) { + return false; + } +} + +contract ValidateNewAuctionTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + // Auction parameters + IEnglishAuctions.AuctionParameters internal auctionParams; + + // Events + /// @dev Emitted when a new auction is created. + event NewAuction( + address indexed auctionCreator, + uint256 indexed auctionId, + address indexed assetContract, + IEnglishAuctions.Auction auction + ); + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles for seller and assets + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc1155)); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc721)); + + vm.stopPrank(); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(buyer, "Buyer"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + + // Sample auction parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100; + uint64 endTimestamp = 200; + + // Auction tokens. + auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + // Mint NFT to seller. + erc721.mint(seller, 1); // to, amount + erc1155.mint(seller, 0, 100); // to, id, amount + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `EnglishAuctions` + address englishAuctions = address(new EnglishAuctionsLogic(address(weth))); + vm.label(englishAuctions, "EnglishAuctions_Extension"); + + // Extension: EnglishAuctionsLogic + Extension memory extension_englishAuctions; + extension_englishAuctions.metadata = ExtensionMetadata({ + name: "EnglishAuctionsLogic", + metadataURI: "ipfs://EnglishAuctions", + implementation: englishAuctions + }); + + extension_englishAuctions.functions = new ExtensionFunction[](12); + extension_englishAuctions.functions[0] = ExtensionFunction( + EnglishAuctionsLogic.totalAuctions.selector, + "totalAuctions()" + ); + extension_englishAuctions.functions[1] = ExtensionFunction( + EnglishAuctionsLogic.createAuction.selector, + "createAuction((address,uint256,uint256,address,uint256,uint256,uint64,uint64,uint64,uint64))" + ); + extension_englishAuctions.functions[2] = ExtensionFunction( + EnglishAuctionsLogic.cancelAuction.selector, + "cancelAuction(uint256)" + ); + extension_englishAuctions.functions[3] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionPayout.selector, + "collectAuctionPayout(uint256)" + ); + extension_englishAuctions.functions[4] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionTokens.selector, + "collectAuctionTokens(uint256)" + ); + extension_englishAuctions.functions[5] = ExtensionFunction( + EnglishAuctionsLogic.bidInAuction.selector, + "bidInAuction(uint256,uint256)" + ); + extension_englishAuctions.functions[6] = ExtensionFunction( + EnglishAuctionsLogic.isNewWinningBid.selector, + "isNewWinningBid(uint256,uint256)" + ); + extension_englishAuctions.functions[7] = ExtensionFunction( + EnglishAuctionsLogic.getAuction.selector, + "getAuction(uint256)" + ); + extension_englishAuctions.functions[8] = ExtensionFunction( + EnglishAuctionsLogic.getAllAuctions.selector, + "getAllAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[9] = ExtensionFunction( + EnglishAuctionsLogic.getAllValidAuctions.selector, + "getAllValidAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[10] = ExtensionFunction( + EnglishAuctionsLogic.getWinningBid.selector, + "getWinningBid(uint256)" + ); + extension_englishAuctions.functions[11] = ExtensionFunction( + EnglishAuctionsLogic.isAuctionExpired.selector, + "isAuctionExpired(uint256)" + ); + + extensions[0] = extension_englishAuctions; + } + + function test_validateNewAuction_whenQuantityIsZero() public { + auctionParams.quantity = 0; + + vm.prank(seller); + vm.expectRevert("Marketplace: auctioning zero quantity."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + modifier whenNonZeroQuantity() { + auctionParams.quantity = 1; + _; + } + + function test_validateNewAuction_whenQuantityGtOneAndAssetERC721() public whenNonZeroQuantity { + auctionParams.quantity = 2; + auctionParams.assetContract = address(erc721); + + vm.prank(seller); + vm.expectRevert("Marketplace: auctioning invalid quantity."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + modifier whenQtyOneOrAssetERC1155() { + auctionParams.quantity = 1; + auctionParams.assetContract = address(erc721); + _; + } + + function test_validateNewAuction_whenTimeBufferIsZero() public whenNonZeroQuantity whenQtyOneOrAssetERC1155 { + auctionParams.timeBufferInSeconds = 0; + + vm.prank(seller); + vm.expectRevert("Marketplace: no time-buffer."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + modifier whenNonZeroTimeBuffer() { + _; + } + + function test_validateNewAuction_whenBidBufferIsZero() + public + whenNonZeroQuantity + whenQtyOneOrAssetERC1155 + whenNonZeroTimeBuffer + { + auctionParams.bidBufferBps = 0; + + vm.prank(seller); + vm.expectRevert("Marketplace: no bid-buffer."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + modifier whenNonZeroBidBuffer() { + _; + } + + function test_validateNewAuction_whenInvalidTimestamps() + public + whenNonZeroQuantity + whenQtyOneOrAssetERC1155 + whenNonZeroTimeBuffer + whenNonZeroBidBuffer + { + vm.warp(auctionParams.startTimestamp + 61 minutes); + + vm.prank(seller); + vm.expectRevert("Marketplace: invalid timestamps."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + + vm.warp(auctionParams.startTimestamp); + + auctionParams.endTimestamp = auctionParams.startTimestamp - 1; + + vm.prank(seller); + vm.expectRevert("Marketplace: invalid timestamps."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + modifier whenValidTimestamps() { + _; + } + + function test_validateNewAuction_whenBuyoutLtMinimumBidAmt() + public + whenNonZeroQuantity + whenQtyOneOrAssetERC1155 + whenNonZeroTimeBuffer + whenNonZeroBidBuffer + whenValidTimestamps + { + auctionParams.buyoutBidAmount = auctionParams.minimumBidAmount - 1; + + vm.prank(seller); + vm.expectRevert("Marketplace: invalid bid amounts."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + modifier whenBuyoutGteMinimumBidAmt() { + _; + } + + function test_validateNewAuction_buyoutGteMinimumBidAmt() + public + whenNonZeroQuantity + whenQtyOneOrAssetERC1155 + whenNonZeroTimeBuffer + whenNonZeroBidBuffer + whenValidTimestamps + whenBuyoutGteMinimumBidAmt + { + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + vm.prank(seller); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + + assertEq(EnglishAuctionsLogic(marketplace).totalAuctions(), 1); + } +} diff --git a/src/test/marketplace/english-auctions/_validateNewAuction/_validateNewAuction.tree b/src/test/marketplace/english-auctions/_validateNewAuction/_validateNewAuction.tree new file mode 100644 index 000000000..24429d6c5 --- /dev/null +++ b/src/test/marketplace/english-auctions/_validateNewAuction/_validateNewAuction.tree @@ -0,0 +1,20 @@ +function _validateNewAuction(AuctionParameters memory _params, TokenType _tokenType) internal view +. +├── when quantity is zero +│ └── it should revert ✅ +└── when the quantity is non zero + ├── when the quantity is greater than one and token type is ERC721 + │ └── it should revert ✅ + └── when the quantity is one or token type is ERC1155 + ├── when the time buffer is zero + │ └── it should revert ✅ + └── when the time buffer is non zero + ├── when the bid buffer is zero + │ └── it should revert ✅ + └── when the bid buffer is non zero + ├── when start and end timestamps are invalid + │ └── it should revert ✅ + └── when start and end timestamps are valid + ├── when buyout amount is less than minimum bid amount + │ └── it should revert ✅ + └── when buyout amount is zero or gte minimum bid amount ✅ \ No newline at end of file diff --git a/src/test/marketplace/english-auctions/bidInAuction/bidInAuction.t.sol b/src/test/marketplace/english-auctions/bidInAuction/bidInAuction.t.sol new file mode 100644 index 000000000..ad7041106 --- /dev/null +++ b/src/test/marketplace/english-auctions/bidInAuction/bidInAuction.t.sol @@ -0,0 +1,683 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test helper imports +import "../../../utils/BaseTest.sol"; + +// Test contracts and interfaces +import { RoyaltyPaymentsLogic } from "contracts/extension/plugin/RoyaltyPayments.sol"; +import { MarketplaceV3, IPlatformFee } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { EnglishAuctionsLogic } from "contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsLogic.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { ERC721Base } from "contracts/base/ERC721Base.sol"; + +import { IEnglishAuctions } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +contract ReentrantRecipient is ERC1155Holder { + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes memory data + ) public virtual override returns (bytes4) { + uint256 auctionId = 0; + uint256 bidAmount = 10 ether; + EnglishAuctionsLogic(msg.sender).bidInAuction(auctionId, bidAmount); + return super.onERC1155Received(operator, from, id, value, data); + } +} + +contract BidInAuctionTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + // Auction parameters + uint256 internal auctionId; + uint256 internal bidAmount; + address internal winningBidder = address(0x123); + IEnglishAuctions.AuctionParameters internal auctionParams; + + // Events + event NewBid( + uint256 indexed auctionId, + address indexed bidder, + address indexed assetContract, + uint256 bidAmount, + IEnglishAuctions.Auction auction + ); + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles for seller and assets + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc1155)); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + vm.stopPrank(); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(buyer, "Buyer"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + + // Sample auction parameters. + address assetContract = address(erc1155); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100 minutes; + uint64 endTimestamp = 200 minutes; + + // Auction tokens. + auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + // Set bidAmount + bidAmount = auctionParams.minimumBidAmount; + + // Mint NFT to seller. + erc721.mint(seller, 1); // to, amount + erc1155.mint(seller, 0, 100); // to, id, amount + + // Create auction + vm.startPrank(seller); + erc721.setApprovalForAll(marketplace, true); + erc1155.setApprovalForAll(marketplace, true); + auctionId = EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + vm.stopPrank(); + + // Mint currency to bidder. + erc20.mint(buyer, 10_000 ether); + + vm.prank(buyer); + erc20.approve(marketplace, 100 ether); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `EnglishAuctions` + address englishAuctions = address(new EnglishAuctionsLogic(address(weth))); + vm.label(englishAuctions, "EnglishAuctions_Extension"); + + // Extension: EnglishAuctionsLogic + Extension memory extension_englishAuctions; + extension_englishAuctions.metadata = ExtensionMetadata({ + name: "EnglishAuctionsLogic", + metadataURI: "ipfs://EnglishAuctions", + implementation: englishAuctions + }); + + extension_englishAuctions.functions = new ExtensionFunction[](12); + extension_englishAuctions.functions[0] = ExtensionFunction( + EnglishAuctionsLogic.totalAuctions.selector, + "totalAuctions()" + ); + extension_englishAuctions.functions[1] = ExtensionFunction( + EnglishAuctionsLogic.createAuction.selector, + "createAuction((address,uint256,uint256,address,uint256,uint256,uint64,uint64,uint64,uint64))" + ); + extension_englishAuctions.functions[2] = ExtensionFunction( + EnglishAuctionsLogic.cancelAuction.selector, + "cancelAuction(uint256)" + ); + extension_englishAuctions.functions[3] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionPayout.selector, + "collectAuctionPayout(uint256)" + ); + extension_englishAuctions.functions[4] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionTokens.selector, + "collectAuctionTokens(uint256)" + ); + extension_englishAuctions.functions[5] = ExtensionFunction( + EnglishAuctionsLogic.bidInAuction.selector, + "bidInAuction(uint256,uint256)" + ); + extension_englishAuctions.functions[6] = ExtensionFunction( + EnglishAuctionsLogic.isNewWinningBid.selector, + "isNewWinningBid(uint256,uint256)" + ); + extension_englishAuctions.functions[7] = ExtensionFunction( + EnglishAuctionsLogic.getAuction.selector, + "getAuction(uint256)" + ); + extension_englishAuctions.functions[8] = ExtensionFunction( + EnglishAuctionsLogic.getAllAuctions.selector, + "getAllAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[9] = ExtensionFunction( + EnglishAuctionsLogic.getAllValidAuctions.selector, + "getAllValidAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[10] = ExtensionFunction( + EnglishAuctionsLogic.getWinningBid.selector, + "getWinningBid(uint256)" + ); + extension_englishAuctions.functions[11] = ExtensionFunction( + EnglishAuctionsLogic.isAuctionExpired.selector, + "isAuctionExpired(uint256)" + ); + + extensions[0] = extension_englishAuctions; + } + + function test_bidInAuction_callIsReentrant() public { + vm.warp(auctionParams.startTimestamp + 1); + address reentrantRecipient = address(new ReentrantRecipient()); + + erc20.mint(reentrantRecipient, 100 ether); + + vm.prank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), reentrantRecipient); + + vm.startPrank(reentrantRecipient); + erc20.approve(marketplace, 100 ether); + vm.expectRevert(); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, auctionParams.buyoutBidAmount); + vm.stopPrank(); + } + + modifier whenCallIsNotReentrant() { + _; + } + + function test_bidInAuction_whenAuctionDoesNotExist() public whenCallIsNotReentrant { + vm.prank(buyer); + vm.expectRevert("Marketplace: invalid auction."); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId + 1, bidAmount); + } + + modifier whenAuctionExists() { + _; + } + + function test_bidInAuction_whenAuctionIsNotActive() public whenCallIsNotReentrant whenAuctionExists { + vm.prank(buyer); + vm.expectRevert("Marketplace: inactive auction."); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, bidAmount); + } + + modifier whenAuctionIsActive() { + vm.warp(auctionParams.startTimestamp + 1); + _; + } + + function test_bidInAuction_whenBidAmountIsZero() + public + whenCallIsNotReentrant + whenAuctionExists + whenAuctionIsActive + { + vm.prank(buyer); + vm.expectRevert("Marketplace: Bidding with zero amount."); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, 0); + } + + modifier whenBidAmountIsNotZero() { + bidAmount = auctionParams.minimumBidAmount; + _; + } + + function test_bidInAuction_whenAuctionCurrencyIsERC20AndMsgValueSent() + public + whenCallIsNotReentrant + whenAuctionExists + whenAuctionIsActive + whenBidAmountIsNotZero + { + vm.deal(buyer, 1 ether); + + vm.prank(buyer); + vm.expectRevert("Marketplace: invalid native tokens sent."); + EnglishAuctionsLogic(marketplace).bidInAuction{ value: 1 }(auctionId, auctionParams.buyoutBidAmount); + } + + function test_bidInAuction_whenBidAmountIsGtBuyoutPrice() + public + whenCallIsNotReentrant + whenAuctionExists + whenAuctionIsActive + whenBidAmountIsNotZero + { + vm.prank(buyer); + vm.expectRevert("Marketplace: Bidding above buyout price."); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, auctionParams.buyoutBidAmount + 1); + } + + modifier whenBidAmountLtBuyoutPrice() { + bidAmount = auctionParams.buyoutBidAmount - 1; + _; + } + + modifier whenBidAmountEqBuyoutPrice() { + bidAmount = auctionParams.buyoutBidAmount; + _; + } + + modifier whenCurrentWinningBid() { + // Existing winning bid. + erc20.mint(winningBidder, 100 ether); + + vm.prank(winningBidder); + erc20.approve(marketplace, 100 ether); + + vm.prank(winningBidder); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, auctionParams.minimumBidAmount + 1); + _; + } + + modifier whenNoCurrentWinningBid() { + _; + } + + function test_bidInAuction_buyoutAndExistingWinningBid() + public + whenCallIsNotReentrant + whenAuctionExists + whenAuctionIsActive + whenBidAmountIsNotZero + whenBidAmountEqBuyoutPrice + whenCurrentWinningBid + { + uint256 winningBidderBal = erc20.balanceOf(winningBidder); + + assertEq(erc20.balanceOf(marketplace), auctionParams.minimumBidAmount + 1); + + assertEq(erc1155.balanceOf(marketplace, auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(seller, auctionParams.tokenId), 99); + + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + + vm.prank(buyer); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, bidAmount); + + assertEq(erc20.balanceOf(winningBidder), winningBidderBal + auctionParams.minimumBidAmount + 1); + assertEq(erc20.balanceOf(marketplace), bidAmount); + + assertEq(erc1155.balanceOf(marketplace, auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(seller, auctionParams.tokenId), 99); + + // Auction is marked CLOSED in auction state when creator collected payout + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + } + + function test_bidInAuction_buyoutAndNoWinningBid() + public + whenCallIsNotReentrant + whenAuctionExists + whenAuctionIsActive + whenBidAmountIsNotZero + whenBidAmountEqBuyoutPrice + whenNoCurrentWinningBid + { + assertEq(erc20.balanceOf(marketplace), 0); + + assertEq(erc1155.balanceOf(marketplace, auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(seller, auctionParams.tokenId), 99); + + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + + vm.prank(buyer); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, bidAmount); + + assertEq(erc20.balanceOf(marketplace), bidAmount); + + assertEq(erc1155.balanceOf(marketplace, auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(seller, auctionParams.tokenId), 99); + + // Auction is marked CLOSED in auction state when creator collected payout + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + } + + function test_bidInAuction_whenBidIsNotNewWinningBid() + public + whenCallIsNotReentrant + whenAuctionExists + whenAuctionIsActive + whenCurrentWinningBid + whenBidAmountIsNotZero + whenBidAmountLtBuyoutPrice + { + assertEq(EnglishAuctionsLogic(marketplace).isNewWinningBid(auctionId, auctionParams.minimumBidAmount), false); + + vm.prank(buyer); + vm.expectRevert("Marketplace: not winning bid."); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, auctionParams.minimumBidAmount); + } + + modifier whenBidIsNewWinningBig() { + bidAmount = auctionParams.buyoutBidAmount - 1; + assertEq(EnglishAuctionsLogic(marketplace).isNewWinningBid(auctionId, bidAmount), true); + _; + } + + modifier whenBidWithinTimeBuffer() { + vm.warp(auctionParams.endTimestamp - auctionParams.timeBufferInSeconds); + _; + } + + function test_bidInAuction_noBuyoutAndExistingWinningBid() + public + whenCallIsNotReentrant + whenAuctionExists + whenAuctionIsActive + whenBidAmountIsNotZero + whenBidAmountLtBuyoutPrice + whenBidIsNewWinningBig + whenCurrentWinningBid + { + uint256 winningBidderBal = erc20.balanceOf(winningBidder); + + assertEq(erc20.balanceOf(marketplace), auctionParams.minimumBidAmount + 1); + + assertEq(erc1155.balanceOf(marketplace, auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(seller, auctionParams.tokenId), 99); + + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + + (address bidderBefore, address currencyBefore, uint256 bidAmountBefore) = EnglishAuctionsLogic(marketplace) + .getWinningBid(auctionId); + + assertEq(bidderBefore, winningBidder); + assertEq(currencyBefore, address(erc20)); + assertEq(bidAmountBefore, auctionParams.minimumBidAmount + 1); + + assertEq(EnglishAuctionsLogic(marketplace).isNewWinningBid(auctionId, bidAmount), true); + + vm.startPrank(buyer); + vm.expectEmit(true, true, true, false); + emit NewBid( + auctionId, + buyer, + address(erc1155), + bidAmount, + EnglishAuctionsLogic(marketplace).getAuction(auctionId) + ); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, bidAmount); + vm.stopPrank(); + + (address bidderAfter, address currencyAfter, uint256 bidAmountAfter) = EnglishAuctionsLogic(marketplace) + .getWinningBid(auctionId); + + assertEq(bidderAfter, buyer); + assertEq(currencyAfter, address(erc20)); + assertEq(bidAmountAfter, bidAmount); + + assertEq(erc20.balanceOf(winningBidder), winningBidderBal + auctionParams.minimumBidAmount + 1); + assertEq(erc20.balanceOf(marketplace), bidAmount); + + assertEq(erc1155.balanceOf(marketplace, auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(seller, auctionParams.tokenId), 99); + + // Auction is marked CLOSED in auction state when creator collected payout + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + } + + function test_bidInAuction_noBuyoutAndNoWinningBid() + public + whenCallIsNotReentrant + whenAuctionExists + whenAuctionIsActive + whenBidAmountIsNotZero + whenBidAmountLtBuyoutPrice + whenBidIsNewWinningBig + whenNoCurrentWinningBid + { + assertEq(erc20.balanceOf(marketplace), 0); + + assertEq(erc1155.balanceOf(marketplace, auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(seller, auctionParams.tokenId), 99); + + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + + (address bidderBefore, address currencyBefore, uint256 bidAmountBefore) = EnglishAuctionsLogic(marketplace) + .getWinningBid(auctionId); + + assertEq(bidderBefore, address(0)); + assertEq(currencyBefore, address(erc20)); + assertEq(bidAmountBefore, 0); + + vm.startPrank(buyer); + vm.expectEmit(true, true, true, false); + emit NewBid( + auctionId, + buyer, + address(erc1155), + bidAmount, + EnglishAuctionsLogic(marketplace).getAuction(auctionId) + ); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, bidAmount); + vm.stopPrank(); + + (address bidderAfter, address currencyAfter, uint256 bidAmountAfter) = EnglishAuctionsLogic(marketplace) + .getWinningBid(auctionId); + + assertEq(bidderAfter, buyer); + assertEq(currencyAfter, address(erc20)); + assertEq(bidAmountAfter, bidAmount); + + assertEq(erc20.balanceOf(marketplace), bidAmount); + + assertEq(erc1155.balanceOf(marketplace, auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(seller, auctionParams.tokenId), 99); + + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + } + + function test_bidInAuction_noBuyoutAndExistingWinningBid_withinTimeBuffer() + public + whenCallIsNotReentrant + whenAuctionExists + whenAuctionIsActive + whenBidAmountIsNotZero + whenBidAmountLtBuyoutPrice + whenBidIsNewWinningBig + whenCurrentWinningBid + whenBidWithinTimeBuffer + { + uint256 winningBidderBal = erc20.balanceOf(winningBidder); + + assertEq(erc20.balanceOf(marketplace), auctionParams.minimumBidAmount + 1); + + assertEq(erc1155.balanceOf(marketplace, auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(seller, auctionParams.tokenId), 99); + + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + + (address bidderBefore, address currencyBefore, uint256 bidAmountBefore) = EnglishAuctionsLogic(marketplace) + .getWinningBid(auctionId); + + assertEq(bidderBefore, winningBidder); + assertEq(currencyBefore, address(erc20)); + assertEq(bidAmountBefore, auctionParams.minimumBidAmount + 1); + + assertEq(EnglishAuctionsLogic(marketplace).getAuction(auctionId).endTimestamp, auctionParams.endTimestamp); + + vm.startPrank(buyer); + vm.expectEmit(true, true, true, false); + emit NewBid( + auctionId, + buyer, + address(erc1155), + bidAmount, + EnglishAuctionsLogic(marketplace).getAuction(auctionId) + ); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, bidAmount); + vm.stopPrank(); + + assertEq( + EnglishAuctionsLogic(marketplace).getAuction(auctionId).endTimestamp, + auctionParams.endTimestamp + auctionParams.timeBufferInSeconds + ); + + (address bidderAfter, address currencyAfter, uint256 bidAmountAfter) = EnglishAuctionsLogic(marketplace) + .getWinningBid(auctionId); + + assertEq(bidderAfter, buyer); + assertEq(currencyAfter, address(erc20)); + assertEq(bidAmountAfter, bidAmount); + + assertEq(erc20.balanceOf(winningBidder), winningBidderBal + auctionParams.minimumBidAmount + 1); + assertEq(erc20.balanceOf(marketplace), bidAmount); + + assertEq(erc1155.balanceOf(marketplace, auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(seller, auctionParams.tokenId), 99); + + // Auction is marked CLOSED in auction state when creator collected payout + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + } + + function test_bidInAuction_noBuyoutAndNoWinningBid_withinTimeBuffer() + public + whenCallIsNotReentrant + whenAuctionExists + whenAuctionIsActive + whenBidAmountIsNotZero + whenBidAmountLtBuyoutPrice + whenBidIsNewWinningBig + whenBidWithinTimeBuffer + whenNoCurrentWinningBid + { + assertEq(erc20.balanceOf(marketplace), 0); + + assertEq(erc1155.balanceOf(marketplace, auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(seller, auctionParams.tokenId), 99); + + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + + (address bidderBefore, address currencyBefore, uint256 bidAmountBefore) = EnglishAuctionsLogic(marketplace) + .getWinningBid(auctionId); + + assertEq(bidderBefore, address(0)); + assertEq(currencyBefore, address(erc20)); + assertEq(bidAmountBefore, 0); + + assertEq(EnglishAuctionsLogic(marketplace).getAuction(auctionId).endTimestamp, auctionParams.endTimestamp); + + vm.startPrank(buyer); + vm.expectEmit(true, true, true, false); + emit NewBid( + auctionId, + buyer, + address(erc1155), + bidAmount, + EnglishAuctionsLogic(marketplace).getAuction(auctionId) + ); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, bidAmount); + vm.stopPrank(); + + assertEq( + EnglishAuctionsLogic(marketplace).getAuction(auctionId).endTimestamp, + auctionParams.endTimestamp + auctionParams.timeBufferInSeconds + ); + + (address bidderAfter, address currencyAfter, uint256 bidAmountAfter) = EnglishAuctionsLogic(marketplace) + .getWinningBid(auctionId); + + assertEq(bidderAfter, buyer); + assertEq(currencyAfter, address(erc20)); + assertEq(bidAmountAfter, bidAmount); + + assertEq(erc20.balanceOf(marketplace), bidAmount); + + assertEq(erc1155.balanceOf(marketplace, auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(seller, auctionParams.tokenId), 99); + + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + } +} diff --git a/src/test/marketplace/english-auctions/bidInAuction/bidInAuction.tree b/src/test/marketplace/english-auctions/bidInAuction/bidInAuction.tree new file mode 100644 index 000000000..bcb182035 --- /dev/null +++ b/src/test/marketplace/english-auctions/bidInAuction/bidInAuction.tree @@ -0,0 +1,52 @@ +function bidInAuction(uint256 _auctionId, uint256 _bidAmount) +├── when the call is reentrant +│ └── it should revert ✅ +└── when the call is not reentrant + ├── when the auction does not exist + │ └── it should revert ✅ + └── when the auction exists + ├── when the auction is not active + │ └── it should revert ✅ + └── when the auction is active + ├── when the bid amount is zero + │ └── it should revert ✅ + └── when the bid amount is not zero + ├── when the bid amount is greater than buyout price + │ └── it should revert ✅ + └── when the bid amount is less than or equal to buyout price + ├── when the bid amount is equal to buyout price + │ ├── when there is a current winning bid ✅ + │ │ ├── it should transfer previous winning bid back to previous winning bidder + │ │ ├── it should transfer auctioned tokens to bidder + │ │ ├── it should escrow incoming bid + │ │ └── it should emit a NewBid event + │ └── when there is no current winning bid ✅ + │ ├── it should transfer auctioned tokens to bidder + │ ├── it should escrow incoming bid + │ └── it should emit a NewBid event + └── when the bid amount is less than buyout price + ├── when the bid is not a new winning bid + │ └── it should revert ✅ + └── when the bid is a new winning bid + ├── when the remaining auction duration is less than time buffer + │ ├── when there is a current winning bid ✅ + │ │ ├── it should add time buffer to auction duration + │ │ ├── it should transfer previous winning bid back to previous winning bidder + │ │ ├── it should escrow incoming bid + │ │ └── it should emit a NewBid event + │ │ └── it set auction status as completed + │ └── when there is no current winning bid ✅ + │ ├── it should add time buffer to auction duration + │ ├── it should escrow incoming bid + │ └── it should emit a NewBid event + │ └── it set auction status as completed + └── when the remaining auction duration is not less than time buffer + ├── when there is a current winning bid ✅ + │ ├── it should transfer previous winning bid back to previous winning bidder + │ ├── it should escrow incoming bid + │ └── it should emit a NewBid event + │ └── it set auction status as completed + └── when there is no current winning bid ✅ + ├── it should escrow incoming bid + └── it should emit a NewBid event + └── it set auction status as completed \ No newline at end of file diff --git a/src/test/marketplace/english-auctions/cancelAuction/cancelAuction.t.sol b/src/test/marketplace/english-auctions/cancelAuction/cancelAuction.t.sol new file mode 100644 index 000000000..aa69aacb2 --- /dev/null +++ b/src/test/marketplace/english-auctions/cancelAuction/cancelAuction.t.sol @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test helper imports +import "../../../utils/BaseTest.sol"; + +// Test contracts and interfaces +import { RoyaltyPaymentsLogic } from "contracts/extension/plugin/RoyaltyPayments.sol"; +import { MarketplaceV3, IPlatformFee } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { EnglishAuctionsLogic } from "contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsLogic.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { ERC721Base } from "contracts/base/ERC721Base.sol"; + +import { IEnglishAuctions } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +contract ReentrantRecipient is ERC1155Holder { + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes memory data + ) public virtual override returns (bytes4) { + uint256 auctionId = 0; + uint256 bidAmount = 10 ether; + EnglishAuctionsLogic(msg.sender).bidInAuction(auctionId, bidAmount); + return super.onERC1155Received(operator, from, id, value, data); + } +} + +contract CancelAuctionTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + // Auction parameters + uint256 internal auctionId; + uint256 internal bidAmount; + address internal winningBidder = address(0x123); + IEnglishAuctions.AuctionParameters internal auctionParams; + + // Events + event CancelledAuction(address indexed auctionCreator, uint256 indexed auctionId); + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles for seller and assets + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc1155)); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + vm.stopPrank(); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(buyer, "Buyer"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + + // Sample auction parameters. + address assetContract = address(erc1155); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100 minutes; + uint64 endTimestamp = 200 minutes; + + // Auction tokens. + auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + // Set bidAmount + bidAmount = auctionParams.minimumBidAmount; + + // Mint NFT to seller. + erc721.mint(seller, 1); // to, amount + erc1155.mint(seller, 0, 100); // to, id, amount + + // Create auction + vm.startPrank(seller); + erc721.setApprovalForAll(marketplace, true); + erc1155.setApprovalForAll(marketplace, true); + auctionId = EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + vm.stopPrank(); + + // Mint currency to bidder. + erc20.mint(buyer, 10_000 ether); + + vm.prank(buyer); + erc20.approve(marketplace, 100 ether); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `EnglishAuctions` + address englishAuctions = address(new EnglishAuctionsLogic(address(weth))); + vm.label(englishAuctions, "EnglishAuctions_Extension"); + + // Extension: EnglishAuctionsLogic + Extension memory extension_englishAuctions; + extension_englishAuctions.metadata = ExtensionMetadata({ + name: "EnglishAuctionsLogic", + metadataURI: "ipfs://EnglishAuctions", + implementation: englishAuctions + }); + + extension_englishAuctions.functions = new ExtensionFunction[](12); + extension_englishAuctions.functions[0] = ExtensionFunction( + EnglishAuctionsLogic.totalAuctions.selector, + "totalAuctions()" + ); + extension_englishAuctions.functions[1] = ExtensionFunction( + EnglishAuctionsLogic.createAuction.selector, + "createAuction((address,uint256,uint256,address,uint256,uint256,uint64,uint64,uint64,uint64))" + ); + extension_englishAuctions.functions[2] = ExtensionFunction( + EnglishAuctionsLogic.cancelAuction.selector, + "cancelAuction(uint256)" + ); + extension_englishAuctions.functions[3] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionPayout.selector, + "collectAuctionPayout(uint256)" + ); + extension_englishAuctions.functions[4] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionTokens.selector, + "collectAuctionTokens(uint256)" + ); + extension_englishAuctions.functions[5] = ExtensionFunction( + EnglishAuctionsLogic.bidInAuction.selector, + "bidInAuction(uint256,uint256)" + ); + extension_englishAuctions.functions[6] = ExtensionFunction( + EnglishAuctionsLogic.isNewWinningBid.selector, + "isNewWinningBid(uint256,uint256)" + ); + extension_englishAuctions.functions[7] = ExtensionFunction( + EnglishAuctionsLogic.getAuction.selector, + "getAuction(uint256)" + ); + extension_englishAuctions.functions[8] = ExtensionFunction( + EnglishAuctionsLogic.getAllAuctions.selector, + "getAllAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[9] = ExtensionFunction( + EnglishAuctionsLogic.getAllValidAuctions.selector, + "getAllValidAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[10] = ExtensionFunction( + EnglishAuctionsLogic.getWinningBid.selector, + "getWinningBid(uint256)" + ); + extension_englishAuctions.functions[11] = ExtensionFunction( + EnglishAuctionsLogic.isAuctionExpired.selector, + "isAuctionExpired(uint256)" + ); + + extensions[0] = extension_englishAuctions; + } + + function test_cancelAuction_whenAuctionDoesntExist() public { + vm.prank(seller); + vm.expectRevert("Marketplace: invalid auction."); + EnglishAuctionsLogic(marketplace).cancelAuction(auctionId + 100); + } + + modifier whenAuctionExists() { + _; + } + + function test_cancelAuction_whenCallerNotCreator() public whenAuctionExists { + vm.prank(buyer); + vm.expectRevert("Marketplace: not auction creator."); + EnglishAuctionsLogic(marketplace).cancelAuction(auctionId); + } + + modifier whenCallerIsCreator() { + _; + } + + function test_cancelAuction_whenWinningBid() public whenAuctionExists whenCallerIsCreator { + vm.warp(auctionParams.startTimestamp + 1); + + vm.prank(buyer); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, bidAmount); + + vm.prank(seller); + vm.expectRevert("Marketplace: bids already made."); + EnglishAuctionsLogic(marketplace).cancelAuction(auctionId); + } + + modifier whenNoWinningBid() { + _; + } + + function test_cancelAuction_whenNoWinningBid() public whenAuctionExists whenCallerIsCreator whenNoWinningBid { + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + assertEq(erc1155.balanceOf(address(marketplace), 0), 1); + assertEq(erc1155.balanceOf(seller, 0), 99); + + vm.prank(seller); + vm.expectEmit(true, true, true, true); + emit CancelledAuction(seller, auctionId); + EnglishAuctionsLogic(marketplace).cancelAuction(auctionId); + + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CANCELLED) + ); + + assertEq(erc1155.balanceOf(address(marketplace), 0), 0); + assertEq(erc1155.balanceOf(seller, 0), 100); + } +} diff --git a/src/test/marketplace/english-auctions/cancelAuction/cancelAuction.tree b/src/test/marketplace/english-auctions/cancelAuction/cancelAuction.tree new file mode 100644 index 000000000..397d6bc6d --- /dev/null +++ b/src/test/marketplace/english-auctions/cancelAuction/cancelAuction.tree @@ -0,0 +1,14 @@ +function cancelAuction(uint256 _auctionId) external +. +├── when auction does not exist +│ └── it should revert ✅ +└── when auction exists + ├── when the caller is not auction creator + │ └── it should revert ✅ + └── when the caller is auction creator + ├── when there is a winning bidder + │ └── it should revert ✅ + └── when there is no winning bidder ✅ + ├── it should set auction status as cancelled + ├── it should transfer auction tokens back to creator + └── it should emit CancelledAuction event diff --git a/src/test/marketplace/english-auctions/collectAuctionPayout/collectAuctionPayout.t.sol b/src/test/marketplace/english-auctions/collectAuctionPayout/collectAuctionPayout.t.sol new file mode 100644 index 000000000..fe88a681a --- /dev/null +++ b/src/test/marketplace/english-auctions/collectAuctionPayout/collectAuctionPayout.t.sol @@ -0,0 +1,304 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test helper imports +import "../../../utils/BaseTest.sol"; + +// Test contracts and interfaces +import { RoyaltyPaymentsLogic } from "contracts/extension/plugin/RoyaltyPayments.sol"; +import { MarketplaceV3, IPlatformFee } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { EnglishAuctionsLogic } from "contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsLogic.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { ERC721Base } from "contracts/base/ERC721Base.sol"; + +import { IEnglishAuctions } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +contract ReentrantRecipient is ERC1155Holder { + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes memory data + ) public virtual override returns (bytes4) { + uint256 auctionId = 0; + uint256 bidAmount = 10 ether; + EnglishAuctionsLogic(msg.sender).bidInAuction(auctionId, bidAmount); + return super.onERC1155Received(operator, from, id, value, data); + } +} + +contract CollectAuctionPayoutTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + address private defaultFeeRecipient; + + // Auction parameters + uint256 internal auctionId; + address internal winningBidder = address(0x123); + IEnglishAuctions.AuctionParameters internal auctionParams; + + // Events + event AuctionClosed( + uint256 indexed auctionId, + address indexed assetContract, + address indexed closer, + uint256 tokenId, + address auctionCreator, + address winningBidder + ); + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles for seller and assets + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc1155)); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + vm.stopPrank(); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(buyer, "Buyer"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + + // Sample auction parameters. + address assetContract = address(erc1155); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100 minutes; + uint64 endTimestamp = 200 minutes; + + // Auction tokens. + auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + // Mint NFT to seller. + erc721.mint(seller, 1); // to, amount + erc1155.mint(seller, 0, 100); // to, id, amount + + // Create auction + vm.startPrank(seller); + erc721.setApprovalForAll(marketplace, true); + erc1155.setApprovalForAll(marketplace, true); + auctionId = EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + vm.stopPrank(); + + // Mint currency to bidder. + erc20.mint(buyer, 10_000 ether); + + vm.prank(buyer); + erc20.approve(marketplace, 100 ether); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `EnglishAuctions` + address englishAuctions = address(new EnglishAuctionsLogic(address(weth))); + defaultFeeRecipient = 0x1Af20C6B23373350aD464700B5965CE4B0D2aD94; + vm.label(englishAuctions, "EnglishAuctions_Extension"); + + // Extension: EnglishAuctionsLogic + Extension memory extension_englishAuctions; + extension_englishAuctions.metadata = ExtensionMetadata({ + name: "EnglishAuctionsLogic", + metadataURI: "ipfs://EnglishAuctions", + implementation: englishAuctions + }); + + extension_englishAuctions.functions = new ExtensionFunction[](12); + extension_englishAuctions.functions[0] = ExtensionFunction( + EnglishAuctionsLogic.totalAuctions.selector, + "totalAuctions()" + ); + extension_englishAuctions.functions[1] = ExtensionFunction( + EnglishAuctionsLogic.createAuction.selector, + "createAuction((address,uint256,uint256,address,uint256,uint256,uint64,uint64,uint64,uint64))" + ); + extension_englishAuctions.functions[2] = ExtensionFunction( + EnglishAuctionsLogic.cancelAuction.selector, + "cancelAuction(uint256)" + ); + extension_englishAuctions.functions[3] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionPayout.selector, + "collectAuctionPayout(uint256)" + ); + extension_englishAuctions.functions[4] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionTokens.selector, + "collectAuctionTokens(uint256)" + ); + extension_englishAuctions.functions[5] = ExtensionFunction( + EnglishAuctionsLogic.bidInAuction.selector, + "bidInAuction(uint256,uint256)" + ); + extension_englishAuctions.functions[6] = ExtensionFunction( + EnglishAuctionsLogic.isNewWinningBid.selector, + "isNewWinningBid(uint256,uint256)" + ); + extension_englishAuctions.functions[7] = ExtensionFunction( + EnglishAuctionsLogic.getAuction.selector, + "getAuction(uint256)" + ); + extension_englishAuctions.functions[8] = ExtensionFunction( + EnglishAuctionsLogic.getAllAuctions.selector, + "getAllAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[9] = ExtensionFunction( + EnglishAuctionsLogic.getAllValidAuctions.selector, + "getAllValidAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[10] = ExtensionFunction( + EnglishAuctionsLogic.getWinningBid.selector, + "getWinningBid(uint256)" + ); + extension_englishAuctions.functions[11] = ExtensionFunction( + EnglishAuctionsLogic.isAuctionExpired.selector, + "isAuctionExpired(uint256)" + ); + + extensions[0] = extension_englishAuctions; + } + + function test_collectAuctionPayout_whenAuctionIsCancelled() public { + vm.prank(seller); + EnglishAuctionsLogic(marketplace).cancelAuction(auctionId); + + vm.prank(seller); + vm.expectRevert("Marketplace: invalid auction."); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + } + + modifier whenAuctionNotCancelled() { + _; + } + + function test_collectAuctionPayout_whenAuctionIsActive() public whenAuctionNotCancelled { + vm.warp(auctionParams.startTimestamp + 1); + + vm.prank(seller); + vm.expectRevert("Marketplace: auction still active."); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + } + + modifier whenAuctionHasEnded() { + vm.warp(auctionParams.endTimestamp + 1); + _; + } + + function test_collectAuctionPayout_whenNoWinningBid() public whenAuctionNotCancelled whenAuctionHasEnded { + vm.warp(auctionParams.endTimestamp + 1); + + vm.prank(seller); + vm.expectRevert("Marketplace: no bids were made."); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + } + + modifier whenAuctionHasWinningBid() { + vm.warp(auctionParams.startTimestamp + 1); + + // Bid in auction + vm.prank(buyer); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, auctionParams.minimumBidAmount); + _; + } + + function test_collectAuctionPayout_whenAuctionTokensAlreadyPaidOut() + public + whenAuctionNotCancelled + whenAuctionHasWinningBid + whenAuctionHasEnded + { + vm.prank(seller); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + + vm.prank(seller); + vm.expectRevert("Marketplace: payout already completed."); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + } + + modifier whenAuctionTokensNotPaidOut() { + _; + } + + function test_collectAuctionPayout_whenAuctionTokensNotYetPaidOut() + public + whenAuctionNotCancelled + whenAuctionHasWinningBid + whenAuctionHasEnded + whenAuctionTokensNotPaidOut + { + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + + uint256 marketplaceBal = erc20.balanceOf(address(marketplace)); + assertEq(marketplaceBal, auctionParams.minimumBidAmount); + assertEq(erc20.balanceOf(seller), 0); + + vm.prank(seller); + vm.expectEmit(true, true, true, true); + emit AuctionClosed(auctionId, address(erc1155), seller, auctionParams.tokenId, seller, buyer); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.COMPLETED) + ); + + uint256 defaultFee = (marketplaceBal * 100) / 10_000; + + assertEq(erc20.balanceOf(address(marketplace)), 0); + assertEq(erc20.balanceOf(seller), marketplaceBal - defaultFee); + assertEq(erc20.balanceOf(defaultFeeRecipient), defaultFee); + } +} diff --git a/src/test/marketplace/english-auctions/collectAuctionPayout/collectAuctionPayout.tree b/src/test/marketplace/english-auctions/collectAuctionPayout/collectAuctionPayout.tree new file mode 100644 index 000000000..571fd4a61 --- /dev/null +++ b/src/test/marketplace/english-auctions/collectAuctionPayout/collectAuctionPayout.tree @@ -0,0 +1,17 @@ +function collectAuctionPayout(uint256 _auctionId) external +. +├── when auction is cancelled +│ └── it should revert ✅ +└── when auction is not cancelled + ├── when auction has not ended + │ └── it should revert ✅ + └── when auction has ended + ├── when there is no winning bid + │ └── it should revert ✅ + └── when there is a winning bid + ├── when creator already paid out + │ └── it should revert ✅ + └── when creator not already paid out ✅ + ├── it should set auction status to completed + ├── it should pay the auction winning bid to creator + └── it should emit AuctionClosed event diff --git a/src/test/marketplace/english-auctions/collectAuctionTokens/collectAuctionTokens.t.sol b/src/test/marketplace/english-auctions/collectAuctionTokens/collectAuctionTokens.t.sol new file mode 100644 index 000000000..11b451d67 --- /dev/null +++ b/src/test/marketplace/english-auctions/collectAuctionTokens/collectAuctionTokens.t.sol @@ -0,0 +1,298 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test helper imports +import "../../../utils/BaseTest.sol"; + +// Test contracts and interfaces +import { RoyaltyPaymentsLogic } from "contracts/extension/plugin/RoyaltyPayments.sol"; +import { MarketplaceV3, IPlatformFee } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { EnglishAuctionsLogic } from "contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsLogic.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { ERC721Base } from "contracts/base/ERC721Base.sol"; + +import { IEnglishAuctions } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +contract ReentrantRecipient is ERC1155Holder { + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes memory data + ) public virtual override returns (bytes4) { + uint256 auctionId = 0; + uint256 bidAmount = 10 ether; + EnglishAuctionsLogic(msg.sender).bidInAuction(auctionId, bidAmount); + return super.onERC1155Received(operator, from, id, value, data); + } +} + +contract CollectAuctionTokensTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + // Auction parameters + uint256 internal auctionId; + address internal winningBidder = address(0x123); + IEnglishAuctions.AuctionParameters internal auctionParams; + + // Events + event AuctionClosed( + uint256 indexed auctionId, + address indexed assetContract, + address indexed closer, + uint256 tokenId, + address auctionCreator, + address winningBidder + ); + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles for seller and assets + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc1155)); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + vm.stopPrank(); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(buyer, "Buyer"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + + // Sample auction parameters. + address assetContract = address(erc1155); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100 minutes; + uint64 endTimestamp = 200 minutes; + + // Auction tokens. + auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + // Mint NFT to seller. + erc721.mint(seller, 1); // to, amount + erc1155.mint(seller, 0, 100); // to, id, amount + + // Create auction + vm.startPrank(seller); + erc721.setApprovalForAll(marketplace, true); + erc1155.setApprovalForAll(marketplace, true); + auctionId = EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + vm.stopPrank(); + + // Mint currency to bidder. + erc20.mint(buyer, 10_000 ether); + + vm.prank(buyer); + erc20.approve(marketplace, 100 ether); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `EnglishAuctions` + address englishAuctions = address(new EnglishAuctionsLogic(address(weth))); + vm.label(englishAuctions, "EnglishAuctions_Extension"); + + // Extension: EnglishAuctionsLogic + Extension memory extension_englishAuctions; + extension_englishAuctions.metadata = ExtensionMetadata({ + name: "EnglishAuctionsLogic", + metadataURI: "ipfs://EnglishAuctions", + implementation: englishAuctions + }); + + extension_englishAuctions.functions = new ExtensionFunction[](12); + extension_englishAuctions.functions[0] = ExtensionFunction( + EnglishAuctionsLogic.totalAuctions.selector, + "totalAuctions()" + ); + extension_englishAuctions.functions[1] = ExtensionFunction( + EnglishAuctionsLogic.createAuction.selector, + "createAuction((address,uint256,uint256,address,uint256,uint256,uint64,uint64,uint64,uint64))" + ); + extension_englishAuctions.functions[2] = ExtensionFunction( + EnglishAuctionsLogic.cancelAuction.selector, + "cancelAuction(uint256)" + ); + extension_englishAuctions.functions[3] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionPayout.selector, + "collectAuctionPayout(uint256)" + ); + extension_englishAuctions.functions[4] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionTokens.selector, + "collectAuctionTokens(uint256)" + ); + extension_englishAuctions.functions[5] = ExtensionFunction( + EnglishAuctionsLogic.bidInAuction.selector, + "bidInAuction(uint256,uint256)" + ); + extension_englishAuctions.functions[6] = ExtensionFunction( + EnglishAuctionsLogic.isNewWinningBid.selector, + "isNewWinningBid(uint256,uint256)" + ); + extension_englishAuctions.functions[7] = ExtensionFunction( + EnglishAuctionsLogic.getAuction.selector, + "getAuction(uint256)" + ); + extension_englishAuctions.functions[8] = ExtensionFunction( + EnglishAuctionsLogic.getAllAuctions.selector, + "getAllAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[9] = ExtensionFunction( + EnglishAuctionsLogic.getAllValidAuctions.selector, + "getAllValidAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[10] = ExtensionFunction( + EnglishAuctionsLogic.getWinningBid.selector, + "getWinningBid(uint256)" + ); + extension_englishAuctions.functions[11] = ExtensionFunction( + EnglishAuctionsLogic.isAuctionExpired.selector, + "isAuctionExpired(uint256)" + ); + + extensions[0] = extension_englishAuctions; + } + + function test_collectAuctionTokens_whenAuctionIsCancelled() public { + vm.prank(seller); + EnglishAuctionsLogic(marketplace).cancelAuction(auctionId); + + vm.prank(buyer); + vm.expectRevert("Marketplace: invalid auction."); + EnglishAuctionsLogic(marketplace).collectAuctionTokens(auctionId); + } + + modifier whenAuctionNotCancelled() { + _; + } + + function test_collectAuctionTokens_whenAuctionIsActive() public whenAuctionNotCancelled { + vm.warp(auctionParams.startTimestamp + 1); + + vm.prank(buyer); + vm.expectRevert("Marketplace: auction still active."); + EnglishAuctionsLogic(marketplace).collectAuctionTokens(auctionId); + } + + modifier whenAuctionHasEnded() { + vm.warp(auctionParams.endTimestamp + 1); + _; + } + + function test_collectAuctionTokens_whenNoWinningBid() public whenAuctionNotCancelled whenAuctionHasEnded { + vm.warp(auctionParams.endTimestamp + 1); + + vm.prank(buyer); + vm.expectRevert("Marketplace: no bids were made."); + EnglishAuctionsLogic(marketplace).collectAuctionTokens(auctionId); + } + + modifier whenAuctionHasWinningBid() { + vm.warp(auctionParams.startTimestamp + 1); + + // Bid in auction + vm.prank(buyer); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, auctionParams.minimumBidAmount); + _; + } + + function test_collectAuctionTokens_whenAuctionTokensAlreadyPaidOut() + public + whenAuctionNotCancelled + whenAuctionHasWinningBid + whenAuctionHasEnded + { + vm.prank(buyer); + EnglishAuctionsLogic(marketplace).collectAuctionTokens(auctionId); + + vm.prank(buyer); + vm.expectRevert("Marketplace: payout already completed."); + EnglishAuctionsLogic(marketplace).collectAuctionTokens(auctionId); + } + + modifier whenAuctionTokensNotPaidOut() { + _; + } + + function test_collectAuctionTokens_whenAuctionTokensNotYetPaidOut() + public + whenAuctionNotCancelled + whenAuctionHasWinningBid + whenAuctionHasEnded + whenAuctionTokensNotPaidOut + { + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + + assertEq(erc1155.balanceOf(address(marketplace), auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 0); + + vm.prank(buyer); + vm.expectEmit(true, true, true, true); + emit AuctionClosed(auctionId, address(erc1155), buyer, auctionParams.tokenId, seller, buyer); + EnglishAuctionsLogic(marketplace).collectAuctionTokens(auctionId); + + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.COMPLETED) + ); + assertEq(EnglishAuctionsLogic(marketplace).getAuction(auctionId).endTimestamp, uint64(block.timestamp)); + + assertEq(erc1155.balanceOf(address(marketplace), auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 1); + } +} diff --git a/src/test/marketplace/english-auctions/collectAuctionTokens/collectAuctionTokens.tree b/src/test/marketplace/english-auctions/collectAuctionTokens/collectAuctionTokens.tree new file mode 100644 index 000000000..d54bd1ba3 --- /dev/null +++ b/src/test/marketplace/english-auctions/collectAuctionTokens/collectAuctionTokens.tree @@ -0,0 +1,18 @@ +function collectAuctionTokens(uint256 _auctionId) external +. +├── when the auction cancelled +│ └── it reverts ✅ +└── when the auction is not cancelled + ├── when the auction is still active + │ └── it should reverts ✅ + └── when the auction is not active + ├── when the auction has no wining bid + │ └── it should reverts ✅ + └── when the auction has a wining bid + ├── when auction bidder has already been paid out tokens + │ └── it should reverts ✅ + └── when auction creator has not been paid out tokens ✅ + ├── it should set auction timestamp to block timestamp + ├── it should set auction state to completed + ├── it should transfer auction tokens to bidder + └── it should emit AuctionClosed event with auction ID, asset contract, caller, tokenId, creator, bidder \ No newline at end of file diff --git a/src/test/marketplace/english-auctions/createAuction/createAuction.t.sol b/src/test/marketplace/english-auctions/createAuction/createAuction.t.sol new file mode 100644 index 000000000..8bc759b31 --- /dev/null +++ b/src/test/marketplace/english-auctions/createAuction/createAuction.t.sol @@ -0,0 +1,318 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test helper imports +import "../../../utils/BaseTest.sol"; + +// Test contracts and interfaces +import { RoyaltyPaymentsLogic } from "contracts/extension/plugin/RoyaltyPayments.sol"; +import { MarketplaceV3, IPlatformFee } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { EnglishAuctionsLogic } from "contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsLogic.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { ERC721Base } from "contracts/base/ERC721Base.sol"; + +import { IEnglishAuctions } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +contract InvalidToken { + function supportsInterface(bytes4) public pure returns (bool) { + return false; + } +} + +contract CreateAuctionTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + // Auction parameters + IEnglishAuctions.AuctionParameters internal auctionParams; + + // Events + /// @dev Emitted when a new auction is created. + event NewAuction( + address indexed auctionCreator, + uint256 indexed auctionId, + address indexed assetContract, + IEnglishAuctions.Auction auction + ); + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles for seller and assets + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + + vm.stopPrank(); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(buyer, "Buyer"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + + // Sample auction parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100; + uint64 endTimestamp = 200; + + // Auction tokens. + auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + // Mint NFT to seller. + erc721.mint(seller, 1); // to, amount + erc1155.mint(seller, 0, 100); // to, id, amount + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `EnglishAuctions` + address englishAuctions = address(new EnglishAuctionsLogic(address(weth))); + vm.label(englishAuctions, "EnglishAuctions_Extension"); + + // Extension: EnglishAuctionsLogic + Extension memory extension_englishAuctions; + extension_englishAuctions.metadata = ExtensionMetadata({ + name: "EnglishAuctionsLogic", + metadataURI: "ipfs://EnglishAuctions", + implementation: englishAuctions + }); + + extension_englishAuctions.functions = new ExtensionFunction[](12); + extension_englishAuctions.functions[0] = ExtensionFunction( + EnglishAuctionsLogic.totalAuctions.selector, + "totalAuctions()" + ); + extension_englishAuctions.functions[1] = ExtensionFunction( + EnglishAuctionsLogic.createAuction.selector, + "createAuction((address,uint256,uint256,address,uint256,uint256,uint64,uint64,uint64,uint64))" + ); + extension_englishAuctions.functions[2] = ExtensionFunction( + EnglishAuctionsLogic.cancelAuction.selector, + "cancelAuction(uint256)" + ); + extension_englishAuctions.functions[3] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionPayout.selector, + "collectAuctionPayout(uint256)" + ); + extension_englishAuctions.functions[4] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionTokens.selector, + "collectAuctionTokens(uint256)" + ); + extension_englishAuctions.functions[5] = ExtensionFunction( + EnglishAuctionsLogic.bidInAuction.selector, + "bidInAuction(uint256,uint256)" + ); + extension_englishAuctions.functions[6] = ExtensionFunction( + EnglishAuctionsLogic.isNewWinningBid.selector, + "isNewWinningBid(uint256,uint256)" + ); + extension_englishAuctions.functions[7] = ExtensionFunction( + EnglishAuctionsLogic.getAuction.selector, + "getAuction(uint256)" + ); + extension_englishAuctions.functions[8] = ExtensionFunction( + EnglishAuctionsLogic.getAllAuctions.selector, + "getAllAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[9] = ExtensionFunction( + EnglishAuctionsLogic.getAllValidAuctions.selector, + "getAllValidAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[10] = ExtensionFunction( + EnglishAuctionsLogic.getWinningBid.selector, + "getWinningBid(uint256)" + ); + extension_englishAuctions.functions[11] = ExtensionFunction( + EnglishAuctionsLogic.isAuctionExpired.selector, + "isAuctionExpired(uint256)" + ); + + extensions[0] = extension_englishAuctions; + } + + function test_createAuction_whenCallerDoesntHaveListerRole() public { + assertEq(Permissions(marketplace).hasRole(keccak256("LISTER_ROLE"), seller), false); + + vm.prank(seller); + vm.expectRevert("!LISTER_ROLE"); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + modifier whenCallerHasListerRole() { + vm.prank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + _; + } + + function test_createAuction_whenAssetDoesnHaveAssetRole() public whenCallerHasListerRole { + assertEq(Permissions(marketplace).hasRole(keccak256("ASSET_ROLE"), address(erc721)), false); + + vm.prank(seller); + vm.expectRevert("!ASSET_ROLE"); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + modifier whenAssetHasAssetRole() { + vm.prank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc721)); + _; + } + + function test_createAuction_whenTokenIsInvalid() public whenCallerHasListerRole whenAssetHasAssetRole { + address newToken = address(new InvalidToken()); + + vm.prank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), newToken); + + auctionParams.assetContract = newToken; + + vm.prank(seller); + vm.expectRevert("Marketplace: auctioned token must be ERC1155 or ERC721."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + modifier whenTokenIsValid() { + _; + } + + function test_createAuction_whenAuctionParamsAreInvalid() + public + whenCallerHasListerRole + whenAssetHasAssetRole + whenTokenIsValid + { + // This is one way for params to be invalid. `_validateNewAuction` has its own tests. + auctionParams.quantity = 0; + + vm.prank(seller); + vm.expectRevert("Marketplace: auctioning zero quantity."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + modifier whenAuctionParamsAreValid() { + _; + } + + function test_createAuction_whenAuctionParamsAreValid() + public + whenCallerHasListerRole + whenAssetHasAssetRole + whenTokenIsValid + whenAuctionParamsAreValid + { + uint256 expectedAuctionId = 0; + + assertEq(EnglishAuctionsLogic(marketplace).totalAuctions(), 0); + assertEq(erc721.ownerOf(0), seller); + assertEq(EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).assetContract, address(0)); + + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + IEnglishAuctions.Auction memory dummyAuction; + + vm.prank(seller); + vm.expectEmit(true, true, true, false); + emit NewAuction(seller, expectedAuctionId, auctionParams.assetContract, dummyAuction); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + + assertEq(EnglishAuctionsLogic(marketplace).totalAuctions(), 1); + assertEq(erc721.ownerOf(0), marketplace); + assertEq(EnglishAuctionsLogic(marketplace).getAllAuctions(0, 0).length, 1); + + assertEq(EnglishAuctionsLogic(marketplace).getAllValidAuctions(0, 0).length, 0); + vm.warp(auctionParams.startTimestamp); + assertEq(EnglishAuctionsLogic(marketplace).getAllValidAuctions(0, 0).length, 1); + + assertEq(EnglishAuctionsLogic(marketplace).getAllAuctions(0, 0).length, 1); + assertEq(EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).auctionId, expectedAuctionId); + assertEq( + EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).assetContract, + auctionParams.assetContract + ); + assertEq(EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).tokenId, auctionParams.tokenId); + assertEq( + EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).minimumBidAmount, + auctionParams.minimumBidAmount + ); + assertEq( + EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).buyoutBidAmount, + auctionParams.buyoutBidAmount + ); + assertEq( + EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).timeBufferInSeconds, + auctionParams.timeBufferInSeconds + ); + assertEq( + EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).bidBufferBps, + auctionParams.bidBufferBps + ); + assertEq( + EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).startTimestamp, + auctionParams.startTimestamp + ); + assertEq( + EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).endTimestamp, + auctionParams.endTimestamp + ); + assertEq(EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).auctionCreator, seller); + assertEq(EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).currency, auctionParams.currency); + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).tokenType), + uint256(IEnglishAuctions.TokenType.ERC721) + ); + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + } +} diff --git a/src/test/marketplace/english-auctions/createAuction/createAuction.tree b/src/test/marketplace/english-auctions/createAuction/createAuction.tree new file mode 100644 index 000000000..dcfa71e4c --- /dev/null +++ b/src/test/marketplace/english-auctions/createAuction/createAuction.tree @@ -0,0 +1,13 @@ +function createAuction(AuctionParameters calldata _params) +├── when the caller does not have LISTER_ROLE +│ └── it should revert ✅ +└── when the caller has LISTER_ROLE + ├── when the asset does not have ASSET_ROLE + │ └── it should revert ✅ + └── when the asset has ASSET_ROLE + ├── when the auction params are invalid + │ └── it should revert ✅ + └── when the auction params are valid ✅ + ├── it should create the intended auction + ├── it should escrow asset to auction + └── it should emit an AuctionCreated event with auction creator, auction ID, asset contract, auction data \ No newline at end of file diff --git a/src/test/minimal-factory/MinimalFactory.t.sol b/src/test/minimal-factory/MinimalFactory.t.sol new file mode 100644 index 000000000..a819ee55b --- /dev/null +++ b/src/test/minimal-factory/MinimalFactory.t.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "contracts/infra/TWMinimalFactory.sol"; +import "contracts/infra/TWProxy.sol"; + +import "@openzeppelin/contracts/proxy/Clones.sol"; + +import "../utils/BaseTest.sol"; + +contract DummyUpgradeable { + uint256 public number; + + constructor() {} + + function initialize(uint256 _num) public { + number = _num; + } +} + +contract TWNotMinimalFactory { + /// @dev Deploys a proxy that points to the given implementation. + function deployProxyByImplementation(address _implementation, bytes memory _data, bytes32 _salt) public { + address deployedProxy = Clones.cloneDeterministic(_implementation, _salt); + + if (_data.length > 0) { + // slither-disable-next-line unused-return + Address.functionCall(deployedProxy, _data); + } + } +} + +contract MinimalFactoryTest is BaseTest { + address internal implementation; + bytes32 internal salt; + bytes internal data; + address admin; + + TWNotMinimalFactory notMinimal; + + function setUp() public override { + super.setUp(); + admin = getActor(5000); + vm.startPrank(admin); + implementation = getContract("TokenERC20"); + salt = keccak256("yooo"); + data = abi.encodeWithSelector( + TokenERC20.initialize.selector, + admin, + "MinimalToken", + "MT", + "ipfs://notCentralized", + new address[](0), + admin, + admin, + 50 + ); + + notMinimal = new TWNotMinimalFactory(); + } + + // gas: Baseline + 140k + function test_gas_twProxy() public { + new TWProxy(implementation, data); + } + + // gas: Baseline + 41.5k + function test_gas_notMinimalFactory() public { + notMinimal.deployProxyByImplementation(implementation, data, salt); + } + + // gas: Baseline + function test_gas_minimal() public { + new TWMinimalFactory(implementation, data, salt); + } + + function test_verify_deployedProxy() public { + vm.stopPrank(); + vm.prank(address(0x123456)); + address minimalFactory = address(new TWMinimalFactory(implementation, data, salt)); + bytes32 salthash = keccak256(abi.encodePacked(address(0x123456), salt)); + address deployedProxy = Clones.predictDeterministicAddress(implementation, salthash, minimalFactory); + + bytes32 contractType = TokenERC20(deployedProxy).contractType(); + assertEq(contractType, bytes32("TokenERC20")); + } +} diff --git a/contracts/mock/Mock.sol b/src/test/mocks/Mock.sol similarity index 92% rename from contracts/mock/Mock.sol rename to src/test/mocks/Mock.sol index 4957f7251..6948d05fb 100644 --- a/contracts/mock/Mock.sol +++ b/src/test/mocks/Mock.sol @@ -1,8 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.11; +/// @author thirdweb + import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "contracts/eip/interface/IERC721.sol"; import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; /* diff --git a/src/test/mocks/MockContractPublisher.sol b/src/test/mocks/MockContractPublisher.sol new file mode 100644 index 000000000..3ce6a09f4 --- /dev/null +++ b/src/test/mocks/MockContractPublisher.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +import "contracts/infra/interface/IContractPublisher.sol"; + +// solhint-disable const-name-snakecase +contract MockContractPublisher is IContractPublisher { + function getAllPublishedContracts( + address + ) external pure override returns (CustomContractInstance[] memory published) { + CustomContractInstance[] memory mocks = new CustomContractInstance[](1); + mocks[0] = CustomContractInstance( + "MockContract", + 123, + "ipfs://mock", + 0x0000000000000000000000000000000000000000000000000000000000000001, + address(0x0000000000000000000000000000000000000000) + ); + return mocks; + } + + function getPublishedContractVersions( + address, + string memory + ) external pure returns (CustomContractInstance[] memory published) { + return new CustomContractInstance[](0); + } + + function getPublishedContract( + address, + string memory + ) external pure returns (CustomContractInstance memory published) { + return CustomContractInstance("", 0, "", "", address(0)); + } + + function publishContract( + address publisher, + string memory contractId, + string memory publishMetadataUri, + string memory compilerMetadataUri, + bytes32 bytecodeHash, + address implementation + ) external {} + + function unpublishContract(address publisher, string memory contractId) external {} + + function setPublisherProfileUri(address, string memory) external {} + + function getPublisherProfileUri(address) external pure returns (string memory uri) { + return ""; + } + + function getPublishedUriFromCompilerUri( + string memory + ) external pure returns (string[] memory publishedMetadataUris) { + return new string[](0); + } +} diff --git a/src/test/mocks/MockERC1155.sol b/src/test/mocks/MockERC1155.sol index 3ffccce5e..c1af0dc5a 100644 --- a/src/test/mocks/MockERC1155.sol +++ b/src/test/mocks/MockERC1155.sol @@ -6,11 +6,7 @@ import "@openzeppelin/contracts/token/ERC1155/presets/ERC1155PresetMinterPauser. contract MockERC1155 is ERC1155PresetMinterPauser { constructor() ERC1155PresetMinterPauser("ipfs://BaseURI") {} - function mint( - address to, - uint256 id, - uint256 amount - ) public virtual { + function mint(address to, uint256 id, uint256 amount) public virtual { _mint(to, id, amount, ""); } @@ -18,13 +14,13 @@ contract MockERC1155 is ERC1155PresetMinterPauser { return true; } - function mintBatch( - address to, - uint256[] memory ids, - uint256[] memory amounts - ) public virtual { + function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts) public virtual { require(hasRole(MINTER_ROLE, _msgSender()), "ERC1155PresetMinterPauser: must have minter role to mint"); _mintBatch(to, ids, amounts, ""); } + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return super.supportsInterface(interfaceId); + } } diff --git a/src/test/mocks/MockERC1155NonBurnable.sol b/src/test/mocks/MockERC1155NonBurnable.sol new file mode 100644 index 000000000..dff355c7f --- /dev/null +++ b/src/test/mocks/MockERC1155NonBurnable.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; + +contract MockERC1155NonBurnable is ERC1155 { + constructor() ERC1155("ipfs://BaseURI") {} + + function mint(address to, uint256 id, uint256 amount) public virtual { + _mint(to, id, amount, ""); + } + + function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts) public virtual { + _mintBatch(to, ids, amounts, ""); + } + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return super.supportsInterface(interfaceId); + } +} diff --git a/src/test/mocks/MockERC20.sol b/src/test/mocks/MockERC20.sol index e73e6910a..6b3a5bb8a 100644 --- a/src/test/mocks/MockERC20.sol +++ b/src/test/mocks/MockERC20.sol @@ -6,12 +6,27 @@ import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol" import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol"; contract MockERC20 is ERC20PresetMinterPauser, ERC20Permit { + bool internal taxActive; + constructor() ERC20PresetMinterPauser("Mock Coin", "MOCK") ERC20Permit("Mock Coin") {} function mint(address to, uint256 amount) public override(ERC20PresetMinterPauser) { _mint(to, amount); } + function toggleTax() external { + taxActive = !taxActive; + } + + function _transfer(address from, address to, uint256 amount) internal override { + if (taxActive) { + uint256 tax = (amount * 10) / 100; + amount -= tax; + super._transfer(from, address(this), tax); + } + super._transfer(from, to, amount); + } + function _beforeTokenTransfer( address from, address to, diff --git a/src/test/mocks/MockERC20CustomDecimals.sol b/src/test/mocks/MockERC20CustomDecimals.sol new file mode 100644 index 000000000..65ad6a20a --- /dev/null +++ b/src/test/mocks/MockERC20CustomDecimals.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol"; + +contract MockERC20CustomDecimals is ERC20PresetMinterPauser, ERC20Permit { + uint8 private immutable _decimals; + + constructor(uint8 decimals_) ERC20PresetMinterPauser("Mock Coin", "MOCK") ERC20Permit("Mock Coin") { + _decimals = decimals_; + } + + function mint(address to, uint256 amount) public override(ERC20PresetMinterPauser) { + _mint(to, amount); + } + + function decimals() public view override returns (uint8) { + return _decimals; + } + + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal override(ERC20PresetMinterPauser, ERC20) { + super._beforeTokenTransfer(from, to, amount); + } +} diff --git a/src/test/mocks/MockERC20NonCompliant.sol b/src/test/mocks/MockERC20NonCompliant.sol new file mode 100644 index 000000000..0e938b1de --- /dev/null +++ b/src/test/mocks/MockERC20NonCompliant.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +contract MockERC20NonCompliant { + mapping(address => uint256) private _balances; + mapping(address => mapping(address => uint256)) private _allowances; + uint256 private _totalSupply; + + constructor() {} + + function decimals() public view virtual returns (uint8) { + return 18; + } + + function totalSupply() public view virtual returns (uint256) { + return _totalSupply; + } + + function balanceOf(address account) public view virtual returns (uint256) { + return _balances[account]; + } + + function allowance(address owner, address spender) public view virtual returns (uint256) { + return _allowances[owner][spender]; + } + + function approve(address spender, uint256 amount) public virtual returns (bool) { + address owner = msg.sender; + _approve(owner, spender, amount); + return true; + } + + // non-compliant ERC20 as transfer doesn't return a bool + function transfer(address to, uint256 amount) public virtual { + address owner = msg.sender; + _transfer(owner, to, amount); + } + + // non-compliant ERC20 as transferFrom doesn't return a bool + function transferFrom(address from, address to, uint256 amount) public { + address spender = msg.sender; + _spendAllowance(from, spender, amount); + _transfer(from, to, amount); + } + + function _transfer(address from, address to, uint256 amount) internal virtual { + require(from != address(0), "ERC20: transfer from the zero address"); + require(to != address(0), "ERC20: transfer to the zero address"); + + uint256 fromBalance = _balances[from]; + require(fromBalance >= amount, "ERC20: transfer amount exceeds balance"); + unchecked { + _balances[from] = fromBalance - amount; + } + _balances[to] += amount; + } + + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _totalSupply += amount; + unchecked { + // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above. + _balances[account] += amount; + } + } + + function mint(address to, uint256 amount) public { + _mint(to, amount); + } + + function _approve(address owner, address spender, uint256 amount) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + } + + function _spendAllowance(address owner, address spender, uint256 amount) internal virtual { + uint256 currentAllowance = allowance(owner, spender); + if (currentAllowance != type(uint256).max) { + require(currentAllowance >= amount, "ERC20: insufficient allowance"); + unchecked { + _approve(owner, spender, currentAllowance - amount); + } + } + } +} diff --git a/src/test/mocks/MockERC721.sol b/src/test/mocks/MockERC721.sol index c567a6ac1..68cf3d14c 100644 --- a/src/test/mocks/MockERC721.sol +++ b/src/test/mocks/MockERC721.sol @@ -17,4 +17,8 @@ contract MockERC721 is ERC721Burnable { tokenId += 1; } } + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return super.supportsInterface(interfaceId); + } } diff --git a/src/test/mocks/MockERC721NonBurnable.sol b/src/test/mocks/MockERC721NonBurnable.sol new file mode 100644 index 000000000..4668d2e75 --- /dev/null +++ b/src/test/mocks/MockERC721NonBurnable.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +contract MockERC721NonBurnable is ERC721 { + uint256 public nextTokenIdToMint; + + constructor() ERC721("MockERC721", "MOCK") {} + + function mint(address _receiver, uint256 _amount) external { + uint256 tokenId = nextTokenIdToMint; + nextTokenIdToMint += _amount; + + for (uint256 i = 0; i < _amount; i += 1) { + _mint(_receiver, tokenId); + tokenId += 1; + } + } + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return super.supportsInterface(interfaceId); + } +} diff --git a/src/test/mocks/MockRoyaltyEngineV1.sol b/src/test/mocks/MockRoyaltyEngineV1.sol new file mode 100644 index 000000000..429c86f41 --- /dev/null +++ b/src/test/mocks/MockRoyaltyEngineV1.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "contracts/extension/interface/IRoyaltyEngineV1.sol"; +import { IERC2981 } from "contracts/eip/interface/IERC2981.sol"; +import { ERC165 } from "contracts/eip/ERC165.sol"; + +contract MockRoyaltyEngineV1 is ERC165, IRoyaltyEngineV1 { + address payable[] public mockRecipients; + uint256[] public mockAmounts; + + constructor(address payable[] memory _mockRecipients, uint256[] memory _mockAmounts) { + mockRecipients = _mockRecipients; + mockAmounts = _mockAmounts; + } + + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { + return interfaceId == type(IRoyaltyEngineV1).interfaceId || super.supportsInterface(interfaceId); + } + + function getRoyalty( + address tokenAddress, + uint256 tokenId, + uint256 value + ) public view override returns (address payable[] memory recipients, uint256[] memory amounts) { + try IERC2981(tokenAddress).royaltyInfo(tokenId, value) returns (address recipient, uint256 amount) { + // Supports EIP2981. Return amounts + recipients = new address payable[](1); + amounts = new uint256[](1); + recipients[0] = payable(recipient); + amounts[0] = amount; + return (recipients, amounts); + } catch {} + + // Non ERC2981. Return mock recipients/amounts. + recipients = mockRecipients; + amounts = mockAmounts; + return (recipients, amounts); + } + + function getRoyaltyView( + address tokenAddress, + uint256 tokenId, + uint256 value + ) public view override returns (address payable[] memory recipients, uint256[] memory amounts) {} +} diff --git a/src/test/mocks/MockThirdwebContract.sol b/src/test/mocks/MockThirdwebContract.sol index d793099e6..5cf31f1a7 100644 --- a/src/test/mocks/MockThirdwebContract.sol +++ b/src/test/mocks/MockThirdwebContract.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.11; -import "contracts/interfaces/IThirdwebContract.sol"; +import "contracts/infra/interface/IThirdwebContract.sol"; // solhint-disable const-name-snakecase contract MockThirdwebContract is IThirdwebContract { diff --git a/src/test/mocks/TestOracle2.sol b/src/test/mocks/TestOracle2.sol new file mode 100644 index 000000000..77c180c5b --- /dev/null +++ b/src/test/mocks/TestOracle2.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +// source: https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/test/TestOracle2.sol + +interface IOracle { + function decimals() external view returns (uint8); + function latestRoundData() + external + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); +} + +contract TestOracle2 is IOracle { + int256 public price; + uint8 private _decimals_; + + constructor(int256 _price, uint8 _decimals) { + price = _price; + _decimals_ = _decimals; + } + + function setPrice(int256 _price) external { + price = _price; + } + + function setDecimals(uint8 _decimals) external { + _decimals_ = _decimals; + } + + function decimals() external view override returns (uint8) { + return _decimals_; + } + + function latestRoundData() + external + view + override + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) + { + // solhint-disable-next-line not-rely-on-time + return (73786976294838215802, price, 1680509051, block.timestamp, 73786976294838215802); + } +} diff --git a/src/test/mocks/TestUniswap.sol b/src/test/mocks/TestUniswap.sol new file mode 100644 index 000000000..4edfef65a --- /dev/null +++ b/src/test/mocks/TestUniswap.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +// source: https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/test/TestUniswap.sol + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; + +import "./WETH9.sol"; + +/// @notice Very basic simulation of what Uniswap does with the swaps for the unit tests on the TokenPaymaster +/// @dev Do not use to test any actual Uniswap interaction logic as this is way too simplistic +contract TestUniswap { + WETH9 public weth; + + constructor(WETH9 _weth) { + weth = _weth; + } + + event StubUniswapExchangeEvent(uint256 amountIn, uint256 amountOut, address tokenIn, address tokenOut); + + function exactOutputSingle(ISwapRouter.ExactOutputSingleParams calldata params) external returns (uint256) { + uint256 amountIn = params.amountInMaximum - 5; + emit StubUniswapExchangeEvent(amountIn, params.amountOut, params.tokenIn, params.tokenOut); + IERC20(params.tokenIn).transferFrom(msg.sender, address(this), amountIn); + IERC20(params.tokenOut).transfer(params.recipient, params.amountOut); + return amountIn; + } + + function exactInputSingle(ISwapRouter.ExactInputSingleParams calldata params) external returns (uint256) { + uint256 amountOut = params.amountOutMinimum + 5; + emit StubUniswapExchangeEvent(params.amountIn, amountOut, params.tokenIn, params.tokenOut); + IERC20(params.tokenIn).transferFrom(msg.sender, address(this), params.amountIn); + IERC20(params.tokenOut).transfer(params.recipient, amountOut); + return amountOut; + } + + /// @notice Simplified code copied from here: + /// https://github.com/Uniswap/v3-periphery/blob/main/contracts/base/PeripheryPayments.sol#L19 + function unwrapWETH9(uint256 amountMinimum, address recipient) public payable { + uint256 balanceWETH9 = weth.balanceOf(address(this)); + require(balanceWETH9 >= amountMinimum, "Insufficient WETH9"); + + if (balanceWETH9 > 0) { + weth.withdraw(balanceWETH9); + payable(recipient).transfer(balanceWETH9); + } + } + + // solhint-disable-next-line no-empty-blocks + receive() external payable {} +} diff --git a/src/test/open-edition-flat-fee/_beforeTokenTransfers/_beforeTokenTransfers.t.sol b/src/test/open-edition-flat-fee/_beforeTokenTransfers/_beforeTokenTransfers.t.sol new file mode 100644 index 000000000..4ea304d13 --- /dev/null +++ b/src/test/open-edition-flat-fee/_beforeTokenTransfers/_beforeTokenTransfers.t.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { OpenEditionERC721FlatFee } from "contracts/prebuilts/open-edition/OpenEditionERC721FlatFee.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "src/test/utils/BaseTest.sol"; + +contract OpenEditionERC721FlatFeeHarness is OpenEditionERC721FlatFee { + function beforeTokenTransfers(address from, address to, uint256 startTokenId_, uint256 quantity) public { + _beforeTokenTransfers(from, to, startTokenId_, quantity); + } +} + +contract OpenEditionERC721FlatFeeTest_beforeTokenTransfers is BaseTest { + OpenEditionERC721FlatFeeHarness public openEdition; + + address private openEditionImpl; + + function setUp() public override { + super.setUp(); + openEditionImpl = address(new OpenEditionERC721FlatFeeHarness()); + vm.prank(deployer); + openEdition = OpenEditionERC721FlatFeeHarness( + address( + new TWProxy( + openEditionImpl, + abi.encodeCall( + OpenEditionERC721FlatFee.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: misc + //////////////////////////////////////////////////////////////*/ + + function test_revert_transfersRestricted() public { + address from = address(0x1); + address to = address(0x2); + bytes32 role = keccak256("TRANSFER_ROLE"); + vm.prank(deployer); + openEdition.revokeRole(role, address(0)); + + vm.expectRevert(bytes("!T")); + openEdition.beforeTokenTransfers(from, to, 0, 1); + } +} diff --git a/src/test/open-edition-flat-fee/_beforeTokenTransfers/_beforeTokenTransfers.tree b/src/test/open-edition-flat-fee/_beforeTokenTransfers/_beforeTokenTransfers.tree new file mode 100644 index 000000000..078bd6a79 --- /dev/null +++ b/src/test/open-edition-flat-fee/_beforeTokenTransfers/_beforeTokenTransfers.tree @@ -0,0 +1,12 @@ +function _beforeTokenTransfers( + address from, + address to, + uint256 startTokenId_, + uint256 quantity +) +└── when address(0) does not have the transfer role + └── when from does not equal address(0) + └── when to does not equal address(0) + └── when from does not have the transfer role + └── when to does not have the transfer role + └── it should revert ✅ diff --git a/src/test/open-edition-flat-fee/_canSetFunctions/_canSetFunctions.t.sol b/src/test/open-edition-flat-fee/_canSetFunctions/_canSetFunctions.t.sol new file mode 100644 index 000000000..c45ca2514 --- /dev/null +++ b/src/test/open-edition-flat-fee/_canSetFunctions/_canSetFunctions.t.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { OpenEditionERC721FlatFee } from "contracts/prebuilts/open-edition/OpenEditionERC721FlatFee.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "src/test/utils/BaseTest.sol"; + +contract OpenEditionERC721FlatFeeHarness is OpenEditionERC721FlatFee { + function canSetPrimarySaleRecipient() external view returns (bool) { + return _canSetPrimarySaleRecipient(); + } + + function canSetOwner() external view returns (bool) { + return _canSetOwner(); + } + + /// @dev Checks whether royalty info can be set in the given execution context. + function canSetRoyaltyInfo() external view returns (bool) { + return _canSetRoyaltyInfo(); + } + + /// @dev Checks whether contract metadata can be set in the given execution context. + function canSetContractURI() external view returns (bool) { + return _canSetContractURI(); + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function canSetClaimConditions() external view returns (bool) { + return _canSetClaimConditions(); + } + + /// @dev Returns whether the shared metadata of tokens can be set in the given execution context. + function canSetSharedMetadata() external view virtual returns (bool) { + return _canSetSharedMetadata(); + } +} + +contract OpenEditionERC721FlatFeeTest_canSetFunctions is BaseTest { + OpenEditionERC721FlatFeeHarness public openEdition; + + address private openEditionImpl; + + function setUp() public override { + super.setUp(); + openEditionImpl = address(new OpenEditionERC721FlatFeeHarness()); + vm.prank(deployer); + openEdition = OpenEditionERC721FlatFeeHarness( + address( + new TWProxy( + openEditionImpl, + abi.encodeCall( + OpenEditionERC721FlatFee.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: misc + //////////////////////////////////////////////////////////////*/ + + function test_canSetPrimarySaleRecipient_returnTrue() public { + vm.prank(deployer); + assertTrue(openEdition.canSetPrimarySaleRecipient()); + } + + function test_canSetPrimarySaleRecipient_returnFalse() public { + assertFalse(openEdition.canSetPrimarySaleRecipient()); + } + + function test_canSetOwner_returnTrue() public { + vm.prank(deployer); + assertTrue(openEdition.canSetOwner()); + } + + function test_canSetOwner_returnFalse() public { + assertFalse(openEdition.canSetOwner()); + } + + function test_canSetRoyaltyInfo_returnTrue() public { + vm.prank(deployer); + assertTrue(openEdition.canSetRoyaltyInfo()); + } + + function test_canSetRoyaltyInfo_returnFalse() public { + assertFalse(openEdition.canSetRoyaltyInfo()); + } + + function test_canSetContractURI_returnTrue() public { + vm.prank(deployer); + assertTrue(openEdition.canSetContractURI()); + } + + function test_canSetContractURI_returnFalse() public { + assertFalse(openEdition.canSetContractURI()); + } + + function test_canSetClaimConditions_returnTrue() public { + vm.prank(deployer); + assertTrue(openEdition.canSetClaimConditions()); + } + + function test_canSetClaimConditions_returnFalse() public { + assertFalse(openEdition.canSetClaimConditions()); + } + + function test_canSetSharedMetadata_returnTrue() public { + vm.prank(deployer); + assertTrue(openEdition.canSetSharedMetadata()); + } + + function test_canSetSharedMetadata_returnFalse() public { + assertFalse(openEdition.canSetSharedMetadata()); + } +} diff --git a/src/test/open-edition-flat-fee/_canSetFunctions/_canSetFunctions.tree b/src/test/open-edition-flat-fee/_canSetFunctions/_canSetFunctions.tree new file mode 100644 index 000000000..1ccb478fd --- /dev/null +++ b/src/test/open-edition-flat-fee/_canSetFunctions/_canSetFunctions.tree @@ -0,0 +1,39 @@ +function _canSetPrimarySaleRecipient() +├── when _msgSender has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when _msgSender does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + +function _canSetOwner() +├── when _msgSender has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when _msgSender does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + + +function _canSetRoyaltyInfo() +├── when _msgSender has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when _msgSender does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + + +function _canSetContractURI() +├── when _msgSender has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when _msgSender does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + + +function _canSetClaimConditions() +├── when _msgSender has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when _msgSender does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + + +function _canSetSharedMetadata() +├── when _msgSender has minter role +│ └── it should return true ✅ +└── when _msgSender does not have minter role + └── it should return false ✅ diff --git a/src/test/open-edition-flat-fee/_collectPriceOnClaim/_collectPriceOnClaim.t.sol b/src/test/open-edition-flat-fee/_collectPriceOnClaim/_collectPriceOnClaim.t.sol new file mode 100644 index 000000000..2b9f5d609 --- /dev/null +++ b/src/test/open-edition-flat-fee/_collectPriceOnClaim/_collectPriceOnClaim.t.sol @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { OpenEditionERC721FlatFee } from "contracts/prebuilts/open-edition/OpenEditionERC721FlatFee.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "src/test/utils/BaseTest.sol"; + +contract OpenEditionERC721FlatFeeHarness is OpenEditionERC721FlatFee { + function collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) external payable { + _collectPriceOnClaim(_primarySaleRecipient, _quantityToClaim, _currency, _pricePerToken); + } +} + +contract OpenEditionERC721FlatFeeTest_collectPrice is BaseTest { + OpenEditionERC721FlatFeeHarness public openEdition; + + address private openEditionImpl; + + address private currency; + address private primarySaleRecipient; + uint256 private msgValue; + uint256 private pricePerToken; + uint256 private qty = 1; + + address private defaultFeeRecipient; + + function setUp() public override { + super.setUp(); + openEditionImpl = address(new OpenEditionERC721FlatFeeHarness()); + vm.prank(deployer); + openEdition = OpenEditionERC721FlatFeeHarness( + address( + new TWProxy( + openEditionImpl, + abi.encodeCall( + OpenEditionERC721FlatFee.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ); + defaultFeeRecipient = openEdition.DEFAULT_FEE_RECIPIENT(); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: misc + //////////////////////////////////////////////////////////////*/ + + modifier pricePerTokenZero() { + _; + } + + modifier pricePerTokenNotZero() { + pricePerToken = 1 ether; + _; + } + + modifier msgValueZero() { + _; + } + + modifier msgValueNotZero() { + msgValue = 1 ether; + _; + } + + modifier valuePriceMismatch() { + msgValue = 1 ether; + pricePerToken = 2 ether; + _; + } + + modifier primarySaleRecipientZeroAddress() { + primarySaleRecipient = address(0); + _; + } + + modifier primarySaleRecipientNotZeroAddress() { + primarySaleRecipient = address(0x0999); + _; + } + + modifier currencyNativeToken() { + currency = NATIVE_TOKEN; + _; + } + + modifier currencyNotNativeToken() { + currency = address(erc20); + _; + } + + function test_revert_pricePerTokenZeroMsgValueNotZero() public pricePerTokenZero msgValueNotZero { + vm.expectRevert("!Value"); + openEdition.collectPriceOnClaim{ value: msgValue }(primarySaleRecipient, qty, currency, pricePerToken); + } + + function test_revert_nativeCurrencyValuePriceMismatch() public currencyNativeToken valuePriceMismatch { + vm.expectRevert(bytes("!V")); + openEdition.collectPriceOnClaim{ value: msgValue }(primarySaleRecipient, qty, currency, pricePerToken); + } + + function test_revert_erc20ValuePriceMismatch() public currencyNotNativeToken valuePriceMismatch { + vm.expectRevert(bytes("!V")); + openEdition.collectPriceOnClaim{ value: msgValue }(primarySaleRecipient, qty, currency, pricePerToken); + } + + function test_state_nativeCurrency() + public + currencyNativeToken + pricePerTokenNotZero + msgValueNotZero + primarySaleRecipientNotZeroAddress + { + uint256 beforeBalancePrimarySaleRecipient = address(primarySaleRecipient).balance; + uint256 defaultFeeRecipientBefore = address(defaultFeeRecipient).balance; + + openEdition.collectPriceOnClaim{ value: msgValue }(primarySaleRecipient, qty, currency, pricePerToken); + + uint256 afterBalancePrimarySaleRecipient = address(primarySaleRecipient).balance; + uint256 defaultFeeRecipientAfter = address(defaultFeeRecipient).balance; + + uint256 defaultFee = (msgValue * 100) / 10_000; + uint256 platformFeeVal = (msgValue * platformFeeBps) / 10_000; + uint256 primarySaleRecipientVal = msgValue - platformFeeVal - defaultFee; + + assertEq(beforeBalancePrimarySaleRecipient + primarySaleRecipientVal, afterBalancePrimarySaleRecipient); + assertEq(defaultFeeRecipientAfter - defaultFeeRecipientBefore, defaultFee); + } + + function test_revert_erc20_msgValueNotZero() + public + currencyNotNativeToken + msgValueNotZero + primarySaleRecipientNotZeroAddress + { + vm.expectRevert("!Value"); + openEdition.collectPriceOnClaim{ value: msgValue }(primarySaleRecipient, qty, currency, pricePerToken); + } + + function test_state_erc20() public currencyNotNativeToken pricePerTokenNotZero primarySaleRecipientNotZeroAddress { + erc20.mint(address(this), pricePerToken); + ERC20(erc20).approve(address(openEdition), pricePerToken); + uint256 beforeBalancePrimarySaleRecipient = erc20.balanceOf(primarySaleRecipient); + uint256 defaultFeeRecipientBefore = erc20.balanceOf(defaultFeeRecipient); + + openEdition.collectPriceOnClaim(primarySaleRecipient, qty, currency, pricePerToken); + + uint256 afterBalancePrimarySaleRecipient = erc20.balanceOf(primarySaleRecipient); + uint256 defaultFeeRecipientAfter = erc20.balanceOf(defaultFeeRecipient); + + uint256 defaultFee = (1 ether * 100) / 10_000; + uint256 platformFeeVal = (1 ether * platformFeeBps) / 10_000; + uint256 primarySaleRecipientVal = 1 ether - platformFeeVal - defaultFee; + + assertEq(beforeBalancePrimarySaleRecipient + primarySaleRecipientVal, afterBalancePrimarySaleRecipient); + assertEq(defaultFeeRecipientAfter - defaultFeeRecipientBefore, defaultFee); + } + + function test_state_erc20StoredPrimarySaleRecipient() + public + currencyNotNativeToken + pricePerTokenNotZero + primarySaleRecipientZeroAddress + { + address storedPrimarySaleRecipient = openEdition.primarySaleRecipient(); + + erc20.mint(address(this), pricePerToken); + ERC20(erc20).approve(address(openEdition), pricePerToken); + uint256 beforeBalancePrimarySaleRecipient = erc20.balanceOf(storedPrimarySaleRecipient); + uint256 defaultFeeRecipientBefore = erc20.balanceOf(defaultFeeRecipient); + + openEdition.collectPriceOnClaim(primarySaleRecipient, qty, currency, pricePerToken); + + uint256 afterBalancePrimarySaleRecipient = erc20.balanceOf(storedPrimarySaleRecipient); + uint256 defaultFeeRecipientAfter = erc20.balanceOf(defaultFeeRecipient); + + uint256 defaultFee = (1 ether * 100) / 10_000; + uint256 platformFeeVal = (1 ether * platformFeeBps) / 10_000; + uint256 primarySaleRecipientVal = 1 ether - platformFeeVal - defaultFee; + + assertEq(beforeBalancePrimarySaleRecipient + primarySaleRecipientVal, afterBalancePrimarySaleRecipient); + assertEq(defaultFeeRecipientAfter - defaultFeeRecipientBefore, defaultFee); + } + + function test_state_nativeCurrencyStoredPrimarySaleRecipient() + public + currencyNativeToken + pricePerTokenNotZero + primarySaleRecipientZeroAddress + msgValueNotZero + { + address storedPrimarySaleRecipient = openEdition.primarySaleRecipient(); + + uint256 beforeBalancePrimarySaleRecipient = address(storedPrimarySaleRecipient).balance; + uint256 defaultFeeRecipientBefore = address(defaultFeeRecipient).balance; + + openEdition.collectPriceOnClaim{ value: msgValue }(primarySaleRecipient, qty, currency, pricePerToken); + + uint256 afterBalancePrimarySaleRecipient = address(storedPrimarySaleRecipient).balance; + uint256 defaultFeeRecipientAfter = address(defaultFeeRecipient).balance; + + uint256 defaultFee = (msgValue * 100) / 10_000; + uint256 platformFeeVal = (msgValue * platformFeeBps) / 10_000; + uint256 primarySaleRecipientVal = msgValue - platformFeeVal - defaultFee; + + assertEq(beforeBalancePrimarySaleRecipient + primarySaleRecipientVal, afterBalancePrimarySaleRecipient); + assertEq(defaultFeeRecipientAfter - defaultFeeRecipientBefore, defaultFee); + } +} diff --git a/src/test/open-edition-flat-fee/_collectPriceOnClaim/_collectPriceOnClaim.tree b/src/test/open-edition-flat-fee/_collectPriceOnClaim/_collectPriceOnClaim.tree new file mode 100644 index 000000000..2054cf049 --- /dev/null +++ b/src/test/open-edition-flat-fee/_collectPriceOnClaim/_collectPriceOnClaim.tree @@ -0,0 +1,37 @@ +function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken +) +├── when _pricePerToken is equal to zero +│ ├── when msg.value does not equal to zero +│ │ └── it should revert ✅ +│ └── when msg.value is equal to zero +│ └── it should return ✅ +└── when _pricePerToken is not equal to zero + ├── when _primarySaleRecipient is equal to address(0) + │ ├── when saleRecipient for _tokenId is equal to address(0) + │ │ ├── when currency is native token + │ │ │ ├── when msg.value does not equal totalPrice + │ │ │ │ └── it should revert ✅ + │ │ │ └── when msg.value does equal totalPrice + │ │ │ └── it should transfer totalPrice to primarySaleRecipient in native token ✅ + │ │ └── when currency is not native token + │ │ └── it should transfer totalPrice to primarySaleRecipient in _currency token ✅ + │ └── when salerecipient for _tokenId is not equal to address(0) + │ ├── when currency is native token + │ │ ├── when msg.value does not equal totalPrice + │ │ │ └── it should revert ✅ + │ │ └── when msg.value does equal totalPrice + │ │ └── it should transfer totalPrice to saleRecipient for _tokenId in native token ✅ + │ └── when currency is not native token + │ └── it should transfer totalPrice to saleRecipient for _tokenId in _currency token ✅ + └── when _primarySaleRecipient is not equal to address(0) + ├── when currency is native token + │ ├── when msg.value does not equal totalPrice + │ │ └── it should revert ✅ + │ └── when msg.value does equal totalPrice + │ └── it should transfer totalPrice to _primarySaleRecipient in native token ✅ + └── when currency is not native token + └── it should transfer totalPrice to _primarySaleRecipient in _currency token ✅ \ No newline at end of file diff --git a/src/test/open-edition-flat-fee/_transferTokensOnClaim/_transferTokensOnClaim.t.sol b/src/test/open-edition-flat-fee/_transferTokensOnClaim/_transferTokensOnClaim.t.sol new file mode 100644 index 000000000..681ca6caa --- /dev/null +++ b/src/test/open-edition-flat-fee/_transferTokensOnClaim/_transferTokensOnClaim.t.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { OpenEditionERC721FlatFee, IERC721AUpgradeable } from "contracts/prebuilts/open-edition/OpenEditionERC721FlatFee.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "src/test/utils/BaseTest.sol"; + +contract OpenEditionERC721FlatFeeHarness is OpenEditionERC721FlatFee { + function transferTokensOnClaim(address _to, uint256 quantityBeingClaimed) public { + _transferTokensOnClaim(_to, quantityBeingClaimed); + } +} + +contract MockERC721Receiver { + function onERC721Received(address, address, uint256, bytes memory) external pure returns (bytes4) { + return this.onERC721Received.selector; + } +} + +contract MockERC721NotReceiver {} + +contract OpenEditionERC721FlatFeeTest_transferTokensOnClaim is BaseTest { + OpenEditionERC721FlatFeeHarness public openEdition; + + MockERC721NotReceiver private notReceiver; + MockERC721Receiver private receiver; + + address private openEditionImpl; + + function setUp() public override { + super.setUp(); + openEditionImpl = address(new OpenEditionERC721FlatFeeHarness()); + vm.prank(deployer); + openEdition = OpenEditionERC721FlatFeeHarness( + address( + new TWProxy( + openEditionImpl, + abi.encodeCall( + OpenEditionERC721FlatFee.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ); + + receiver = new MockERC721Receiver(); + notReceiver = new MockERC721NotReceiver(); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: misc + //////////////////////////////////////////////////////////////*/ + + function test_revert_TransferToNonReceiverContract() public { + vm.expectRevert(IERC721AUpgradeable.TransferToNonERC721ReceiverImplementer.selector); + openEdition.transferTokensOnClaim(address(notReceiver), 1); + } + + function test_state_transferToReceiverContract() public { + uint256 receiverBalanceBefore = openEdition.balanceOf(address(receiver)); + uint256 nextTokenToMintBefore = openEdition.nextTokenIdToMint(); + + openEdition.transferTokensOnClaim(address(receiver), 1); + + uint256 receiverBalanceAfter = openEdition.balanceOf(address(receiver)); + uint256 nextTokenToMintAfter = openEdition.nextTokenIdToMint(); + + assertEq(receiverBalanceAfter, receiverBalanceBefore + 1); + assertEq(nextTokenToMintAfter, nextTokenToMintBefore + 1); + } + + function test_state_transferToEOA() public { + address to = address(0x01); + uint256 receiverBalanceBefore = openEdition.balanceOf(to); + uint256 nextTokenToMintBefore = openEdition.nextTokenIdToMint(); + + openEdition.transferTokensOnClaim(to, 1); + + uint256 receiverBalanceAfter = openEdition.balanceOf(to); + uint256 nextTokenToMintAfter = openEdition.nextTokenIdToMint(); + + assertEq(receiverBalanceAfter, receiverBalanceBefore + 1); + assertEq(nextTokenToMintAfter, nextTokenToMintBefore + 1); + } +} diff --git a/src/test/open-edition-flat-fee/_transferTokensOnClaim/_transferTokensOnClaim.tree b/src/test/open-edition-flat-fee/_transferTokensOnClaim/_transferTokensOnClaim.tree new file mode 100644 index 000000000..bddcf87f6 --- /dev/null +++ b/src/test/open-edition-flat-fee/_transferTokensOnClaim/_transferTokensOnClaim.tree @@ -0,0 +1,8 @@ +function _transferTokensOnClaim(address _to, uint256 _quantityBeingClaimed) +├── when _to is a smart contract +│ ├── when _to has not implemented ERC721Receiver +│ │ └── it should revert ✅ +│ └── when _to has implemented ERC721Receiver +│ └── it should mint _quantityBeingClaimed tokens to _to ✅ +└── when _to is an EOA + └── it should mint _quantityBeingClaimed tokens to _to ✅ \ No newline at end of file diff --git a/src/test/open-edition-flat-fee/initialize/initialize.t.sol b/src/test/open-edition-flat-fee/initialize/initialize.t.sol new file mode 100644 index 000000000..7e60c0903 --- /dev/null +++ b/src/test/open-edition-flat-fee/initialize/initialize.t.sol @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { OpenEditionERC721FlatFee, Royalty } from "contracts/prebuilts/open-edition/OpenEditionERC721FlatFee.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "src/test/utils/BaseTest.sol"; + +contract OpenEditionERC721FlatFeeTest_initialize is BaseTest { + event ContractURIUpdated(string prevURI, string newURI); + event OwnerUpdated(address indexed prevOwner, address indexed newOwner); + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + event PrimarySaleRecipientUpdated(address indexed recipient); + + OpenEditionERC721FlatFee public openEdition; + + address private openEditionImpl; + + function deployOpenEdition( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _royaltyRecipient, + uint128 _royaltyBps, + uint128 _platformFeeBps, + address _platformFeeRecipient, + address _imp + ) public { + vm.prank(deployer); + openEdition = OpenEditionERC721FlatFee( + address( + new TWProxy( + _imp, + abi.encodeCall( + OpenEditionERC721FlatFee.initialize, + ( + _defaultAdmin, + _name, + _symbol, + _contractURI, + _trustedForwarders, + _saleRecipient, + _royaltyRecipient, + _royaltyBps, + _platformFeeBps, + _platformFeeRecipient + ) + ) + ) + ) + ); + } + + function setUp() public override { + super.setUp(); + openEditionImpl = address(new OpenEditionERC721FlatFee()); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: initialize + //////////////////////////////////////////////////////////////*/ + + function test_state() public { + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient, + openEditionImpl + ); + + address _saleRecipient = openEdition.primarySaleRecipient(); + (address _royaltyRecipient, uint16 _royaltyBps) = openEdition.getDefaultRoyaltyInfo(); + string memory _name = openEdition.name(); + string memory _symbol = openEdition.symbol(); + string memory _contractURI = openEdition.contractURI(); + address _owner = openEdition.owner(); + + assertEq(_name, NAME); + assertEq(_symbol, SYMBOL); + assertEq(_contractURI, CONTRACT_URI); + assertEq(_saleRecipient, saleRecipient); + assertEq(_royaltyRecipient, royaltyRecipient); + assertEq(_royaltyBps, royaltyBps); + assertEq(_owner, deployer); + + for (uint256 i = 0; i < forwarders().length; i++) { + assertEq(openEdition.isTrustedForwarder(forwarders()[i]), true); + } + + assertTrue(openEdition.hasRole(openEdition.DEFAULT_ADMIN_ROLE(), deployer)); + assertTrue(openEdition.hasRole(keccak256("TRANSFER_ROLE"), deployer)); + assertTrue(openEdition.hasRole(keccak256("MINTER_ROLE"), deployer)); + assertTrue(openEdition.hasRole(keccak256("TRANSFER_ROLE"), address(0))); + } + + function test_revert_RoyaltyTooHigh() public { + uint128 _royaltyBps = 10001; + + vm.expectRevert(abi.encodeWithSelector(Royalty.RoyaltyExceededMaxFeeBps.selector, 10_000, _royaltyBps)); + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + _royaltyBps, + platformFeeBps, + platformFeeRecipient, + openEditionImpl + ); + } + + function test_event_ContractURIUpdated() public { + vm.expectEmit(false, false, false, true); + emit ContractURIUpdated("", CONTRACT_URI); + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient, + openEditionImpl + ); + } + + function test_event_OwnerUpdated() public { + vm.expectEmit(true, true, false, false); + emit OwnerUpdated(address(0), deployer); + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient, + openEditionImpl + ); + } + + function test_event_TransferRoleAddressZero() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + vm.expectEmit(true, true, false, false); + emit RoleGranted(role, address(0), deployer); + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient, + openEditionImpl + ); + } + + function test_event_TransferRoleAdmin() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + vm.expectEmit(true, true, false, false); + emit RoleGranted(role, deployer, deployer); + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient, + openEditionImpl + ); + } + + function test_event_MinterRoleAdmin() public { + bytes32 role = keccak256("MINTER_ROLE"); + vm.expectEmit(true, true, false, false); + emit RoleGranted(role, deployer, deployer); + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient, + openEditionImpl + ); + } + + function test_event_DefaultAdminRoleAdmin() public { + bytes32 role = bytes32(0x00); + vm.expectEmit(true, true, false, false); + emit RoleGranted(role, deployer, deployer); + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient, + openEditionImpl + ); + } + + function test_event_PrimarysaleRecipientUpdated() public { + vm.expectEmit(true, false, false, false); + emit PrimarySaleRecipientUpdated(saleRecipient); + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient, + openEditionImpl + ); + } +} diff --git a/src/test/open-edition-flat-fee/initialize/initialize.tree b/src/test/open-edition-flat-fee/initialize/initialize.tree new file mode 100644 index 000000000..f56ad144b --- /dev/null +++ b/src/test/open-edition-flat-fee/initialize/initialize.tree @@ -0,0 +1,39 @@ +function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _royaltyRecipient, + uint128 _royaltyBps, + uint128 _platformFeeBps, + address _platformFeeRecipient +) +├── when _trustedForwarders.length > 0 +│ └── it should set _trustedForwarder[_trustedForwarders[i]] as true for each address in _trustedForwarders ✅ +├── it should set _name as the value provided in _name ✅ +├── it should set _symbol as the value provided in _symbol ✅ +├── it should set _currentIndex as 0 ✅ +├── it should set contractURI as _contractURI ✅ +├── it should emit ContractURIUpdated with the parameters: prevURI, _uri ✅ +├── it should set _defaultAdmin as the owner of the contract ✅ +├── it should emit OwnerUpdated with the parameters: _prevOwner, _defaultAdmin ✅ +├── it should assign the role DEFAULT_ADMIN_ROLE to _defaultAdmin ✅ +├── it should emit RoleGranted with the parameters: DEFAULT_ADMIN_ROLE, _defaultAdmin, msg.sender ✅ +├── it should assign the role _minterRole to _defaultAdmin ✅ +├── it should emit RoleGranted with the parameters: _minterRole, _defaultAdmin, msg.sender ✅ +├── it should assign the role _transferRole to _defaultAdmin ✅ +├── it should emit RoleGranted with the parameters: _transferRole, _defaultAdmin, msg.sender ✅ +├── it should assign the role _transferRole to address(0) ✅ +├── it should emit RoleGranted with the parameters: _transferRole, address(0), msg.sender ✅ +├── when _royaltyBps is greater than 10_000 +│ └── it should revert ✅ +├── when _royaltyBps is less than or equal to 10_000 +│ ├── it should set royaltyRecipient as _royaltyRecipient ✅ +│ ├── it should set royaltyBps as uint16(_royaltyBps) ✅ +│ └── it should emit DefaultRoyalty with the parameters _royaltyRecipient, _royaltyBps +├── it should set recipient as _primarySaleRecipient ✅ +├── it should emit PrimarySaleRecipientUpdated with the parameters _primarySaleRecipient ✅ +├── it should set transferRole as keccak256("TRANSFER_ROLE") ✅ +└── it should set minterRole as keccak256("MINTER_ROLE") ✅ diff --git a/src/test/open-edition-flat-fee/misc/misc.t.sol b/src/test/open-edition-flat-fee/misc/misc.t.sol new file mode 100644 index 000000000..760373600 --- /dev/null +++ b/src/test/open-edition-flat-fee/misc/misc.t.sol @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { IERC721AUpgradeable, OpenEditionERC721FlatFee, ISharedMetadata } from "contracts/prebuilts/open-edition/OpenEditionERC721FlatFee.sol"; +import { NFTMetadataRenderer } from "contracts/lib/NFTMetadataRenderer.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "src/test/utils/BaseTest.sol"; +import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; + +contract HarnessOpenEditionERC721FlatFee is OpenEditionERC721FlatFee { + function msgData() public view returns (bytes memory) { + return _msgData(); + } +} + +contract OpenEditionERC721FlatFeeTest_misc is BaseTest { + OpenEditionERC721FlatFee public openEdition; + HarnessOpenEditionERC721FlatFee public harnessOpenEdition; + + address private openEditionImpl; + address private harnessImpl; + + address private receiver = 0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd; + + ISharedMetadata.SharedMetadataInfo public sharedMetadata; + + function setUp() public override { + super.setUp(); + openEditionImpl = address(new OpenEditionERC721FlatFee()); + vm.prank(deployer); + openEdition = OpenEditionERC721FlatFee( + address( + new TWProxy( + openEditionImpl, + abi.encodeCall( + OpenEditionERC721FlatFee.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ); + + sharedMetadata = ISharedMetadata.SharedMetadataInfo({ + name: "Test", + description: "Test", + imageURI: "https://test.com", + animationURI: "https://test.com" + }); + } + + function deployHarness() internal { + harnessImpl = address(new HarnessOpenEditionERC721FlatFee()); + harnessOpenEdition = HarnessOpenEditionERC721FlatFee( + address( + new TWProxy( + harnessImpl, + abi.encodeCall( + OpenEditionERC721FlatFee.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: misc + //////////////////////////////////////////////////////////////*/ + + modifier claimTokens() { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "0"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + OpenEditionERC721FlatFee.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 0; + alp.currency = address(erc20); + + vm.warp(1); + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + vm.prank(receiver, receiver); + openEdition.claim(receiver, 100, address(erc20), 0, alp, ""); + _; + } + + modifier callerOwner() { + vm.startPrank(receiver); + _; + } + + modifier callerNotOwner() { + _; + } + + function test_tokenURI_revert_tokenDoesNotExist() public { + vm.expectRevert(bytes("!ID")); + openEdition.tokenURI(1); + } + + function test_tokenURI_returnMetadata() public claimTokens { + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + + string memory uri = openEdition.tokenURI(1); + assertEq( + uri, + NFTMetadataRenderer.createMetadataEdition({ + name: sharedMetadata.name, + description: sharedMetadata.description, + imageURI: sharedMetadata.imageURI, + animationURI: sharedMetadata.animationURI, + tokenOfEdition: 1 + }) + ); + } + + function test_startTokenId_returnOne() public { + assertEq(openEdition.startTokenId(), 1); + } + + function test_totalMinted_returnZero() public { + assertEq(openEdition.totalMinted(), 0); + } + + function test_totalMinted_returnOneHundred() public claimTokens { + assertEq(openEdition.totalMinted(), 100); + } + + function test_nextTokenIdToMint_returnOne() public { + assertEq(openEdition.nextTokenIdToMint(), 1); + } + + function test_nextTokenIdToMint_returnOneHundredAndOne() public claimTokens { + assertEq(openEdition.nextTokenIdToMint(), 101); + } + + function test_nextTokenIdToClaim_returnOne() public { + assertEq(openEdition.nextTokenIdToClaim(), 1); + } + + function test_nextTokenIdToClaim_returnOneHundredAndOne() public claimTokens { + assertEq(openEdition.nextTokenIdToClaim(), 101); + } + + function test_burn_revert_callerNotOwner() public claimTokens callerNotOwner { + vm.expectRevert(IERC721AUpgradeable.TransferCallerNotOwnerNorApproved.selector); + openEdition.burn(1); + } + + function test_burn_state_callerOwner() public claimTokens callerOwner { + uint256 balanceBeforeBurn = openEdition.balanceOf(receiver); + + openEdition.burn(1); + + uint256 balanceAfterBurn = openEdition.balanceOf(receiver); + + assertEq(balanceBeforeBurn - balanceAfterBurn, 1); + } + + function test_burn_state_callerApproved() public claimTokens { + uint256 balanceBeforeBurn = openEdition.balanceOf(receiver); + + vm.prank(receiver); + openEdition.setApprovalForAll(deployer, true); + + vm.prank(deployer); + openEdition.burn(1); + + uint256 balanceAfterBurn = openEdition.balanceOf(receiver); + + assertEq(balanceBeforeBurn - balanceAfterBurn, 1); + } + + function test_supportsInterface() public { + assertEq(openEdition.supportsInterface(type(IERC2981Upgradeable).interfaceId), true); + bytes4 invalidId = bytes4(0); + assertEq(openEdition.supportsInterface(invalidId), false); + } + + function test_msgData_returnValue() public { + deployHarness(); + bytes memory msgData = harnessOpenEdition.msgData(); + bytes4 expectedData = harnessOpenEdition.msgData.selector; + assertEq(bytes4(msgData), expectedData); + } +} diff --git a/src/test/open-edition-flat-fee/misc/misc.tree b/src/test/open-edition-flat-fee/misc/misc.tree new file mode 100644 index 000000000..07abb950c --- /dev/null +++ b/src/test/open-edition-flat-fee/misc/misc.tree @@ -0,0 +1,33 @@ +function tokenURI(uint256 _tokenId) +├── when _tokenId does not exist +│ └── it should revert ✅ +└── when _tokenID does exist + └── it should return the shared metadata ✅ + +function supportsInterface(bytes4 interfaceId) +├── it should return true for any of the listed interface ids ✅ +└── it should return false for any interfaces ids that are not listed ✅ + +function _startTokenId() +└── it should return 1 ✅ + +function startTokenId() +└── it should return _startTokenId (1) ✅ + +function totalminted() +└── it should return the total number of NFTs minted ✅ + +function nextTokenIdToMint() +└── it should return the next token ID to mint (last minted + 1) ✅ + +function nextTokenIdToClaim() +└── it should return the next token ID to mint (last minted + 1) ✅ + +function burn(uint256 tokenId) +├── when caller is not the owner of tokenId +│ ├── when caller is not an approved operator of the owner of tokenId +│ │ └── it should revert ✅ +│ └── when caller is an approved operator of the owner of tokenId +│ └── it should burn the token ✅ +└── when caller is the owner of tokenId + └── it should burn the token ✅ \ No newline at end of file diff --git a/src/test/open-edition/_beforeTokenTransfers/_beforeTokenTransfers.t.sol b/src/test/open-edition/_beforeTokenTransfers/_beforeTokenTransfers.t.sol new file mode 100644 index 000000000..1ed885681 --- /dev/null +++ b/src/test/open-edition/_beforeTokenTransfers/_beforeTokenTransfers.t.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { OpenEditionERC721 } from "contracts/prebuilts/open-edition/OpenEditionERC721.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "src/test/utils/BaseTest.sol"; + +contract OpenEditionERC721Harness is OpenEditionERC721 { + function beforeTokenTransfers(address from, address to, uint256 startTokenId_, uint256 quantity) public { + _beforeTokenTransfers(from, to, startTokenId_, quantity); + } +} + +contract OpenEditionERC721Test_beforeTokenTransfers is BaseTest { + OpenEditionERC721Harness public openEdition; + + address private openEditionImpl; + + function setUp() public override { + super.setUp(); + openEditionImpl = address(new OpenEditionERC721Harness()); + vm.prank(deployer); + openEdition = OpenEditionERC721Harness( + address( + new TWProxy( + openEditionImpl, + abi.encodeCall( + OpenEditionERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps + ) + ) + ) + ) + ); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: misc + //////////////////////////////////////////////////////////////*/ + + function test_revert_transfersRestricted() public { + address from = address(0x1); + address to = address(0x2); + bytes32 role = keccak256("TRANSFER_ROLE"); + vm.prank(deployer); + openEdition.revokeRole(role, address(0)); + + vm.expectRevert(bytes("!T")); + openEdition.beforeTokenTransfers(from, to, 0, 1); + } +} diff --git a/src/test/open-edition/_beforeTokenTransfers/_beforeTokenTransfers.tree b/src/test/open-edition/_beforeTokenTransfers/_beforeTokenTransfers.tree new file mode 100644 index 000000000..078bd6a79 --- /dev/null +++ b/src/test/open-edition/_beforeTokenTransfers/_beforeTokenTransfers.tree @@ -0,0 +1,12 @@ +function _beforeTokenTransfers( + address from, + address to, + uint256 startTokenId_, + uint256 quantity +) +└── when address(0) does not have the transfer role + └── when from does not equal address(0) + └── when to does not equal address(0) + └── when from does not have the transfer role + └── when to does not have the transfer role + └── it should revert ✅ diff --git a/src/test/open-edition/_canSetFunctions/_canSetFunctions.t.sol b/src/test/open-edition/_canSetFunctions/_canSetFunctions.t.sol new file mode 100644 index 000000000..6903bdae7 --- /dev/null +++ b/src/test/open-edition/_canSetFunctions/_canSetFunctions.t.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { OpenEditionERC721 } from "contracts/prebuilts/open-edition/OpenEditionERC721.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "src/test/utils/BaseTest.sol"; + +contract OpenEditionERC721Harness is OpenEditionERC721 { + function canSetPrimarySaleRecipient() external view returns (bool) { + return _canSetPrimarySaleRecipient(); + } + + function canSetOwner() external view returns (bool) { + return _canSetOwner(); + } + + /// @dev Checks whether royalty info can be set in the given execution context. + function canSetRoyaltyInfo() external view returns (bool) { + return _canSetRoyaltyInfo(); + } + + /// @dev Checks whether contract metadata can be set in the given execution context. + function canSetContractURI() external view returns (bool) { + return _canSetContractURI(); + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function canSetClaimConditions() external view returns (bool) { + return _canSetClaimConditions(); + } + + /// @dev Returns whether the shared metadata of tokens can be set in the given execution context. + function canSetSharedMetadata() external view virtual returns (bool) { + return _canSetSharedMetadata(); + } +} + +contract OpenEditionERC721Test_canSetFunctions is BaseTest { + OpenEditionERC721Harness public openEdition; + + address private openEditionImpl; + + function setUp() public override { + super.setUp(); + openEditionImpl = address(new OpenEditionERC721Harness()); + vm.prank(deployer); + openEdition = OpenEditionERC721Harness( + address( + new TWProxy( + openEditionImpl, + abi.encodeCall( + OpenEditionERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps + ) + ) + ) + ) + ); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: misc + //////////////////////////////////////////////////////////////*/ + + function test_canSetPrimarySaleRecipient_returnTrue() public { + vm.prank(deployer); + assertTrue(openEdition.canSetPrimarySaleRecipient()); + } + + function test_canSetPrimarySaleRecipient_returnFalse() public { + assertFalse(openEdition.canSetPrimarySaleRecipient()); + } + + function test_canSetOwner_returnTrue() public { + vm.prank(deployer); + assertTrue(openEdition.canSetOwner()); + } + + function test_canSetOwner_returnFalse() public { + assertFalse(openEdition.canSetOwner()); + } + + function test_canSetRoyaltyInfo_returnTrue() public { + vm.prank(deployer); + assertTrue(openEdition.canSetRoyaltyInfo()); + } + + function test_canSetRoyaltyInfo_returnFalse() public { + assertFalse(openEdition.canSetRoyaltyInfo()); + } + + function test_canSetContractURI_returnTrue() public { + vm.prank(deployer); + assertTrue(openEdition.canSetContractURI()); + } + + function test_canSetContractURI_returnFalse() public { + assertFalse(openEdition.canSetContractURI()); + } + + function test_canSetClaimConditions_returnTrue() public { + vm.prank(deployer); + assertTrue(openEdition.canSetClaimConditions()); + } + + function test_canSetClaimConditions_returnFalse() public { + assertFalse(openEdition.canSetClaimConditions()); + } + + function test_canSetSharedMetadata_returnTrue() public { + vm.prank(deployer); + assertTrue(openEdition.canSetSharedMetadata()); + } + + function test_canSetSharedMetadata_returnFalse() public { + assertFalse(openEdition.canSetSharedMetadata()); + } +} diff --git a/src/test/open-edition/_canSetFunctions/_canSetFunctions.tree b/src/test/open-edition/_canSetFunctions/_canSetFunctions.tree new file mode 100644 index 000000000..1ccb478fd --- /dev/null +++ b/src/test/open-edition/_canSetFunctions/_canSetFunctions.tree @@ -0,0 +1,39 @@ +function _canSetPrimarySaleRecipient() +├── when _msgSender has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when _msgSender does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + +function _canSetOwner() +├── when _msgSender has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when _msgSender does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + + +function _canSetRoyaltyInfo() +├── when _msgSender has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when _msgSender does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + + +function _canSetContractURI() +├── when _msgSender has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when _msgSender does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + + +function _canSetClaimConditions() +├── when _msgSender has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when _msgSender does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + + +function _canSetSharedMetadata() +├── when _msgSender has minter role +│ └── it should return true ✅ +└── when _msgSender does not have minter role + └── it should return false ✅ diff --git a/src/test/open-edition/_collectPriceOnClaim/_collectPriceOnClaim.t.sol b/src/test/open-edition/_collectPriceOnClaim/_collectPriceOnClaim.t.sol new file mode 100644 index 000000000..5b49bcdd7 --- /dev/null +++ b/src/test/open-edition/_collectPriceOnClaim/_collectPriceOnClaim.t.sol @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { OpenEditionERC721 } from "contracts/prebuilts/open-edition/OpenEditionERC721.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "src/test/utils/BaseTest.sol"; + +contract OpenEditionERC721Harness is OpenEditionERC721 { + function collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) external payable { + _collectPriceOnClaim(_primarySaleRecipient, _quantityToClaim, _currency, _pricePerToken); + } +} + +contract OpenEditionERC721Test_collectPrice is BaseTest { + OpenEditionERC721Harness public openEdition; + + address private openEditionImpl; + + address private currency; + address private primarySaleRecipient; + uint256 private msgValue; + uint256 private pricePerToken; + uint256 private qty = 1; + + function setUp() public override { + super.setUp(); + openEditionImpl = address(new OpenEditionERC721Harness()); + vm.prank(deployer); + openEdition = OpenEditionERC721Harness( + address( + new TWProxy( + openEditionImpl, + abi.encodeCall( + OpenEditionERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps + ) + ) + ) + ) + ); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: misc + //////////////////////////////////////////////////////////////*/ + + modifier pricePerTokenZero() { + _; + } + + modifier pricePerTokenNotZero() { + pricePerToken = 1 ether; + _; + } + + modifier msgValueZero() { + _; + } + + modifier msgValueNotZero() { + msgValue = 1 ether; + _; + } + + modifier valuePriceMismatch() { + msgValue = 1 ether; + pricePerToken = 2 ether; + _; + } + + modifier primarySaleRecipientZeroAddress() { + primarySaleRecipient = address(0); + _; + } + + modifier primarySaleRecipientNotZeroAddress() { + primarySaleRecipient = address(0x0999); + _; + } + + modifier currencyNativeToken() { + currency = NATIVE_TOKEN; + _; + } + + modifier currencyNotNativeToken() { + currency = address(erc20); + _; + } + + function test_revert_pricePerTokenZeroMsgValueNotZero() public pricePerTokenZero msgValueNotZero { + vm.expectRevert("!Value"); + openEdition.collectPriceOnClaim{ value: msgValue }(primarySaleRecipient, qty, currency, pricePerToken); + } + + function test_revert_nativeCurrencyValuePriceMismatch() public currencyNativeToken valuePriceMismatch { + vm.expectRevert(bytes("!V")); + openEdition.collectPriceOnClaim{ value: msgValue }(primarySaleRecipient, qty, currency, pricePerToken); + } + + function test_revert_erc20ValuePriceMismatch() public currencyNotNativeToken valuePriceMismatch { + vm.expectRevert(bytes("!V")); + openEdition.collectPriceOnClaim{ value: msgValue }(primarySaleRecipient, qty, currency, pricePerToken); + } + + function test_state_nativeCurrency() + public + currencyNativeToken + pricePerTokenNotZero + msgValueNotZero + primarySaleRecipientNotZeroAddress + { + uint256 beforeBalancePrimarySaleRecipient = address(primarySaleRecipient).balance; + + openEdition.collectPriceOnClaim{ value: msgValue }(primarySaleRecipient, qty, currency, pricePerToken); + + uint256 afterBalancePrimarySaleRecipient = address(primarySaleRecipient).balance; + + uint256 primarySaleRecipientVal = msgValue; + + assertEq(beforeBalancePrimarySaleRecipient + primarySaleRecipientVal, afterBalancePrimarySaleRecipient); + } + + function test_revert_erc20_msgValueNotZero() + public + currencyNotNativeToken + msgValueNotZero + primarySaleRecipientNotZeroAddress + { + vm.expectRevert("!Value"); + openEdition.collectPriceOnClaim{ value: msgValue }(primarySaleRecipient, qty, currency, pricePerToken); + } + + function test_state_erc20() public currencyNotNativeToken pricePerTokenNotZero primarySaleRecipientNotZeroAddress { + erc20.mint(address(this), pricePerToken); + ERC20(erc20).approve(address(openEdition), pricePerToken); + uint256 beforeBalancePrimarySaleRecipient = erc20.balanceOf(primarySaleRecipient); + + openEdition.collectPriceOnClaim(primarySaleRecipient, qty, currency, pricePerToken); + + uint256 afterBalancePrimarySaleRecipient = erc20.balanceOf(primarySaleRecipient); + + uint256 primarySaleRecipientVal = 1 ether; + + assertEq(beforeBalancePrimarySaleRecipient + primarySaleRecipientVal, afterBalancePrimarySaleRecipient); + } + + function test_state_erc20StoredPrimarySaleRecipient() + public + currencyNotNativeToken + pricePerTokenNotZero + primarySaleRecipientZeroAddress + { + address storedPrimarySaleRecipient = openEdition.primarySaleRecipient(); + + erc20.mint(address(this), pricePerToken); + ERC20(erc20).approve(address(openEdition), pricePerToken); + uint256 beforeBalancePrimarySaleRecipient = erc20.balanceOf(storedPrimarySaleRecipient); + + openEdition.collectPriceOnClaim(primarySaleRecipient, qty, currency, pricePerToken); + + uint256 afterBalancePrimarySaleRecipient = erc20.balanceOf(storedPrimarySaleRecipient); + + uint256 primarySaleRecipientVal = 1 ether; + + assertEq(beforeBalancePrimarySaleRecipient + primarySaleRecipientVal, afterBalancePrimarySaleRecipient); + } + + function test_state_nativeCurrencyStoredPrimarySaleRecipient() + public + currencyNativeToken + pricePerTokenNotZero + primarySaleRecipientZeroAddress + msgValueNotZero + { + address storedPrimarySaleRecipient = openEdition.primarySaleRecipient(); + + uint256 beforeBalancePrimarySaleRecipient = address(storedPrimarySaleRecipient).balance; + + openEdition.collectPriceOnClaim{ value: msgValue }(primarySaleRecipient, qty, currency, pricePerToken); + + uint256 afterBalancePrimarySaleRecipient = address(storedPrimarySaleRecipient).balance; + + uint256 primarySaleRecipientVal = msgValue; + + assertEq(beforeBalancePrimarySaleRecipient + primarySaleRecipientVal, afterBalancePrimarySaleRecipient); + } +} diff --git a/src/test/open-edition/_collectPriceOnClaim/_collectPriceOnClaim.tree b/src/test/open-edition/_collectPriceOnClaim/_collectPriceOnClaim.tree new file mode 100644 index 000000000..2054cf049 --- /dev/null +++ b/src/test/open-edition/_collectPriceOnClaim/_collectPriceOnClaim.tree @@ -0,0 +1,37 @@ +function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken +) +├── when _pricePerToken is equal to zero +│ ├── when msg.value does not equal to zero +│ │ └── it should revert ✅ +│ └── when msg.value is equal to zero +│ └── it should return ✅ +└── when _pricePerToken is not equal to zero + ├── when _primarySaleRecipient is equal to address(0) + │ ├── when saleRecipient for _tokenId is equal to address(0) + │ │ ├── when currency is native token + │ │ │ ├── when msg.value does not equal totalPrice + │ │ │ │ └── it should revert ✅ + │ │ │ └── when msg.value does equal totalPrice + │ │ │ └── it should transfer totalPrice to primarySaleRecipient in native token ✅ + │ │ └── when currency is not native token + │ │ └── it should transfer totalPrice to primarySaleRecipient in _currency token ✅ + │ └── when salerecipient for _tokenId is not equal to address(0) + │ ├── when currency is native token + │ │ ├── when msg.value does not equal totalPrice + │ │ │ └── it should revert ✅ + │ │ └── when msg.value does equal totalPrice + │ │ └── it should transfer totalPrice to saleRecipient for _tokenId in native token ✅ + │ └── when currency is not native token + │ └── it should transfer totalPrice to saleRecipient for _tokenId in _currency token ✅ + └── when _primarySaleRecipient is not equal to address(0) + ├── when currency is native token + │ ├── when msg.value does not equal totalPrice + │ │ └── it should revert ✅ + │ └── when msg.value does equal totalPrice + │ └── it should transfer totalPrice to _primarySaleRecipient in native token ✅ + └── when currency is not native token + └── it should transfer totalPrice to _primarySaleRecipient in _currency token ✅ \ No newline at end of file diff --git a/src/test/open-edition/_transferTokensOnClaim/_transferTokensOnClaim.t.sol b/src/test/open-edition/_transferTokensOnClaim/_transferTokensOnClaim.t.sol new file mode 100644 index 000000000..d0d2f2173 --- /dev/null +++ b/src/test/open-edition/_transferTokensOnClaim/_transferTokensOnClaim.t.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { OpenEditionERC721, IERC721AUpgradeable } from "contracts/prebuilts/open-edition/OpenEditionERC721.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "src/test/utils/BaseTest.sol"; + +contract OpenEditionERC721Harness is OpenEditionERC721 { + function transferTokensOnClaim(address _to, uint256 quantityBeingClaimed) public { + _transferTokensOnClaim(_to, quantityBeingClaimed); + } +} + +contract MockERC721Receiver { + function onERC721Received(address, address, uint256, bytes memory) external pure returns (bytes4) { + return this.onERC721Received.selector; + } +} + +contract MockERC721NotReceiver {} + +contract OpenEditionERC721Test_transferTokensOnClaim is BaseTest { + OpenEditionERC721Harness public openEdition; + + MockERC721NotReceiver private notReceiver; + MockERC721Receiver private receiver; + + address private openEditionImpl; + + function setUp() public override { + super.setUp(); + openEditionImpl = address(new OpenEditionERC721Harness()); + vm.prank(deployer); + openEdition = OpenEditionERC721Harness( + address( + new TWProxy( + openEditionImpl, + abi.encodeCall( + OpenEditionERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps + ) + ) + ) + ) + ); + + receiver = new MockERC721Receiver(); + notReceiver = new MockERC721NotReceiver(); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: misc + //////////////////////////////////////////////////////////////*/ + + function test_revert_TransferToNonReceiverContract() public { + vm.expectRevert(IERC721AUpgradeable.TransferToNonERC721ReceiverImplementer.selector); + openEdition.transferTokensOnClaim(address(notReceiver), 1); + } + + function test_state_transferToReceiverContract() public { + uint256 receiverBalanceBefore = openEdition.balanceOf(address(receiver)); + uint256 nextTokenToMintBefore = openEdition.nextTokenIdToMint(); + + openEdition.transferTokensOnClaim(address(receiver), 1); + + uint256 receiverBalanceAfter = openEdition.balanceOf(address(receiver)); + uint256 nextTokenToMintAfter = openEdition.nextTokenIdToMint(); + + assertEq(receiverBalanceAfter, receiverBalanceBefore + 1); + assertEq(nextTokenToMintAfter, nextTokenToMintBefore + 1); + } + + function test_state_transferToEOA() public { + address to = address(0x01); + uint256 receiverBalanceBefore = openEdition.balanceOf(to); + uint256 nextTokenToMintBefore = openEdition.nextTokenIdToMint(); + + openEdition.transferTokensOnClaim(to, 1); + + uint256 receiverBalanceAfter = openEdition.balanceOf(to); + uint256 nextTokenToMintAfter = openEdition.nextTokenIdToMint(); + + assertEq(receiverBalanceAfter, receiverBalanceBefore + 1); + assertEq(nextTokenToMintAfter, nextTokenToMintBefore + 1); + } +} diff --git a/src/test/open-edition/_transferTokensOnClaim/_transferTokensOnClaim.tree b/src/test/open-edition/_transferTokensOnClaim/_transferTokensOnClaim.tree new file mode 100644 index 000000000..bddcf87f6 --- /dev/null +++ b/src/test/open-edition/_transferTokensOnClaim/_transferTokensOnClaim.tree @@ -0,0 +1,8 @@ +function _transferTokensOnClaim(address _to, uint256 _quantityBeingClaimed) +├── when _to is a smart contract +│ ├── when _to has not implemented ERC721Receiver +│ │ └── it should revert ✅ +│ └── when _to has implemented ERC721Receiver +│ └── it should mint _quantityBeingClaimed tokens to _to ✅ +└── when _to is an EOA + └── it should mint _quantityBeingClaimed tokens to _to ✅ \ No newline at end of file diff --git a/src/test/open-edition/initialize/initialize.t.sol b/src/test/open-edition/initialize/initialize.t.sol new file mode 100644 index 000000000..9f759ffb6 --- /dev/null +++ b/src/test/open-edition/initialize/initialize.t.sol @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { OpenEditionERC721, Royalty } from "contracts/prebuilts/open-edition/OpenEditionERC721.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "src/test/utils/BaseTest.sol"; + +contract OpenEditionERC721Test_initialize is BaseTest { + event ContractURIUpdated(string prevURI, string newURI); + event OwnerUpdated(address indexed prevOwner, address indexed newOwner); + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + event PrimarySaleRecipientUpdated(address indexed recipient); + + OpenEditionERC721 public openEdition; + + address private openEditionImpl; + + function deployOpenEdition( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _royaltyRecipient, + uint128 _royaltyBps, + address _imp + ) public { + vm.prank(deployer); + openEdition = OpenEditionERC721( + address( + new TWProxy( + _imp, + abi.encodeCall( + OpenEditionERC721.initialize, + ( + _defaultAdmin, + _name, + _symbol, + _contractURI, + _trustedForwarders, + _saleRecipient, + _royaltyRecipient, + _royaltyBps + ) + ) + ) + ) + ); + } + + function setUp() public override { + super.setUp(); + openEditionImpl = address(new OpenEditionERC721()); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: initialize + //////////////////////////////////////////////////////////////*/ + + function test_state() public { + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + openEditionImpl + ); + + address _saleRecipient = openEdition.primarySaleRecipient(); + (address _royaltyRecipient, uint16 _royaltyBps) = openEdition.getDefaultRoyaltyInfo(); + string memory _name = openEdition.name(); + string memory _symbol = openEdition.symbol(); + string memory _contractURI = openEdition.contractURI(); + address _owner = openEdition.owner(); + + assertEq(_name, NAME); + assertEq(_symbol, SYMBOL); + assertEq(_contractURI, CONTRACT_URI); + assertEq(_saleRecipient, saleRecipient); + assertEq(_royaltyRecipient, royaltyRecipient); + assertEq(_royaltyBps, royaltyBps); + assertEq(_owner, deployer); + + for (uint256 i = 0; i < forwarders().length; i++) { + assertEq(openEdition.isTrustedForwarder(forwarders()[i]), true); + } + + assertTrue(openEdition.hasRole(openEdition.DEFAULT_ADMIN_ROLE(), deployer)); + assertTrue(openEdition.hasRole(keccak256("TRANSFER_ROLE"), deployer)); + assertTrue(openEdition.hasRole(keccak256("MINTER_ROLE"), deployer)); + assertTrue(openEdition.hasRole(keccak256("TRANSFER_ROLE"), address(0))); + } + + function test_revert_RoyaltyTooHigh() public { + uint128 _royaltyBps = 10001; + + vm.expectRevert(abi.encodeWithSelector(Royalty.RoyaltyExceededMaxFeeBps.selector, 10_000, _royaltyBps)); + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + _royaltyBps, + openEditionImpl + ); + } + + function test_event_ContractURIUpdated() public { + vm.expectEmit(false, false, false, true); + emit ContractURIUpdated("", CONTRACT_URI); + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + openEditionImpl + ); + } + + function test_event_OwnerUpdated() public { + vm.expectEmit(true, true, false, false); + emit OwnerUpdated(address(0), deployer); + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + openEditionImpl + ); + } + + function test_event_TransferRoleAddressZero() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + vm.expectEmit(true, true, false, false); + emit RoleGranted(role, address(0), deployer); + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + openEditionImpl + ); + } + + function test_event_TransferRoleAdmin() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + vm.expectEmit(true, true, false, false); + emit RoleGranted(role, deployer, deployer); + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + openEditionImpl + ); + } + + function test_event_MinterRoleAdmin() public { + bytes32 role = keccak256("MINTER_ROLE"); + vm.expectEmit(true, true, false, false); + emit RoleGranted(role, deployer, deployer); + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + openEditionImpl + ); + } + + function test_event_DefaultAdminRoleAdmin() public { + bytes32 role = bytes32(0x00); + vm.expectEmit(true, true, false, false); + emit RoleGranted(role, deployer, deployer); + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + openEditionImpl + ); + } + + function test_event_PrimarysaleRecipientUpdated() public { + vm.expectEmit(true, false, false, false); + emit PrimarySaleRecipientUpdated(saleRecipient); + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + openEditionImpl + ); + } +} diff --git a/src/test/open-edition/initialize/initialize.tree b/src/test/open-edition/initialize/initialize.tree new file mode 100644 index 000000000..f56ad144b --- /dev/null +++ b/src/test/open-edition/initialize/initialize.tree @@ -0,0 +1,39 @@ +function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _royaltyRecipient, + uint128 _royaltyBps, + uint128 _platformFeeBps, + address _platformFeeRecipient +) +├── when _trustedForwarders.length > 0 +│ └── it should set _trustedForwarder[_trustedForwarders[i]] as true for each address in _trustedForwarders ✅ +├── it should set _name as the value provided in _name ✅ +├── it should set _symbol as the value provided in _symbol ✅ +├── it should set _currentIndex as 0 ✅ +├── it should set contractURI as _contractURI ✅ +├── it should emit ContractURIUpdated with the parameters: prevURI, _uri ✅ +├── it should set _defaultAdmin as the owner of the contract ✅ +├── it should emit OwnerUpdated with the parameters: _prevOwner, _defaultAdmin ✅ +├── it should assign the role DEFAULT_ADMIN_ROLE to _defaultAdmin ✅ +├── it should emit RoleGranted with the parameters: DEFAULT_ADMIN_ROLE, _defaultAdmin, msg.sender ✅ +├── it should assign the role _minterRole to _defaultAdmin ✅ +├── it should emit RoleGranted with the parameters: _minterRole, _defaultAdmin, msg.sender ✅ +├── it should assign the role _transferRole to _defaultAdmin ✅ +├── it should emit RoleGranted with the parameters: _transferRole, _defaultAdmin, msg.sender ✅ +├── it should assign the role _transferRole to address(0) ✅ +├── it should emit RoleGranted with the parameters: _transferRole, address(0), msg.sender ✅ +├── when _royaltyBps is greater than 10_000 +│ └── it should revert ✅ +├── when _royaltyBps is less than or equal to 10_000 +│ ├── it should set royaltyRecipient as _royaltyRecipient ✅ +│ ├── it should set royaltyBps as uint16(_royaltyBps) ✅ +│ └── it should emit DefaultRoyalty with the parameters _royaltyRecipient, _royaltyBps +├── it should set recipient as _primarySaleRecipient ✅ +├── it should emit PrimarySaleRecipientUpdated with the parameters _primarySaleRecipient ✅ +├── it should set transferRole as keccak256("TRANSFER_ROLE") ✅ +└── it should set minterRole as keccak256("MINTER_ROLE") ✅ diff --git a/src/test/open-edition/misc/misc.t.sol b/src/test/open-edition/misc/misc.t.sol new file mode 100644 index 000000000..44e6b20cb --- /dev/null +++ b/src/test/open-edition/misc/misc.t.sol @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { IERC721AUpgradeable, OpenEditionERC721, ISharedMetadata } from "contracts/prebuilts/open-edition/OpenEditionERC721.sol"; +import { NFTMetadataRenderer } from "contracts/lib/NFTMetadataRenderer.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "src/test/utils/BaseTest.sol"; +import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; + +contract HarnessOpenEditionERC721 is OpenEditionERC721 { + function msgData() public view returns (bytes memory) { + return _msgData(); + } +} + +contract OpenEditionERC721Test_misc is BaseTest { + OpenEditionERC721 public openEdition; + HarnessOpenEditionERC721 public harnessOpenEdition; + + address private openEditionImpl; + address private harnessImpl; + + address private receiver = 0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd; + + ISharedMetadata.SharedMetadataInfo public sharedMetadata; + + function setUp() public override { + super.setUp(); + openEditionImpl = address(new OpenEditionERC721()); + vm.prank(deployer); + openEdition = OpenEditionERC721( + address( + new TWProxy( + openEditionImpl, + abi.encodeCall( + OpenEditionERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps + ) + ) + ) + ) + ); + + sharedMetadata = ISharedMetadata.SharedMetadataInfo({ + name: "Test", + description: "Test", + imageURI: "https://test.com", + animationURI: "https://test.com" + }); + } + + function deployHarness() internal { + harnessImpl = address(new HarnessOpenEditionERC721()); + harnessOpenEdition = HarnessOpenEditionERC721( + address( + new TWProxy( + harnessImpl, + abi.encodeCall( + OpenEditionERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps + ) + ) + ) + ) + ); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: misc + //////////////////////////////////////////////////////////////*/ + + modifier claimTokens() { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "0"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + OpenEditionERC721.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 0; + alp.currency = address(erc20); + + vm.warp(1); + + OpenEditionERC721.ClaimCondition[] memory conditions = new OpenEditionERC721.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + vm.prank(receiver, receiver); + openEdition.claim(receiver, 100, address(erc20), 0, alp, ""); + _; + } + + modifier callerOwner() { + vm.startPrank(receiver); + _; + } + + modifier callerNotOwner() { + _; + } + + function test_tokenURI_revert_tokenDoesNotExist() public { + vm.expectRevert(bytes("!ID")); + openEdition.tokenURI(1); + } + + function test_tokenURI_returnMetadata() public claimTokens { + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + + string memory uri = openEdition.tokenURI(1); + assertEq( + uri, + NFTMetadataRenderer.createMetadataEdition({ + name: sharedMetadata.name, + description: sharedMetadata.description, + imageURI: sharedMetadata.imageURI, + animationURI: sharedMetadata.animationURI, + tokenOfEdition: 1 + }) + ); + } + + function test_startTokenId_returnOne() public { + assertEq(openEdition.startTokenId(), 1); + } + + function test_totalMinted_returnZero() public { + assertEq(openEdition.totalMinted(), 0); + } + + function test_totalMinted_returnOneHundred() public claimTokens { + assertEq(openEdition.totalMinted(), 100); + } + + function test_nextTokenIdToMint_returnOne() public { + assertEq(openEdition.nextTokenIdToMint(), 1); + } + + function test_nextTokenIdToMint_returnOneHundredAndOne() public claimTokens { + assertEq(openEdition.nextTokenIdToMint(), 101); + } + + function test_nextTokenIdToClaim_returnOne() public { + assertEq(openEdition.nextTokenIdToClaim(), 1); + } + + function test_nextTokenIdToClaim_returnOneHundredAndOne() public claimTokens { + assertEq(openEdition.nextTokenIdToClaim(), 101); + } + + function test_burn_revert_callerNotOwner() public claimTokens callerNotOwner { + vm.expectRevert(IERC721AUpgradeable.TransferCallerNotOwnerNorApproved.selector); + openEdition.burn(1); + } + + function test_burn_state_callerOwner() public claimTokens callerOwner { + uint256 balanceBeforeBurn = openEdition.balanceOf(receiver); + + openEdition.burn(1); + + uint256 balanceAfterBurn = openEdition.balanceOf(receiver); + + assertEq(balanceBeforeBurn - balanceAfterBurn, 1); + } + + function test_burn_state_callerApproved() public claimTokens { + uint256 balanceBeforeBurn = openEdition.balanceOf(receiver); + + vm.prank(receiver); + openEdition.setApprovalForAll(deployer, true); + + vm.prank(deployer); + openEdition.burn(1); + + uint256 balanceAfterBurn = openEdition.balanceOf(receiver); + + assertEq(balanceBeforeBurn - balanceAfterBurn, 1); + } + + function test_supportsInterface() public { + assertEq(openEdition.supportsInterface(type(IERC2981Upgradeable).interfaceId), true); + bytes4 invalidId = bytes4(0); + assertEq(openEdition.supportsInterface(invalidId), false); + } + + function test_msgData_returnValue() public { + deployHarness(); + bytes memory msgData = harnessOpenEdition.msgData(); + bytes4 expectedData = harnessOpenEdition.msgData.selector; + assertEq(bytes4(msgData), expectedData); + } +} diff --git a/src/test/open-edition/misc/misc.tree b/src/test/open-edition/misc/misc.tree new file mode 100644 index 000000000..07abb950c --- /dev/null +++ b/src/test/open-edition/misc/misc.tree @@ -0,0 +1,33 @@ +function tokenURI(uint256 _tokenId) +├── when _tokenId does not exist +│ └── it should revert ✅ +└── when _tokenID does exist + └── it should return the shared metadata ✅ + +function supportsInterface(bytes4 interfaceId) +├── it should return true for any of the listed interface ids ✅ +└── it should return false for any interfaces ids that are not listed ✅ + +function _startTokenId() +└── it should return 1 ✅ + +function startTokenId() +└── it should return _startTokenId (1) ✅ + +function totalminted() +└── it should return the total number of NFTs minted ✅ + +function nextTokenIdToMint() +└── it should return the next token ID to mint (last minted + 1) ✅ + +function nextTokenIdToClaim() +└── it should return the next token ID to mint (last minted + 1) ✅ + +function burn(uint256 tokenId) +├── when caller is not the owner of tokenId +│ ├── when caller is not an approved operator of the owner of tokenId +│ │ └── it should revert ✅ +│ └── when caller is an approved operator of the owner of tokenId +│ └── it should burn the token ✅ +└── when caller is the owner of tokenId + └── it should burn the token ✅ \ No newline at end of file diff --git a/src/test/pack/Pack.t.sol b/src/test/pack/Pack.t.sol new file mode 100644 index 000000000..37fdae635 --- /dev/null +++ b/src/test/pack/Pack.t.sol @@ -0,0 +1,1253 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { Pack, IERC2981Upgradeable, IERC721Receiver, IERC1155Upgradeable } from "contracts/prebuilts/pack/Pack.sol"; +import { IPack } from "contracts/prebuilts/interface/IPack.sol"; +import { ITokenBundle } from "contracts/extension/interface/ITokenBundle.sol"; +import { CurrencyTransferLib } from "contracts/lib/CurrencyTransferLib.sol"; + +// Test imports +import { MockERC20 } from "../mocks/MockERC20.sol"; +import { Wallet } from "../utils/Wallet.sol"; +import "../utils/BaseTest.sol"; + +contract PackTest is BaseTest { + /// @notice Emitted when a set of packs is created. + event PackCreated(uint256 indexed packId, address recipient, uint256 totalPacksCreated); + + /// @notice Emitted when a pack is opened. + event PackOpened( + uint256 indexed packId, + address indexed opener, + uint256 numOfPacksOpened, + ITokenBundle.Token[] rewardUnitsDistributed + ); + + Pack internal pack; + + Wallet internal tokenOwner; + string internal packUri; + ITokenBundle.Token[] internal packContents; + ITokenBundle.Token[] internal additionalContents; + uint256[] internal numOfRewardUnits; + uint256[] internal additionalContentsRewardUnits; + + function setUp() public override { + super.setUp(); + + pack = Pack(payable(getContract("Pack"))); + + tokenOwner = getWallet(); + packUri = "ipfs://"; + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 0, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 0, + totalAmount: 100 + }) + ); + numOfRewardUnits.push(20); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 1000 ether + }) + ); + numOfRewardUnits.push(50); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 1, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 2, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 1000 ether + }) + ); + numOfRewardUnits.push(100); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 3, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 4, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 5, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 1, + totalAmount: 500 + }) + ); + numOfRewardUnits.push(50); + + erc20.mint(address(tokenOwner), 2000 ether); + erc721.mint(address(tokenOwner), 6); + erc1155.mint(address(tokenOwner), 0, 100); + erc1155.mint(address(tokenOwner), 1, 500); + + // additional contents, to check `addPackContents` + additionalContents.push( + ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 2, + totalAmount: 200 + }) + ); + additionalContentsRewardUnits.push(50); + + additionalContents.push( + ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 1000 ether + }) + ); + additionalContentsRewardUnits.push(100); + + tokenOwner.setAllowanceERC20(address(erc20), address(pack), type(uint256).max); + tokenOwner.setApprovalForAllERC721(address(erc721), address(pack), true); + tokenOwner.setApprovalForAllERC1155(address(erc1155), address(pack), true); + + vm.prank(deployer); + pack.grantRole(keccak256("MINTER_ROLE"), address(tokenOwner)); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function test_revert_addPackContents_RandomAccountGrief() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(1); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + // random address tries to transfer zero amount + address randomAccount = address(0x123); + vm.prank(randomAccount); + pack.safeTransferFrom(randomAccount, address(567), packId, 0, ""); // zero transfer + + // canUpdatePack should remain true, since no packs were transferred + assertTrue(pack.canUpdatePack(packId)); + + erc20.mint(address(tokenOwner), 1000 ether); + erc1155.mint(address(tokenOwner), 2, 200); + + vm.prank(address(tokenOwner)); + // Should not revert + pack.addPackContents(packId, additionalContents, additionalContentsRewardUnits, recipient); + } + + function test_checkForwarders() public { + assertFalse(pack.isTrustedForwarder(eoaForwarder)); + assertFalse(pack.isTrustedForwarder(forwarder)); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `createPack` + //////////////////////////////////////////////////////////////*/ + + function test_interface() public pure { + console2.logBytes4(type(IERC20).interfaceId); + console2.logBytes4(type(IERC721).interfaceId); + console2.logBytes4(type(IERC1155).interfaceId); + } + + function test_supportsInterface() public { + assertEq(pack.supportsInterface(type(IERC2981Upgradeable).interfaceId), true); + assertEq(pack.supportsInterface(type(IERC721Receiver).interfaceId), true); + assertEq(pack.supportsInterface(type(IERC1155Receiver).interfaceId), true); + assertEq(pack.supportsInterface(type(IERC1155Upgradeable).interfaceId), true); + } + + /** + * note: Testing state changes; token owner calls `createPack` to pack owned tokens. + */ + function test_state_createPack() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(1); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + assertEq(packId + 1, pack.nextTokenIdToMint()); + + (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); + assertEq(packed.length, packContents.length); + for (uint256 i = 0; i < packed.length; i += 1) { + assertEq(packed[i].assetContract, packContents[i].assetContract); + assertEq(uint256(packed[i].tokenType), uint256(packContents[i].tokenType)); + assertEq(packed[i].tokenId, packContents[i].tokenId); + assertEq(packed[i].totalAmount, packContents[i].totalAmount); + } + + assertEq(packUri, pack.uri(packId)); + } + + /* + * note: Testing state changes; token owner calls `createPack` to pack native tokens. + */ + function test_state_createPack_nativeTokens() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(0x123); + + vm.deal(address(tokenOwner), 100 ether); + packContents.push( + ITokenBundle.Token({ + assetContract: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 20 ether + }) + ); + numOfRewardUnits.push(20); + + vm.prank(address(tokenOwner)); + pack.createPack{ value: 20 ether }(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + assertEq(packId + 1, pack.nextTokenIdToMint()); + + (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); + assertEq(packed.length, packContents.length); + for (uint256 i = 0; i < packed.length; i += 1) { + assertEq(packed[i].assetContract, packContents[i].assetContract); + assertEq(uint256(packed[i].tokenType), uint256(packContents[i].tokenType)); + assertEq(packed[i].tokenId, packContents[i].tokenId); + assertEq(packed[i].totalAmount, packContents[i].totalAmount); + } + + assertEq(packUri, pack.uri(packId)); + } + + /** + * note: Testing state changes; token owner calls `createPack` to pack owned tokens. + * Only assets with ASSET_ROLE can be packed. + */ + function test_state_createPack_withAssetRoleRestriction() public { + vm.startPrank(deployer); + pack.revokeRole(keccak256("ASSET_ROLE"), address(0)); + for (uint256 i = 0; i < packContents.length; i += 1) { + if (!pack.hasRole(keccak256("ASSET_ROLE"), packContents[i].assetContract)) { + pack.grantRole(keccak256("ASSET_ROLE"), packContents[i].assetContract); + } + } + vm.stopPrank(); + + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + assertEq(packId + 1, pack.nextTokenIdToMint()); + + (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); + assertEq(packed.length, packContents.length); + for (uint256 i = 0; i < packed.length; i += 1) { + assertEq(packed[i].assetContract, packContents[i].assetContract); + assertEq(uint256(packed[i].tokenType), uint256(packContents[i].tokenType)); + assertEq(packed[i].tokenId, packContents[i].tokenId); + assertEq(packed[i].totalAmount, packContents[i].totalAmount); + } + + assertEq(packUri, pack.uri(packId)); + } + + /** + * note: Testing event emission; token owner calls `createPack` to pack owned tokens. + */ + function test_event_createPack_PackCreated() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectEmit(true, true, true, true); + emit PackCreated(packId, recipient, 226); + + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + vm.stopPrank(); + } + + /** + * note: Testing token balances; token owner calls `createPack` to pack owned tokens. + */ + function test_balances_createPack() public { + // ERC20 balance + assertEq(erc20.balanceOf(address(tokenOwner)), 2000 ether); + assertEq(erc20.balanceOf(address(pack)), 0); + + // ERC721 balance + assertEq(erc721.ownerOf(0), address(tokenOwner)); + assertEq(erc721.ownerOf(1), address(tokenOwner)); + assertEq(erc721.ownerOf(2), address(tokenOwner)); + assertEq(erc721.ownerOf(3), address(tokenOwner)); + assertEq(erc721.ownerOf(4), address(tokenOwner)); + assertEq(erc721.ownerOf(5), address(tokenOwner)); + + // ERC1155 balance + assertEq(erc1155.balanceOf(address(tokenOwner), 0), 100); + assertEq(erc1155.balanceOf(address(pack), 0), 0); + + assertEq(erc1155.balanceOf(address(tokenOwner), 1), 500); + assertEq(erc1155.balanceOf(address(pack), 1), 0); + + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(1); + + vm.prank(address(tokenOwner)); + (, uint256 totalSupply) = pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + // ERC20 balance + assertEq(erc20.balanceOf(address(tokenOwner)), 0); + assertEq(erc20.balanceOf(address(pack)), 2000 ether); + + // ERC721 balance + assertEq(erc721.ownerOf(0), address(pack)); + assertEq(erc721.ownerOf(1), address(pack)); + assertEq(erc721.ownerOf(2), address(pack)); + assertEq(erc721.ownerOf(3), address(pack)); + assertEq(erc721.ownerOf(4), address(pack)); + assertEq(erc721.ownerOf(5), address(pack)); + + // ERC1155 balance + assertEq(erc1155.balanceOf(address(tokenOwner), 0), 0); + assertEq(erc1155.balanceOf(address(pack), 0), 100); + + assertEq(erc1155.balanceOf(address(tokenOwner), 1), 0); + assertEq(erc1155.balanceOf(address(pack), 1), 500); + + // Pack wrapped token balance + assertEq(pack.balanceOf(address(recipient), packId), totalSupply); + } + + /** + * note: Testing revert condition; token owner calls `createPack` to pack owned tokens. + * Only assets with ASSET_ROLE can be packed, but assets being packed don't have that role. + */ + function test_revert_createPack_access_ASSET_ROLE() public { + vm.prank(deployer); + pack.revokeRole(keccak256("ASSET_ROLE"), address(0)); + + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + vm.expectRevert( + abi.encodeWithSelector( + Permissions.PermissionsUnauthorizedAccount.selector, + address(erc721), + keccak256("ASSET_ROLE") + ) + ); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` to pack owned tokens, without MINTER_ROLE. + */ + function test_revert_createPack_access_MINTER_ROLE() public { + vm.prank(address(tokenOwner)); + pack.renounceRole(keccak256("MINTER_ROLE"), address(tokenOwner)); + + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + vm.expectRevert( + abi.encodeWithSelector( + Permissions.PermissionsUnauthorizedAccount.selector, + address(tokenOwner), + keccak256("MINTER_ROLE") + ) + ); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` with insufficient value when packing native tokens. + */ + function test_revert_createPack_nativeTokens_insufficientValue() public { + address recipient = address(0x123); + + vm.deal(address(tokenOwner), 100 ether); + + packContents.push( + ITokenBundle.Token({ + assetContract: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 20 ether + }) + ); + numOfRewardUnits.push(1); + + vm.prank(address(tokenOwner)); + vm.expectRevert( + abi.encodeWithSelector(CurrencyTransferLib.CurrencyTransferLibMismatchedValue.selector, 0, 20 ether) + ); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` to pack un-owned ERC20 tokens. + */ + function test_revert_createPack_notOwner_ERC20() public { + tokenOwner.transferERC20(address(erc20), address(0x12), 1000 ether); + + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("ERC20: transfer amount exceeds balance"); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` to pack un-owned ERC721 tokens. + */ + function test_revert_createPack_notOwner_ERC721() public { + tokenOwner.transferERC721(address(erc721), address(0x12), 0); + + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("ERC721: caller is not token owner or approved"); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` to pack un-owned ERC1155 tokens. + */ + function test_revert_createPack_notOwner_ERC1155() public { + tokenOwner.transferERC1155(address(erc1155), address(0x12), 0, 100, ""); + + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("ERC1155: insufficient balance for transfer"); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` to pack un-approved ERC20 tokens. + */ + function test_revert_createPack_notApprovedTransfer_ERC20() public { + tokenOwner.setAllowanceERC20(address(erc20), address(pack), 0); + + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("ERC20: insufficient allowance"); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` to pack un-approved ERC721 tokens. + */ + function test_revert_createPack_notApprovedTransfer_ERC721() public { + tokenOwner.setApprovalForAllERC721(address(erc721), address(pack), false); + + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("ERC721: caller is not token owner or approved"); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` to pack un-approved ERC1155 tokens. + */ + function test_revert_createPack_notApprovedTransfer_ERC1155() public { + tokenOwner.setApprovalForAllERC1155(address(erc1155), address(pack), false); + + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("ERC1155: caller is not token owner or approved"); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` with invalid token-type. + */ + function test_revert_createPack_invalidTokenType() public { + ITokenBundle.Token[] memory invalidContent = new ITokenBundle.Token[](1); + uint256[] memory rewardUnits = new uint256[](1); + + invalidContent[0] = ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 1 + }); + rewardUnits[0] = 1; + + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("!TokenType"); + pack.createPack(invalidContent, rewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` with total-amount as 0. + */ + function test_revert_createPack_zeroTotalAmount() public { + ITokenBundle.Token[] memory invalidContent = new ITokenBundle.Token[](1); + uint256[] memory rewardUnits = new uint256[](1); + + invalidContent[0] = ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 0 + }); + rewardUnits[0] = 10; + + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("0 amt"); + pack.createPack(invalidContent, rewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` with no tokens to pack. + */ + function test_revert_createPack_noTokensToPack() public { + ITokenBundle.Token[] memory emptyContent; + uint256[] memory rewardUnits; + + address recipient = address(0x123); + + bytes memory err = "!Len"; + vm.startPrank(address(tokenOwner)); + vm.expectRevert(err); + pack.createPack(emptyContent, rewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` with unequal length of contents and rewardUnits. + */ + function test_revert_createPack_invalidRewardUnits() public { + uint256[] memory rewardUnits; + + address recipient = address(0x123); + + bytes memory err = "!Len"; + vm.startPrank(address(tokenOwner)); + vm.expectRevert(err); + pack.createPack(packContents, rewardUnits, packUri, 0, 1, recipient); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `addPackContents` + //////////////////////////////////////////////////////////////*/ + + /** + * note: Testing state changes; token owner calls `addPackContents` to pack more tokens. + */ + function test_state_addPackContents() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(1); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); + assertEq(packed.length, packContents.length); + for (uint256 i = 0; i < packed.length; i += 1) { + assertEq(packed[i].assetContract, packContents[i].assetContract); + assertEq(uint256(packed[i].tokenType), uint256(packContents[i].tokenType)); + assertEq(packed[i].tokenId, packContents[i].tokenId); + assertEq(packed[i].totalAmount, packContents[i].totalAmount); + } + + erc20.mint(address(tokenOwner), 1000 ether); + erc1155.mint(address(tokenOwner), 2, 200); + + vm.prank(address(tokenOwner)); + pack.addPackContents(packId, additionalContents, additionalContentsRewardUnits, recipient); + + (packed, ) = pack.getPackContents(packId); + assertEq(packed.length, packContents.length + additionalContents.length); + for (uint256 i = packContents.length; i < packed.length; i += 1) { + assertEq(packed[i].assetContract, additionalContents[i - packContents.length].assetContract); + assertEq(uint256(packed[i].tokenType), uint256(additionalContents[i - packContents.length].tokenType)); + assertEq(packed[i].tokenId, additionalContents[i - packContents.length].tokenId); + assertEq(packed[i].totalAmount, additionalContents[i - packContents.length].totalAmount); + } + } + + /** + * note: Testing token balances; token owner calls `addPackContents` to pack more tokens + * in an already existing pack. + */ + function test_balances_addPackContents() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(1); + + vm.prank(address(tokenOwner)); + (, uint256 totalSupply) = pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + // ERC20 balance + assertEq(erc20.balanceOf(address(tokenOwner)), 0); + assertEq(erc20.balanceOf(address(pack)), 2000 ether); + + // ERC721 balance + assertEq(erc721.ownerOf(0), address(pack)); + assertEq(erc721.ownerOf(1), address(pack)); + assertEq(erc721.ownerOf(2), address(pack)); + assertEq(erc721.ownerOf(3), address(pack)); + assertEq(erc721.ownerOf(4), address(pack)); + assertEq(erc721.ownerOf(5), address(pack)); + + // ERC1155 balance + assertEq(erc1155.balanceOf(address(tokenOwner), 0), 0); + assertEq(erc1155.balanceOf(address(pack), 0), 100); + + assertEq(erc1155.balanceOf(address(tokenOwner), 1), 0); + assertEq(erc1155.balanceOf(address(pack), 1), 500); + + // Pack wrapped token balance + assertEq(pack.balanceOf(address(recipient), packId), totalSupply); + + erc20.mint(address(tokenOwner), 1000 ether); + erc1155.mint(address(tokenOwner), 2, 200); + + vm.prank(address(tokenOwner)); + (uint256 newTotalSupply, uint256 additionalSupply) = pack.addPackContents( + packId, + additionalContents, + additionalContentsRewardUnits, + recipient + ); + + // ERC20 balance after adding more tokens + assertEq(erc20.balanceOf(address(tokenOwner)), 0); + assertEq(erc20.balanceOf(address(pack)), 3000 ether); + + // ERC1155 balance after adding more tokens + assertEq(erc1155.balanceOf(address(tokenOwner), 2), 0); + assertEq(erc1155.balanceOf(address(pack), 2), 200); + + // Pack wrapped token balance + assertEq(pack.balanceOf(address(recipient), packId), newTotalSupply); + assertEq(totalSupply + additionalSupply, newTotalSupply); + } + + /** + * note: Testing revert condition; non-creator calls `addPackContents`. + */ + function test_revert_addPackContents_NotMinterRole() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(1); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + address randomAccount = address(0x123); + + vm.prank(randomAccount); + vm.expectRevert( + abi.encodeWithSelector( + Permissions.PermissionsUnauthorizedAccount.selector, + randomAccount, + keccak256("MINTER_ROLE") + ) + ); + pack.addPackContents(packId, additionalContents, additionalContentsRewardUnits, recipient); + } + + /** + * note: Testing revert condition; adding tokens to non-existent pack. + */ + function test_revert_addPackContents_PackNonExistent() public { + vm.prank(address(tokenOwner)); + vm.expectRevert("!Allowed"); + pack.addPackContents(0, packContents, numOfRewardUnits, address(1)); + } + + /** + * note: Testing revert condition; adding tokens after packs have been distributed. + */ + function test_revert_addPackContents_CantUpdateAnymore() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(1); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + vm.prank(recipient); + pack.safeTransferFrom(recipient, address(567), packId, 1, ""); + + vm.prank(address(tokenOwner)); + vm.expectRevert("!Allowed"); + pack.addPackContents(packId, additionalContents, additionalContentsRewardUnits, recipient); + } + + /** + * note: Testing revert condition; adding tokens with a different recipient. + */ + function test_revert_addPackContents_NotRecipient() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(1); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + address randomRecipient = address(0x12345); + + bytes memory err = "!Bal"; + vm.expectRevert(err); + vm.prank(address(tokenOwner)); + pack.addPackContents(packId, additionalContents, additionalContentsRewardUnits, randomRecipient); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `openPack` + //////////////////////////////////////////////////////////////*/ + + /** + * note: Testing state changes; pack owner calls `openPack` to redeem underlying rewards. + */ + function test_state_openPack() public { + vm.warp(1000); + uint256 packId = pack.nextTokenIdToMint(); + uint256 packsToOpen = 3; + address recipient = address(1); + + vm.prank(address(tokenOwner)); + (, uint256 totalSupply) = pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); + + vm.prank(recipient, recipient); + ITokenBundle.Token[] memory rewardUnits = pack.openPack(packId, packsToOpen); + console2.log("total reward units: ", rewardUnits.length); + + for (uint256 i = 0; i < rewardUnits.length; i++) { + console2.log("----- reward unit number: ", i, "------"); + console2.log("asset contract: ", rewardUnits[i].assetContract); + console2.log("token type: ", uint256(rewardUnits[i].tokenType)); + console2.log("tokenId: ", rewardUnits[i].tokenId); + if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC20) { + console2.log("total amount: ", rewardUnits[i].totalAmount / 1 ether, "ether"); + } else { + console2.log("total amount: ", rewardUnits[i].totalAmount); + } + console2.log(""); + } + + assertEq(packUri, pack.uri(packId)); + assertEq(pack.totalSupply(packId), totalSupply - packsToOpen); + + (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); + assertEq(packed.length, packContents.length); + } + + /** + * note: Total amount should get updated correctly -- reduce perUnitAmount from totalAmount of the token content, for each reward + */ + function test_state_openPack_totalAmounts_ERC721() public { + vm.warp(1000); + uint256 packId = pack.nextTokenIdToMint(); + uint256 packsToOpen = 1; + address recipient = address(1); + + erc721.mint(address(tokenOwner), 6); + + ITokenBundle.Token[] memory tempContents = new ITokenBundle.Token[](1); + uint256[] memory tempNumRewardUnits = new uint256[](1); + + tempContents[0] = ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 0, + totalAmount: 1 + }); + tempNumRewardUnits[0] = 1; + + vm.prank(address(tokenOwner)); + (, uint256 totalSupply) = pack.createPack(tempContents, tempNumRewardUnits, packUri, 0, 1, recipient); + + vm.prank(recipient, recipient); + ITokenBundle.Token[] memory rewardUnits = pack.openPack(packId, packsToOpen); + + assertEq(packUri, pack.uri(packId)); + assertEq(pack.totalSupply(packId), totalSupply - packsToOpen); + + (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); + assertEq(packed.length, tempContents.length); + assertEq(packed[0].totalAmount, tempContents[0].totalAmount - rewardUnits[0].totalAmount); + } + + /** + * note: Total amount should get updated correctly -- reduce perUnitAmount from totalAmount of the token content, for each reward + */ + function test_state_openPack_totalAmounts_ERC1155() public { + vm.warp(1000); + uint256 packId = pack.nextTokenIdToMint(); + uint256 packsToOpen = 1; + address recipient = address(1); + + erc1155.mint(address(tokenOwner), 0, 100); + + ITokenBundle.Token[] memory tempContents = new ITokenBundle.Token[](1); + uint256[] memory tempNumRewardUnits = new uint256[](1); + + tempContents[0] = ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 0, + totalAmount: 100 + }); + tempNumRewardUnits[0] = 10; + + vm.prank(address(tokenOwner)); + (, uint256 totalSupply) = pack.createPack(tempContents, tempNumRewardUnits, packUri, 0, 1, recipient); + + vm.prank(recipient, recipient); + ITokenBundle.Token[] memory rewardUnits = pack.openPack(packId, packsToOpen); + + assertEq(packUri, pack.uri(packId)); + assertEq(pack.totalSupply(packId), totalSupply - packsToOpen); + + (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); + assertEq(packed.length, tempContents.length); + assertEq(packed[0].totalAmount, tempContents[0].totalAmount - rewardUnits[0].totalAmount); + } + + /** + * note: Total amount should get updated correctly -- reduce perUnitAmount from totalAmount of the token content, for each reward + */ + function test_state_openPack_totalAmounts_ERC20() public { + vm.warp(1000); + uint256 packId = pack.nextTokenIdToMint(); + uint256 packsToOpen = 1; + address recipient = address(1); + + erc20.mint(address(tokenOwner), 2000 ether); + + ITokenBundle.Token[] memory tempContents = new ITokenBundle.Token[](1); + uint256[] memory tempNumRewardUnits = new uint256[](1); + + tempContents[0] = ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 1000 ether + }); + tempNumRewardUnits[0] = 50; + + vm.prank(address(tokenOwner)); + (, uint256 totalSupply) = pack.createPack(tempContents, tempNumRewardUnits, packUri, 0, 1, recipient); + + vm.prank(recipient, recipient); + ITokenBundle.Token[] memory rewardUnits = pack.openPack(packId, packsToOpen); + + assertEq(packUri, pack.uri(packId)); + assertEq(pack.totalSupply(packId), totalSupply - packsToOpen); + + (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); + assertEq(packed.length, tempContents.length); + assertEq(packed[0].totalAmount, tempContents[0].totalAmount - rewardUnits[0].totalAmount); + } + + /** + * note: Testing event emission; pack owner calls `openPack` to open owned packs. + */ + function test_event_openPack_PackOpened() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(0x123); + + ITokenBundle.Token[] memory emptyRewardUnitsForTestingEvent; + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + vm.expectEmit(true, true, false, false); + emit PackOpened(packId, recipient, 1, emptyRewardUnitsForTestingEvent); + + vm.prank(recipient, recipient); + pack.openPack(packId, 1); + } + + function test_balances_openPack() public { + uint256 packId = pack.nextTokenIdToMint(); + uint256 packsToOpen = 3; + address recipient = address(1); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); + + // ERC20 balance + assertEq(erc20.balanceOf(address(recipient)), 0); + assertEq(erc20.balanceOf(address(pack)), 2000 ether); + + // ERC721 balance + assertEq(erc721.ownerOf(0), address(pack)); + assertEq(erc721.ownerOf(1), address(pack)); + assertEq(erc721.ownerOf(2), address(pack)); + assertEq(erc721.ownerOf(3), address(pack)); + assertEq(erc721.ownerOf(4), address(pack)); + assertEq(erc721.ownerOf(5), address(pack)); + + // ERC1155 balance + assertEq(erc1155.balanceOf(address(recipient), 0), 0); + assertEq(erc1155.balanceOf(address(pack), 0), 100); + + assertEq(erc1155.balanceOf(address(recipient), 1), 0); + assertEq(erc1155.balanceOf(address(pack), 1), 500); + + vm.prank(recipient, recipient); + ITokenBundle.Token[] memory rewardUnits = pack.openPack(packId, packsToOpen); + console2.log("total reward units: ", rewardUnits.length); + + uint256 erc20Amount; + uint256[] memory erc1155Amounts = new uint256[](2); + uint256 erc721Amount; + + for (uint256 i = 0; i < rewardUnits.length; i++) { + console2.log("----- reward unit number: ", i, "------"); + console2.log("asset contract: ", rewardUnits[i].assetContract); + console2.log("token type: ", uint256(rewardUnits[i].tokenType)); + console2.log("tokenId: ", rewardUnits[i].tokenId); + if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC20) { + console2.log("total amount: ", rewardUnits[i].totalAmount / 1 ether, "ether"); + console.log("balance of recipient: ", erc20.balanceOf(address(recipient)) / 1 ether, "ether"); + erc20Amount += rewardUnits[i].totalAmount; + } else if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC1155) { + console2.log("total amount: ", rewardUnits[i].totalAmount); + console.log("balance of recipient: ", erc1155.balanceOf(address(recipient), rewardUnits[i].tokenId)); + erc1155Amounts[rewardUnits[i].tokenId] += rewardUnits[i].totalAmount; + } else if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC721) { + console2.log("total amount: ", rewardUnits[i].totalAmount); + console.log("balance of recipient: ", erc721.balanceOf(address(recipient))); + erc721Amount += rewardUnits[i].totalAmount; + } + console2.log(""); + } + + assertEq(erc20.balanceOf(address(recipient)), erc20Amount); + assertEq(erc721.balanceOf(address(recipient)), erc721Amount); + + for (uint256 i = 0; i < erc1155Amounts.length; i += 1) { + assertEq(erc1155.balanceOf(address(recipient), i), erc1155Amounts[i]); + } + } + + /** + * note: Testing revert condition; caller of `openPack` is not EOA. + */ + function test_revert_openPack_notEOA() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + vm.startPrank(recipient, address(27)); + string memory err = "!EOA"; + vm.expectRevert(bytes(err)); + pack.openPack(packId, 1); + } + + /** + * note: Testing revert condition; pack owner calls `openPack` to open more than owned packs. + */ + function test_revert_openPack_openMoreThanOwned() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + (, uint256 totalSupply) = pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + bytes memory err = "!Bal"; + vm.startPrank(recipient, recipient); + vm.expectRevert(err); + pack.openPack(packId, totalSupply + 1); + } + + /** + * note: Testing revert condition; pack owner calls `openPack` before start timestamp. + */ + function test_revert_openPack_openBeforeStart() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(0x123); + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 1000, 1, recipient); + + vm.startPrank(recipient, recipient); + vm.expectRevert("cant open"); + pack.openPack(packId, 1); + } + + /** + * note: Testing revert condition; pack owner calls `openPack` with pack-id non-existent or not owned. + */ + function test_revert_openPack_invalidPackId() public { + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + bytes memory err = "!Bal"; + vm.startPrank(recipient, recipient); + vm.expectRevert(err); + pack.openPack(2, 1); + } + + /*/////////////////////////////////////////////////////////////// + Fuzz testing + //////////////////////////////////////////////////////////////*/ + + uint256 internal constant MAX_TOKENS = 2000; + + function getTokensToPack( + uint256 len + ) internal returns (ITokenBundle.Token[] memory tokensToPack, uint256[] memory rewardUnits) { + vm.assume(len < MAX_TOKENS); + tokensToPack = new ITokenBundle.Token[](len); + rewardUnits = new uint256[](len); + + for (uint256 i = 0; i < len; i += 1) { + uint256 random = uint256(keccak256(abi.encodePacked(len + i))) % MAX_TOKENS; + uint256 selector = random % 4; + + if (selector == 0) { + tokensToPack[i] = ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: (random + 1) * 10 ether + }); + rewardUnits[i] = random + 1; + + erc20.mint(address(tokenOwner), tokensToPack[i].totalAmount); + } else if (selector == 1) { + uint256 tokenId = erc721.nextTokenIdToMint(); + + tokensToPack[i] = ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: tokenId, + totalAmount: 1 + }); + rewardUnits[i] = 1; + + erc721.mint(address(tokenOwner), 1); + } else if (selector == 2) { + tokensToPack[i] = ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: random, + totalAmount: (random + 1) * 10 + }); + rewardUnits[i] = random + 1; + + erc1155.mint(address(tokenOwner), tokensToPack[i].tokenId, tokensToPack[i].totalAmount); + } else if (selector == 3) { + tokensToPack[i] = ITokenBundle.Token({ + assetContract: address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 5 ether + }); + rewardUnits[i] = 5; + } + } + } + + function checkBalances( + ITokenBundle.Token[] memory rewardUnits, + address + ) + internal + pure + returns (uint256 nativeTokenAmount, uint256 erc20Amount, uint256[] memory erc1155Amounts, uint256 erc721Amount) + { + erc1155Amounts = new uint256[](MAX_TOKENS); + + for (uint256 i = 0; i < rewardUnits.length; i++) { + // console2.log("----- reward unit number: ", i, "------"); + // console2.log("asset contract: ", rewardUnits[i].assetContract); + // console2.log("token type: ", uint256(rewardUnits[i].tokenType)); + // console2.log("tokenId: ", rewardUnits[i].tokenId); + if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC20) { + if (rewardUnits[i].assetContract == address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) { + // console2.log("total amount: ", rewardUnits[i].totalAmount / 1 ether, "ether"); + // console.log("balance of recipient: ", address(recipient).balance); + nativeTokenAmount += rewardUnits[i].totalAmount; + } else { + // console2.log("total amount: ", rewardUnits[i].totalAmount / 1 ether, "ether"); + // console.log("balance of recipient: ", erc20.balanceOf(address(recipient)) / 1 ether, "ether"); + erc20Amount += rewardUnits[i].totalAmount; + } + } else if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC1155) { + // console2.log("total amount: ", rewardUnits[i].totalAmount); + // console.log("balance of recipient: ", erc1155.balanceOf(address(recipient), rewardUnits[i].tokenId)); + erc1155Amounts[rewardUnits[i].tokenId] += rewardUnits[i].totalAmount; + } else if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC721) { + // console2.log("total amount: ", rewardUnits[i].totalAmount); + // console.log("balance of recipient: ", erc721.balanceOf(address(recipient))); + erc721Amount += rewardUnits[i].totalAmount; + } + // console2.log(""); + } + } + + function test_fuzz_state_createPack(uint256 x, uint128 y) public { + (ITokenBundle.Token[] memory tokensToPack, uint256[] memory rewardUnits) = getTokensToPack(x); + if (tokensToPack.length == 0) { + return; + } + + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(0x123); + uint256 totalRewardUnits; + uint256 nativeTokenPacked; + + for (uint256 i = 0; i < tokensToPack.length; i += 1) { + totalRewardUnits += rewardUnits[i]; + if (tokensToPack[i].assetContract == address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) { + nativeTokenPacked += tokensToPack[i].totalAmount; + } + } + vm.deal(address(tokenOwner), nativeTokenPacked); + vm.assume(y > 0 && totalRewardUnits % y == 0); + + vm.prank(address(tokenOwner)); + (, uint256 totalSupply) = pack.createPack{ value: nativeTokenPacked }( + tokensToPack, + rewardUnits, + packUri, + 0, + y, + recipient + ); + console2.log("total supply: ", totalSupply); + console2.log("total reward units: ", totalRewardUnits); + + assertEq(packId + 1, pack.nextTokenIdToMint()); + + (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); + assertEq(packed.length, tokensToPack.length); + for (uint256 i = 0; i < packed.length; i += 1) { + assertEq(packed[i].assetContract, tokensToPack[i].assetContract); + assertEq(uint256(packed[i].tokenType), uint256(tokensToPack[i].tokenType)); + assertEq(packed[i].tokenId, tokensToPack[i].tokenId); + assertEq(packed[i].totalAmount, tokensToPack[i].totalAmount); + } + + assertEq(packUri, pack.uri(packId)); + } + + /*/////////////////////////////////////////////////////////////// + Scenario/Exploit tests + //////////////////////////////////////////////////////////////*/ + /** + * note: Testing revert condition; token owner calls `createPack` to pack owned tokens. + */ + function test_revert_createPack_reentrancy() public { + MaliciousERC20 malERC20 = new MaliciousERC20(payable(address(pack))); + ITokenBundle.Token[] memory content = new ITokenBundle.Token[](1); + uint256[] memory rewards = new uint256[](1); + + malERC20.mint(address(tokenOwner), 10 ether); + content[0] = ITokenBundle.Token({ + assetContract: address(malERC20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 10 ether + }); + rewards[0] = 10; + + tokenOwner.setAllowanceERC20(address(malERC20), address(pack), 10 ether); + + address recipient = address(0x123); + + vm.prank(address(deployer)); + pack.grantRole(keccak256("MINTER_ROLE"), address(malERC20)); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("ReentrancyGuard: reentrant call"); + pack.createPack(content, rewards, packUri, 0, 1, recipient); + } +} + +contract MaliciousERC20 is MockERC20, ITokenBundle { + Pack public pack; + + constructor(address payable _pack) { + pack = Pack(_pack); + } + + function transferFrom(address from, address to, uint256 amount) public override returns (bool) { + ITokenBundle.Token[] memory content = new ITokenBundle.Token[](1); + uint256[] memory rewards = new uint256[](1); + + address recipient = address(0x123); + pack.createPack(content, rewards, "", 0, 1, recipient); + return super.transferFrom(from, to, amount); + } +} diff --git a/src/test/pack/PackVRFDirect.t.sol b/src/test/pack/PackVRFDirect.t.sol new file mode 100644 index 000000000..55a620e19 --- /dev/null +++ b/src/test/pack/PackVRFDirect.t.sol @@ -0,0 +1,1055 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { PackVRFDirect, IERC2981Upgradeable, IERC721Receiver, IERC1155Upgradeable } from "contracts/prebuilts/pack/PackVRFDirect.sol"; +import { IPack } from "contracts/prebuilts/interface/IPack.sol"; +import { ITokenBundle } from "contracts/extension/interface/ITokenBundle.sol"; +import { CurrencyTransferLib } from "contracts/lib/CurrencyTransferLib.sol"; + +// Test imports +import { MockERC20 } from "../mocks/MockERC20.sol"; +import { Wallet } from "../utils/Wallet.sol"; +import "../utils/BaseTest.sol"; + +contract PackVRFDirectTest is BaseTest { + /// @notice Emitted when a set of packs is created. + event PackCreated(uint256 indexed packId, address recipient, uint256 totalPacksCreated); + + /// @notice Emitted when the opening of a pack is requested. + event PackOpenRequested(address indexed opener, uint256 indexed packId, uint256 amountToOpen, uint256 requestId); + + /// @notice Emitted when Chainlink VRF fulfills a random number request. + event PackRandomnessFulfilled(uint256 indexed packId, uint256 indexed requestId); + + /// @notice Emitted when a pack is opened. + event PackOpened( + uint256 indexed packId, + address indexed opener, + uint256 numOfPacksOpened, + ITokenBundle.Token[] rewardUnitsDistributed + ); + + PackVRFDirect internal pack; + + Wallet internal tokenOwner; + string internal packUri; + ITokenBundle.Token[] internal packContents; + ITokenBundle.Token[] internal additionalContents; + uint256[] internal numOfRewardUnits; + uint256[] internal additionalContentsRewardUnits; + + function setUp() public virtual override { + super.setUp(); + + pack = PackVRFDirect(payable(getContract("PackVRFDirect"))); + + tokenOwner = getWallet(); + packUri = "ipfs://"; + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 0, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 0, + totalAmount: 100 + }) + ); + numOfRewardUnits.push(20); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 1000 ether + }) + ); + numOfRewardUnits.push(50); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 1, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 2, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 1000 ether + }) + ); + numOfRewardUnits.push(100); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 3, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 4, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 5, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 1, + totalAmount: 500 + }) + ); + numOfRewardUnits.push(50); + + erc20.mint(address(tokenOwner), 2000 ether); + erc721.mint(address(tokenOwner), 6); + erc1155.mint(address(tokenOwner), 0, 100); + erc1155.mint(address(tokenOwner), 1, 500); + + // additional contents, to check `addPackContents` + additionalContents.push( + ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 2, + totalAmount: 200 + }) + ); + additionalContentsRewardUnits.push(50); + + additionalContents.push( + ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 1000 ether + }) + ); + additionalContentsRewardUnits.push(100); + + tokenOwner.setAllowanceERC20(address(erc20), address(pack), type(uint256).max); + tokenOwner.setApprovalForAllERC721(address(erc721), address(pack), true); + tokenOwner.setApprovalForAllERC1155(address(erc1155), address(pack), true); + + vm.prank(deployer); + pack.grantRole(keccak256("MINTER_ROLE"), address(tokenOwner)); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `createPack` + //////////////////////////////////////////////////////////////*/ + + function test_interface() public pure { + console2.logBytes4(type(IERC20).interfaceId); + console2.logBytes4(type(IERC721).interfaceId); + console2.logBytes4(type(IERC1155).interfaceId); + } + + function test_supportsInterface() public { + assertEq(pack.supportsInterface(type(IERC2981Upgradeable).interfaceId), true); + assertEq(pack.supportsInterface(type(IERC721Receiver).interfaceId), true); + assertEq(pack.supportsInterface(type(IERC1155Receiver).interfaceId), true); + assertEq(pack.supportsInterface(type(IERC1155Upgradeable).interfaceId), true); + } + + /** + * note: Testing state changes; token owner calls `createPack` to pack owned tokens. + */ + function test_state_createPack() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(1); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + assertEq(packId + 1, pack.nextTokenIdToMint()); + + (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); + assertEq(packed.length, packContents.length); + for (uint256 i = 0; i < packed.length; i += 1) { + assertEq(packed[i].assetContract, packContents[i].assetContract); + assertEq(uint256(packed[i].tokenType), uint256(packContents[i].tokenType)); + assertEq(packed[i].tokenId, packContents[i].tokenId); + assertEq(packed[i].totalAmount, packContents[i].totalAmount); + } + + assertEq(packUri, pack.uri(packId)); + } + + /* + * note: Testing state changes; token owner calls `createPack` to pack native tokens. + */ + function test_state_createPack_nativeTokens() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(0x123); + + vm.deal(address(tokenOwner), 100 ether); + packContents.push( + ITokenBundle.Token({ + assetContract: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 20 ether + }) + ); + numOfRewardUnits.push(20); + + vm.prank(address(tokenOwner)); + pack.createPack{ value: 20 ether }(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + assertEq(packId + 1, pack.nextTokenIdToMint()); + + (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); + assertEq(packed.length, packContents.length); + for (uint256 i = 0; i < packed.length; i += 1) { + assertEq(packed[i].assetContract, packContents[i].assetContract); + assertEq(uint256(packed[i].tokenType), uint256(packContents[i].tokenType)); + assertEq(packed[i].tokenId, packContents[i].tokenId); + assertEq(packed[i].totalAmount, packContents[i].totalAmount); + } + + assertEq(packUri, pack.uri(packId)); + } + + /** + * note: Testing event emission; token owner calls `createPack` to pack owned tokens. + */ + function test_event_createPack_PackCreated() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectEmit(true, true, true, true); + emit PackCreated(packId, recipient, 226); + + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + vm.stopPrank(); + } + + /** + * note: Testing token balances; token owner calls `createPack` to pack owned tokens. + */ + function test_balances_createPack() public { + // ERC20 balance + assertEq(erc20.balanceOf(address(tokenOwner)), 2000 ether); + assertEq(erc20.balanceOf(address(pack)), 0); + + // ERC721 balance + assertEq(erc721.ownerOf(0), address(tokenOwner)); + assertEq(erc721.ownerOf(1), address(tokenOwner)); + assertEq(erc721.ownerOf(2), address(tokenOwner)); + assertEq(erc721.ownerOf(3), address(tokenOwner)); + assertEq(erc721.ownerOf(4), address(tokenOwner)); + assertEq(erc721.ownerOf(5), address(tokenOwner)); + + // ERC1155 balance + assertEq(erc1155.balanceOf(address(tokenOwner), 0), 100); + assertEq(erc1155.balanceOf(address(pack), 0), 0); + + assertEq(erc1155.balanceOf(address(tokenOwner), 1), 500); + assertEq(erc1155.balanceOf(address(pack), 1), 0); + + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(1); + + vm.prank(address(tokenOwner)); + (, uint256 totalSupply) = pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + // ERC20 balance + assertEq(erc20.balanceOf(address(tokenOwner)), 0); + assertEq(erc20.balanceOf(address(pack)), 2000 ether); + + // ERC721 balance + assertEq(erc721.ownerOf(0), address(pack)); + assertEq(erc721.ownerOf(1), address(pack)); + assertEq(erc721.ownerOf(2), address(pack)); + assertEq(erc721.ownerOf(3), address(pack)); + assertEq(erc721.ownerOf(4), address(pack)); + assertEq(erc721.ownerOf(5), address(pack)); + + // ERC1155 balance + assertEq(erc1155.balanceOf(address(tokenOwner), 0), 0); + assertEq(erc1155.balanceOf(address(pack), 0), 100); + + assertEq(erc1155.balanceOf(address(tokenOwner), 1), 0); + assertEq(erc1155.balanceOf(address(pack), 1), 500); + + // Pack wrapped token balance + assertEq(pack.balanceOf(address(recipient), packId), totalSupply); + } + + /** + * note: Testing revert condition; token owner calls `createPack` to pack owned tokens, without MINTER_ROLE. + */ + function test_revert_createPack_access_MINTER_ROLE() public { + vm.prank(address(tokenOwner)); + pack.renounceRole(keccak256("MINTER_ROLE"), address(tokenOwner)); + + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + vm.expectRevert( + abi.encodeWithSelector( + Permissions.PermissionsUnauthorizedAccount.selector, + address(tokenOwner), + keccak256("MINTER_ROLE") + ) + ); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` with insufficient value when packing native tokens. + */ + function test_revert_createPack_nativeTokens_insufficientValue() public { + address recipient = address(0x123); + + vm.deal(address(tokenOwner), 100 ether); + + packContents.push( + ITokenBundle.Token({ + assetContract: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 20 ether + }) + ); + numOfRewardUnits.push(1); + + vm.prank(address(tokenOwner)); + vm.expectRevert( + abi.encodeWithSelector(CurrencyTransferLib.CurrencyTransferLibMismatchedValue.selector, 0, 20 ether) + ); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` to pack un-owned ERC20 tokens. + */ + function test_revert_createPack_notOwner_ERC20() public { + tokenOwner.transferERC20(address(erc20), address(0x12), 1000 ether); + + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("ERC20: transfer amount exceeds balance"); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` to pack un-owned ERC721 tokens. + */ + function test_revert_createPack_notOwner_ERC721() public { + tokenOwner.transferERC721(address(erc721), address(0x12), 0); + + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("ERC721: caller is not token owner or approved"); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` to pack un-owned ERC1155 tokens. + */ + function test_revert_createPack_notOwner_ERC1155() public { + tokenOwner.transferERC1155(address(erc1155), address(0x12), 0, 100, ""); + + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("ERC1155: insufficient balance for transfer"); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` to pack un-approved ERC20 tokens. + */ + function test_revert_createPack_notApprovedTransfer_ERC20() public { + tokenOwner.setAllowanceERC20(address(erc20), address(pack), 0); + + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("ERC20: insufficient allowance"); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` to pack un-approved ERC721 tokens. + */ + function test_revert_createPack_notApprovedTransfer_ERC721() public { + tokenOwner.setApprovalForAllERC721(address(erc721), address(pack), false); + + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("ERC721: caller is not token owner or approved"); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` to pack un-approved ERC1155 tokens. + */ + function test_revert_createPack_notApprovedTransfer_ERC1155() public { + tokenOwner.setApprovalForAllERC1155(address(erc1155), address(pack), false); + + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("ERC1155: caller is not token owner or approved"); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` with invalid token-type. + */ + function test_revert_createPack_invalidTokenType() public { + ITokenBundle.Token[] memory invalidContent = new ITokenBundle.Token[](1); + uint256[] memory rewardUnits = new uint256[](1); + + invalidContent[0] = ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 1 + }); + rewardUnits[0] = 1; + + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("!TokenType"); + pack.createPack(invalidContent, rewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` with total-amount as 0. + */ + function test_revert_createPack_zeroTotalAmount() public { + ITokenBundle.Token[] memory invalidContent = new ITokenBundle.Token[](1); + uint256[] memory rewardUnits = new uint256[](1); + + invalidContent[0] = ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 0 + }); + rewardUnits[0] = 10; + + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("0 amt"); + pack.createPack(invalidContent, rewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` with no tokens to pack. + */ + function test_revert_createPack_noTokensToPack() public { + ITokenBundle.Token[] memory emptyContent; + uint256[] memory rewardUnits; + + address recipient = address(0x123); + + bytes memory err = "!Len"; + vm.startPrank(address(tokenOwner)); + vm.expectRevert(err); + pack.createPack(emptyContent, rewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` with unequal length of contents and rewardUnits. + */ + function test_revert_createPack_invalidRewardUnits() public { + uint256[] memory rewardUnits; + + address recipient = address(0x123); + + bytes memory err = "!Len"; + vm.startPrank(address(tokenOwner)); + vm.expectRevert(err); + pack.createPack(packContents, rewardUnits, packUri, 0, 1, recipient); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `openPackAndClaimRewards` + //////////////////////////////////////////////////////////////*/ + + /** + * note: Testing state changes; pack owner calls `openPack` to redeem underlying rewards. + */ + function test_state_openPackAndClaimRewards() public { + vm.warp(1000); + uint256 packId = pack.nextTokenIdToMint(); + uint256 packsToOpen = 3; + address recipient = address(1); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); + + vm.prank(recipient, recipient); + uint256 requestId = pack.openPackAndClaimRewards(packId, packsToOpen, 2_500_000); + console2.log("request ID for opening pack:", requestId); + + uint256[] memory randomValues = new uint256[](1); + randomValues[0] = 12345678; + + ITokenBundle.Token[] memory emptyRewardUnitsForTestingEvent; + + vm.expectEmit(true, true, false, false); + emit PackOpened(packId, recipient, 1, emptyRewardUnitsForTestingEvent); + + vm.prank(vrfV2Wrapper); + pack.rawFulfillRandomWords(requestId, randomValues); + + assertFalse(pack.canClaimRewards(recipient)); + } + + /** + * note: Testing state changes; pack owner calls `openPack` to redeem underlying rewards. + */ + function test_state_openPackAndClaimRewards_lowGasFailsafe() public { + vm.warp(1000); + uint256 packId = pack.nextTokenIdToMint(); + uint256 packsToOpen = 3; + address recipient = address(1); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); + + vm.prank(recipient, recipient); + uint256 requestId = pack.openPackAndClaimRewards(packId, packsToOpen, 2); + console2.log("request ID for opening pack:", requestId); + + uint256[] memory randomValues = new uint256[](1); + randomValues[0] = 12345678; + + // check state before + assertFalse(pack.canClaimRewards(recipient)); + console.log(pack.canClaimRewards(recipient)); + + // mock the call with low gas, causing revert in _claimRewards + vm.prank(vrfV2Wrapper); + pack.rawFulfillRandomWords{ gas: 100_000 }(requestId, randomValues); + + // check state after + assertTrue(pack.canClaimRewards(recipient)); + console.log(pack.canClaimRewards(recipient)); + } + + /** + * note: Cannot open pack again while a previous openPack request is in flight. + */ + function test_revert_openPackAndClaimRewards_ReqInFlight() public { + vm.warp(1000); + uint256 packId = pack.nextTokenIdToMint(); + uint256 packsToOpen = 3; + address recipient = address(1); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); + + vm.prank(recipient, recipient); + uint256 requestId = pack.openPackAndClaimRewards(packId, packsToOpen, 2_500_000); + console2.log("request ID for opening pack:", requestId); + + vm.expectRevert("ReqInFlight"); + + vm.prank(recipient, recipient); + pack.openPackAndClaimRewards(packId, packsToOpen, 2_500_000); + + vm.expectRevert("ReqInFlight"); + + vm.prank(recipient, recipient); + pack.openPack(packId, packsToOpen); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `openPack` + //////////////////////////////////////////////////////////////*/ + + /** + * note: Testing state changes; pack owner calls `openPack` to redeem underlying rewards. + */ + function test_state_openPack() public { + vm.warp(1000); + uint256 packId = pack.nextTokenIdToMint(); + uint256 packsToOpen = 3; + address recipient = address(1); + + vm.prank(address(tokenOwner)); + (, uint256 totalSupply) = pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); + + vm.prank(recipient, recipient); + uint256 requestId = pack.openPack(packId, packsToOpen); + console2.log("request ID for opening pack:", requestId); + + uint256[] memory randomValues = new uint256[](1); + randomValues[0] = 12345678; + + vm.prank(vrfV2Wrapper); + pack.rawFulfillRandomWords(requestId, randomValues); + + vm.prank(recipient, recipient); + ITokenBundle.Token[] memory rewardUnits = pack.claimRewards(); + console2.log("total reward units: ", rewardUnits.length); + + for (uint256 i = 0; i < rewardUnits.length; i++) { + console2.log("----- reward unit number: ", i, "------"); + console2.log("asset contract: ", rewardUnits[i].assetContract); + console2.log("token type: ", uint256(rewardUnits[i].tokenType)); + console2.log("tokenId: ", rewardUnits[i].tokenId); + if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC20) { + console2.log("total amount: ", rewardUnits[i].totalAmount / 1 ether, "ether"); + } else { + console2.log("total amount: ", rewardUnits[i].totalAmount); + } + console2.log(""); + } + + assertEq(packUri, pack.uri(packId)); + assertEq(pack.totalSupply(packId), totalSupply - packsToOpen); + + (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); + assertEq(packed.length, packContents.length); + } + + /** + * note: Testing event emission; pack owner calls `openPack` to open owned packs. + */ + function test_event_openPack() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + vm.expectEmit(true, true, false, false); + emit PackOpenRequested(recipient, packId, 1, VRFV2Wrapper(vrfV2Wrapper).lastRequestId()); + + vm.prank(recipient, recipient); + uint256 requestId = pack.openPack(packId, 1); + + vm.expectEmit(true, true, false, true); + emit PackRandomnessFulfilled(packId, requestId); + + uint256[] memory randomValues = new uint256[](1); + randomValues[0] = 12345678; + + vm.prank(vrfV2Wrapper); + pack.rawFulfillRandomWords(requestId, randomValues); + + ITokenBundle.Token[] memory emptyRewardUnitsForTestingEvent; + + vm.expectEmit(true, true, false, false); + emit PackOpened(packId, recipient, 1, emptyRewardUnitsForTestingEvent); + + vm.prank(recipient, recipient); + pack.claimRewards(); + } + + function test_balances_openPack() public { + uint256 packId = pack.nextTokenIdToMint(); + uint256 packsToOpen = 3; + address recipient = address(1); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); + + // ERC20 balance + assertEq(erc20.balanceOf(address(recipient)), 0); + assertEq(erc20.balanceOf(address(pack)), 2000 ether); + + // ERC721 balance + assertEq(erc721.ownerOf(0), address(pack)); + assertEq(erc721.ownerOf(1), address(pack)); + assertEq(erc721.ownerOf(2), address(pack)); + assertEq(erc721.ownerOf(3), address(pack)); + assertEq(erc721.ownerOf(4), address(pack)); + assertEq(erc721.ownerOf(5), address(pack)); + + // ERC1155 balance + assertEq(erc1155.balanceOf(address(recipient), 0), 0); + assertEq(erc1155.balanceOf(address(pack), 0), 100); + + assertEq(erc1155.balanceOf(address(recipient), 1), 0); + assertEq(erc1155.balanceOf(address(pack), 1), 500); + + vm.prank(recipient, recipient); + uint256 requestId = pack.openPack(packId, packsToOpen); + console2.log("request ID for opening pack:", requestId); + + uint256[] memory randomValues = new uint256[](1); + randomValues[0] = 12345678; + + vm.prank(vrfV2Wrapper); + pack.rawFulfillRandomWords(requestId, randomValues); + + vm.prank(recipient, recipient); + ITokenBundle.Token[] memory rewardUnits = pack.claimRewards(); + console2.log("total reward units: ", rewardUnits.length); + + uint256 erc20Amount; + uint256[] memory erc1155Amounts = new uint256[](2); + uint256 erc721Amount; + + for (uint256 i = 0; i < rewardUnits.length; i++) { + console2.log("----- reward unit number: ", i, "------"); + console2.log("asset contract: ", rewardUnits[i].assetContract); + console2.log("token type: ", uint256(rewardUnits[i].tokenType)); + console2.log("tokenId: ", rewardUnits[i].tokenId); + if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC20) { + console2.log("total amount: ", rewardUnits[i].totalAmount / 1 ether, "ether"); + console.log("balance of recipient: ", erc20.balanceOf(address(recipient)) / 1 ether, "ether"); + erc20Amount += rewardUnits[i].totalAmount; + } else if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC1155) { + console2.log("total amount: ", rewardUnits[i].totalAmount); + console.log("balance of recipient: ", erc1155.balanceOf(address(recipient), rewardUnits[i].tokenId)); + erc1155Amounts[rewardUnits[i].tokenId] += rewardUnits[i].totalAmount; + } else if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC721) { + console2.log("total amount: ", rewardUnits[i].totalAmount); + console.log("balance of recipient: ", erc721.balanceOf(address(recipient))); + erc721Amount += rewardUnits[i].totalAmount; + } + console2.log(""); + } + + assertEq(erc20.balanceOf(address(recipient)), erc20Amount); + assertEq(erc721.balanceOf(address(recipient)), erc721Amount); + + for (uint256 i = 0; i < erc1155Amounts.length; i += 1) { + assertEq(erc1155.balanceOf(address(recipient), i), erc1155Amounts[i]); + } + } + + /** + * note: Testing revert condition; caller of `openPack` is not EOA. + */ + function test_revert_openPack_notEOA() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + vm.startPrank(recipient, address(27)); + string memory err = "!EOA"; + vm.expectRevert(bytes(err)); + pack.openPack(packId, 1); + } + + /** + * note: Cannot open pack again while a previous openPack request is in flight. + */ + function test_revert_openPack_ReqInFlight() public { + vm.warp(1000); + uint256 packId = pack.nextTokenIdToMint(); + uint256 packsToOpen = 3; + address recipient = address(1); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); + + vm.prank(recipient, recipient); + uint256 requestId = pack.openPack(packId, packsToOpen); + console2.log("request ID for opening pack:", requestId); + + vm.expectRevert("ReqInFlight"); + + vm.prank(recipient, recipient); + pack.openPack(packId, packsToOpen); + + vm.expectRevert("ReqInFlight"); + + vm.prank(recipient, recipient); + pack.openPackAndClaimRewards(packId, packsToOpen, 2_500_000); + } + + /** + * note: Testing revert condition; pack owner calls `openPack` to open more than owned packs. + */ + function test_revert_openPack_openMoreThanOwned() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + (, uint256 totalSupply) = pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + bytes memory err = "!Bal"; + vm.startPrank(recipient, recipient); + vm.expectRevert(err); + pack.openPack(packId, totalSupply + 1); + } + + /** + * note: Testing revert condition; pack owner calls `openPack` before start timestamp. + */ + function test_revert_openPack_openBeforeStart() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(0x123); + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 1000, 1, recipient); + + vm.startPrank(recipient, recipient); + vm.expectRevert("!Open"); + pack.openPack(packId, 1); + } + + /** + * note: Testing revert condition; pack owner calls `openPack` with pack-id non-existent or not owned. + */ + function test_revert_openPack_invalidPackId() public { + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + bytes memory err = "!Bal"; + vm.startPrank(recipient, recipient); + vm.expectRevert(err); + pack.openPack(2, 1); + } + + /*/////////////////////////////////////////////////////////////// + Fuzz testing + //////////////////////////////////////////////////////////////*/ + + uint256 internal constant MAX_TOKENS = 2000; + + function getTokensToPack( + uint256 len + ) internal returns (ITokenBundle.Token[] memory tokensToPack, uint256[] memory rewardUnits) { + vm.assume(len < MAX_TOKENS); + tokensToPack = new ITokenBundle.Token[](len); + rewardUnits = new uint256[](len); + + for (uint256 i = 0; i < len; i += 1) { + uint256 random = uint256(keccak256(abi.encodePacked(len + i))) % MAX_TOKENS; + uint256 selector = random % 4; + + if (selector == 0) { + tokensToPack[i] = ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: (random + 1) * 10 ether + }); + rewardUnits[i] = random + 1; + + erc20.mint(address(tokenOwner), tokensToPack[i].totalAmount); + } else if (selector == 1) { + uint256 tokenId = erc721.nextTokenIdToMint(); + + tokensToPack[i] = ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: tokenId, + totalAmount: 1 + }); + rewardUnits[i] = 1; + + erc721.mint(address(tokenOwner), 1); + } else if (selector == 2) { + tokensToPack[i] = ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: random, + totalAmount: (random + 1) * 10 + }); + rewardUnits[i] = random + 1; + + erc1155.mint(address(tokenOwner), tokensToPack[i].tokenId, tokensToPack[i].totalAmount); + } else if (selector == 3) { + tokensToPack[i] = ITokenBundle.Token({ + assetContract: address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 5 ether + }); + rewardUnits[i] = 5; + } + } + } + + function checkBalances( + ITokenBundle.Token[] memory rewardUnits, + address + ) + internal + pure + returns (uint256 nativeTokenAmount, uint256 erc20Amount, uint256[] memory erc1155Amounts, uint256 erc721Amount) + { + erc1155Amounts = new uint256[](MAX_TOKENS); + + for (uint256 i = 0; i < rewardUnits.length; i++) { + // console2.log("----- reward unit number: ", i, "------"); + // console2.log("asset contract: ", rewardUnits[i].assetContract); + // console2.log("token type: ", uint256(rewardUnits[i].tokenType)); + // console2.log("tokenId: ", rewardUnits[i].tokenId); + if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC20) { + if (rewardUnits[i].assetContract == address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) { + // console2.log("total amount: ", rewardUnits[i].totalAmount / 1 ether, "ether"); + // console.log("balance of recipient: ", address(recipient).balance); + nativeTokenAmount += rewardUnits[i].totalAmount; + } else { + // console2.log("total amount: ", rewardUnits[i].totalAmount / 1 ether, "ether"); + // console.log("balance of recipient: ", erc20.balanceOf(address(recipient)) / 1 ether, "ether"); + erc20Amount += rewardUnits[i].totalAmount; + } + } else if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC1155) { + // console2.log("total amount: ", rewardUnits[i].totalAmount); + // console.log("balance of recipient: ", erc1155.balanceOf(address(recipient), rewardUnits[i].tokenId)); + erc1155Amounts[rewardUnits[i].tokenId] += rewardUnits[i].totalAmount; + } else if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC721) { + // console2.log("total amount: ", rewardUnits[i].totalAmount); + // console.log("balance of recipient: ", erc721.balanceOf(address(recipient))); + erc721Amount += rewardUnits[i].totalAmount; + } + // console2.log(""); + } + } + + function test_fuzz_state_createPack(uint256 x, uint128 y) public { + (ITokenBundle.Token[] memory tokensToPack, uint256[] memory rewardUnits) = getTokensToPack(x); + if (tokensToPack.length == 0) { + return; + } + + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(0x123); + uint256 totalRewardUnits; + uint256 nativeTokenPacked; + + for (uint256 i = 0; i < tokensToPack.length; i += 1) { + totalRewardUnits += rewardUnits[i]; + if (tokensToPack[i].assetContract == address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) { + nativeTokenPacked += tokensToPack[i].totalAmount; + } + } + vm.deal(address(tokenOwner), nativeTokenPacked); + vm.assume(y > 0 && totalRewardUnits % y == 0); + + vm.prank(address(tokenOwner)); + (, uint256 totalSupply) = pack.createPack{ value: nativeTokenPacked }( + tokensToPack, + rewardUnits, + packUri, + 0, + y, + recipient + ); + console2.log("total supply: ", totalSupply); + console2.log("total reward units: ", totalRewardUnits); + + assertEq(packId + 1, pack.nextTokenIdToMint()); + + (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); + assertEq(packed.length, tokensToPack.length); + for (uint256 i = 0; i < packed.length; i += 1) { + assertEq(packed[i].assetContract, tokensToPack[i].assetContract); + assertEq(uint256(packed[i].tokenType), uint256(tokensToPack[i].tokenType)); + assertEq(packed[i].tokenId, tokensToPack[i].tokenId); + assertEq(packed[i].totalAmount, tokensToPack[i].totalAmount); + } + + assertEq(packUri, pack.uri(packId)); + } + + /*/////////////////////////////////////////////////////////////// + Scenario/Exploit tests + //////////////////////////////////////////////////////////////*/ + /** + * note: Testing revert condition; token owner calls `createPack` to pack owned tokens. + */ + function test_revert_createPack_reentrancy() public { + MaliciousERC20 malERC20 = new MaliciousERC20(payable(address(pack))); + ITokenBundle.Token[] memory content = new ITokenBundle.Token[](1); + uint256[] memory rewards = new uint256[](1); + + malERC20.mint(address(tokenOwner), 10 ether); + content[0] = ITokenBundle.Token({ + assetContract: address(malERC20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 10 ether + }); + rewards[0] = 10; + + tokenOwner.setAllowanceERC20(address(malERC20), address(pack), 10 ether); + + address recipient = address(0x123); + + vm.prank(address(deployer)); + pack.grantRole(keccak256("MINTER_ROLE"), address(malERC20)); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("ReentrancyGuard: reentrant call"); + pack.createPack(content, rewards, packUri, 0, 1, recipient); + } +} + +contract MaliciousERC20 is MockERC20, ITokenBundle { + Pack public pack; + + constructor(address payable _pack) { + pack = Pack(_pack); + } + + function transferFrom(address from, address to, uint256 amount) public override returns (bool) { + ITokenBundle.Token[] memory content = new ITokenBundle.Token[](1); + uint256[] memory rewards = new uint256[](1); + + address recipient = address(0x123); + pack.createPack(content, rewards, "", 0, 1, recipient); + return super.transferFrom(from, to, amount); + } +} diff --git a/src/test/plugin/Map.t.sol b/src/test/plugin/Map.t.sol new file mode 100644 index 000000000..1888b9841 --- /dev/null +++ b/src/test/plugin/Map.t.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { PluginMap, IPluginMap } from "contracts/extension/plugin/PluginMap.sol"; +import "../utils/BaseTest.sol"; + +contract MapTest is BaseTest { + using Strings for uint256; + PluginMap internal map; + + address[] private pluginAddresses; + IPluginMap.Plugin[] private plugins; + + function setUp() public override { + super.setUp(); + + uint256 total = 50; + + address pluginAddress; + + for (uint256 i = 0; i < total; i += 1) { + if (i % 10 == 0) { + pluginAddress = address(uint160(0x50000 + i)); + pluginAddresses.push(pluginAddress); + } + plugins.push( + IPluginMap.Plugin(bytes4(keccak256(abi.encodePacked(i.toString()))), i.toString(), pluginAddress) + ); + } + + map = new PluginMap(plugins); + } + + function test_state_getPluginForFunction() external { + uint256 len = plugins.length; + for (uint256 i = 0; i < len; i += 1) { + address pluginAddress = plugins[i].pluginAddress; + bytes4 selector = plugins[i].functionSelector; + + assertEq(pluginAddress, map.getPluginForFunction(selector)); + } + } + + function test_state_getAllFunctionsOfPlugin() external { + uint256 len = plugins.length; + for (uint256 i = 0; i < len; i += 1) { + address pluginAddress = plugins[i].pluginAddress; + + uint256 expectedNum; + + for (uint256 j = 0; j < plugins.length; j += 1) { + if (plugins[j].pluginAddress == pluginAddress) { + expectedNum += 1; + } + } + + bytes4[] memory expectedFns = new bytes4[](expectedNum); + uint256 idx; + + for (uint256 j = 0; j < plugins.length; j += 1) { + if (plugins[j].pluginAddress == pluginAddress) { + expectedFns[idx] = plugins[j].functionSelector; + idx += 1; + } + } + + bytes4[] memory fns = map.getAllFunctionsOfPlugin(pluginAddress); + + assertEq(fns.length, expectedNum); + + for (uint256 k = 0; k < fns.length; k += 1) { + assertEq(fns[k], expectedFns[k]); + } + } + } + + function test_state_getAllPlugins() external { + IPluginMap.Plugin[] memory pluginsStored = map.getAllPlugins(); + + for (uint256 i = 0; i < pluginsStored.length; i += 1) { + assertEq(pluginsStored[i].pluginAddress, plugins[i].pluginAddress); + assertEq(pluginsStored[i].functionSelector, plugins[i].functionSelector); + } + } +} diff --git a/src/test/plugin/Router.t.sol b/src/test/plugin/Router.t.sol new file mode 100644 index 000000000..532f0e1cb --- /dev/null +++ b/src/test/plugin/Router.t.sol @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "contracts/extension/plugin/PluginMap.sol"; +import "contracts/extension/plugin/Router.sol"; +import { BaseTest } from "../utils/BaseTest.sol"; +import "lib/forge-std/src/console.sol"; + +contract RouterImplementation is Router { + constructor(address _functionMap) Router(_functionMap) {} + + function _canSetPlugin() internal pure override returns (bool) { + return true; + } +} + +library CounterStorage { + /// @custom:storage-location erc7201:counter.storage + /// @dev keccak256(abi.encode(uint256(keccak256("counter.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant COUNTER_STORAGE_POSITION = + 0x3a8940d2c88113c2296117248b8b2aedcf41634993b4c0b4ea1a36805e66c300; + + struct Data { + uint256 number; + } + + function counterStorage() internal pure returns (Data storage counterData) { + bytes32 position = COUNTER_STORAGE_POSITION; + assembly { + counterData.slot := position + } + } +} + +contract Counter { + function number() external view returns (uint256) { + CounterStorage.Data storage data = CounterStorage.counterStorage(); + return data.number; + } + + function setNumber(uint256 _newNum) external { + CounterStorage.Data storage data = CounterStorage.counterStorage(); + data.number = _newNum; + } + + function doubleNumber() external { + CounterStorage.Data storage data = CounterStorage.counterStorage(); + data.number *= 4; // Buggy! + } + + function extraFunction() external pure {} +} + +contract CounterAlternate1 { + function doubleNumber() external { + CounterStorage.Data storage data = CounterStorage.counterStorage(); + data.number *= 2; // Fixed! + } +} + +contract CounterAlternate2 { + function tripleNumber() external { + CounterStorage.Data storage data = CounterStorage.counterStorage(); + data.number *= 3; // Fixed! + } +} + +contract RouterTest is BaseTest { + address internal map; + address internal router; + + address internal counter; + address internal counterAlternate1; + address internal counterAlternate2; + + function setUp() public override { + super.setUp(); + + counter = address(new Counter()); + counterAlternate1 = address(new CounterAlternate1()); + counterAlternate2 = address(new CounterAlternate2()); + + IPluginMap.Plugin[] memory pluginMaps = new IPluginMap.Plugin[](3); + pluginMaps[0] = IPluginMap.Plugin(Counter.number.selector, "number()", counter); + pluginMaps[1] = IPluginMap.Plugin(Counter.setNumber.selector, "setNumber(uint256)", counter); + pluginMaps[2] = IPluginMap.Plugin(Counter.doubleNumber.selector, "doubleNumber()", counter); + + map = address(new PluginMap(pluginMaps)); + router = address(new RouterImplementation(map)); + } + + function test_state_addPlugin() external { + // Set number. + uint256 num = 5; + Counter(router).setNumber(num); + assertEq(Counter(router).number(), num); + + // Add extension for `tripleNumber`. + RouterImplementation(payable(router)).addPlugin( + IPluginMap.Plugin(CounterAlternate2.tripleNumber.selector, "tripleNumber()", counterAlternate2) + ); + + // Triple number. + CounterAlternate2(router).tripleNumber(); + assertEq(Counter(router).number(), num * 3); + + // Get and check all overriden extensions. + IPluginMap.Plugin[] memory pluginsStored = RouterImplementation(payable(router)).getAllPlugins(); + assertEq(pluginsStored.length, 4); + + bool isStored; + + for (uint256 i = 0; i < pluginsStored.length; i += 1) { + if (pluginsStored[i].functionSelector == CounterAlternate2.tripleNumber.selector) { + isStored = true; + assertEq(pluginsStored[i].pluginAddress, counterAlternate2); + } + } + + assertTrue(isStored); + } + + function test_revert_addPlugin_defaultExists() external { + vm.expectRevert("Router: default plugin exists for function."); + RouterImplementation(payable(router)).addPlugin( + IPluginMap.Plugin(Counter.doubleNumber.selector, "doubleNumber()", counterAlternate1) + ); + } + + function test_revert_addPlugin_pluginAlreadyExists() external { + RouterImplementation(payable(router)).addPlugin( + IPluginMap.Plugin(CounterAlternate2.tripleNumber.selector, "tripleNumber()", counterAlternate2) + ); + vm.expectRevert(); + RouterImplementation(payable(router)).addPlugin( + IPluginMap.Plugin(CounterAlternate2.tripleNumber.selector, "tripleNumber()", counterAlternate2) + ); + } + + function test_revert_addPlugin_selectorSignatureMismatch() external { + vm.expectRevert("Router: fn selector and signature mismatch."); + RouterImplementation(payable(router)).addPlugin( + IPluginMap.Plugin(CounterAlternate2.tripleNumber.selector, "doubleNumber()", counterAlternate2) + ); + } + + function test_state_updatePlugin() external { + // Set number. + uint256 num = 5; + Counter(router).setNumber(num); + assertEq(Counter(router).number(), num); + + // Double number. Bug: it quadruples the number. + Counter(router).doubleNumber(); + assertEq(Counter(router).number(), num * 4); + + // Reset number. + Counter(router).setNumber(num); + assertEq(Counter(router).number(), num); + + // Fix the extension for `doubleNumber`. + RouterImplementation(payable(router)).updatePlugin( + IPluginMap.Plugin(Counter.doubleNumber.selector, "doubleNumber()", counterAlternate1) + ); + + // Double number. Fixed: it doubles the number. + Counter(router).doubleNumber(); + assertEq(Counter(router).number(), num * 2); + + // Get and check all overriden extensions. + assertEq( + RouterImplementation(payable(router)).getPluginForFunction(Counter.doubleNumber.selector), + counterAlternate1 + ); + + IPluginMap.Plugin[] memory pluginsStored = RouterImplementation(payable(router)).getAllPlugins(); + assertEq(pluginsStored.length, 3); + + bool isStored; + + for (uint256 i = 0; i < pluginsStored.length; i += 1) { + if (pluginsStored[i].functionSelector == Counter.doubleNumber.selector) { + assertEq(pluginsStored[i].pluginAddress, counterAlternate1); + isStored = true; + } + } + + assertTrue(isStored); + } + + function test_state_getAllFunctionsOfPlugin() public { + // add fixed function from counterAlternate + RouterImplementation(payable(router)).updatePlugin( + IPluginMap.Plugin(Counter.doubleNumber.selector, "doubleNumber()", counterAlternate1) + ); + + // add previously not added function of counter + RouterImplementation(payable(router)).addPlugin( + IPluginMap.Plugin(Counter.extraFunction.selector, "extraFunction()", counter) + ); + + // check plugins for counter + bytes4[] memory functions = RouterImplementation(payable(router)).getAllFunctionsOfPlugin(counter); + assertEq(functions.length, 4); + console.logBytes4(functions[0]); + console.logBytes4(functions[1]); + console.logBytes4(functions[2]); + console.logBytes4(functions[3]); + + // check plugins for counterAlternate + functions = RouterImplementation(payable(router)).getAllFunctionsOfPlugin(counterAlternate1); + assertEq(functions.length, 1); + console.logBytes4(functions[0]); + } + + function test_revert_updatePlugin_selectorSignatureMismatch() external { + vm.expectRevert("Router: fn selector and signature mismatch."); + RouterImplementation(payable(router)).updatePlugin( + IPluginMap.Plugin(CounterAlternate1.doubleNumber.selector, "tripleNumber()", counterAlternate2) + ); + } + + function test_revert_updatePlugin_functionDNE() external { + vm.expectRevert("Map: No plugin available for selector"); + RouterImplementation(payable(router)).updatePlugin( + IPluginMap.Plugin(CounterAlternate2.tripleNumber.selector, "tripleNumber()", counterAlternate2) + ); + } + + function test_state_removePlugin() external { + RouterImplementation(payable(router)).addPlugin( + IPluginMap.Plugin(CounterAlternate2.tripleNumber.selector, "tripleNumber()", counterAlternate2) + ); + + assertEq( + RouterImplementation(payable(router)).getPluginForFunction(CounterAlternate2.tripleNumber.selector), + counterAlternate2 + ); + + RouterImplementation(payable(router)).removePlugin(CounterAlternate2.tripleNumber.selector); + + vm.expectRevert("Map: No plugin available for selector"); + RouterImplementation(payable(router)).getPluginForFunction(CounterAlternate2.tripleNumber.selector); + } + + function test_revert_removePlugin_pluginDNE() external { + vm.expectRevert("Router: No plugin available for selector"); + RouterImplementation(payable(router)).removePlugin(CounterAlternate2.tripleNumber.selector); + } + + function test_state_getPluginForFunction() public { + // add fixed function from counterAlternate + RouterImplementation(payable(router)).updatePlugin( + IPluginMap.Plugin(Counter.doubleNumber.selector, "doubleNumber()", counterAlternate1) + ); + + // add previously not added function of counter + RouterImplementation(payable(router)).addPlugin( + IPluginMap.Plugin(Counter.extraFunction.selector, "extraFunction()", counter) + ); + + address pluginAddress = RouterImplementation(payable(router)).getPluginForFunction( + Counter.doubleNumber.selector + ); + assertEq(pluginAddress, counterAlternate1); + + pluginAddress = RouterImplementation(payable(router)).getPluginForFunction(Counter.extraFunction.selector); + assertEq(pluginAddress, counter); + } +} diff --git a/src/test/plugin/RouterImmutable.t.sol b/src/test/plugin/RouterImmutable.t.sol new file mode 100644 index 000000000..8ad44f4fc --- /dev/null +++ b/src/test/plugin/RouterImmutable.t.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "contracts/extension/plugin/PluginMap.sol"; +import "contracts/extension/plugin/RouterImmutable.sol"; +import { BaseTest } from "../utils/BaseTest.sol"; + +contract Counter { + uint256 private number_; + + function number() external view returns (uint256) { + return number_; + } + + function setNumber(uint256 _newNum) external { + number_ = _newNum; + } + + function doubleNumber() external { + number_ *= 2; + } +} + +contract RouterImmutableTest is BaseTest { + address internal map; + address internal router; + + function setUp() public override { + super.setUp(); + + address counter = address(new Counter()); + + IPluginMap.Plugin[] memory pluginMaps = new IPluginMap.Plugin[](2); + pluginMaps[0] = IPluginMap.Plugin(Counter.number.selector, "number()", counter); + pluginMaps[1] = IPluginMap.Plugin(Counter.setNumber.selector, "setNumber(uint256)", counter); + + map = address(new PluginMap(pluginMaps)); + router = address(new RouterImmutable(map)); + } + + function test_state_callWithRouter() external { + uint256 num = 5; + + Counter(router).setNumber(num); + + assertEq(Counter(router).number(), num); + } + + function test_revert_callWithRouter() external { + vm.expectRevert("Map: No plugin available for selector"); + Counter(router).doubleNumber(); + } +} diff --git a/src/test/scripts/generateRoot.ts b/src/test/scripts/generateRoot.ts index 5459320a5..128499938 100644 --- a/src/test/scripts/generateRoot.ts +++ b/src/test/scripts/generateRoot.ts @@ -1,4 +1,5 @@ -const { MerkleTree } = require("merkletreejs"); +const { MerkleTree } = require("@thirdweb-dev/merkletree"); + const keccak256 = require("keccak256"); const { ethers } = require("ethers"); @@ -6,13 +7,20 @@ const process = require("process"); const members = [ "0x92Bb439374a091c7507bE100183d8D1Ed2c9dAD3", - "0xD0d82c095d184e6E2c8B72689c9171DE59FFd28d", - "0xFD78F7E2dF2B8c3D5bff0413c96f3237500898B3", + "0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd", + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", ]; let val = process.argv[2]; +let price = process.argv[3]; +let currency = process.argv[4]; -const hashedLeafs = members.map(l => ethers.utils.solidityKeccak256(["address", "uint256"], [l, val])); +const hashedLeafs = members.map(l => + ethers.utils.solidityKeccak256( + ["address", "uint256", "uint256", "address"], + [l, val, price, currency], + ), +); const tree = new MerkleTree(hashedLeafs, keccak256, { sort: true, diff --git a/src/test/scripts/generateRootAirdrop.ts b/src/test/scripts/generateRootAirdrop.ts new file mode 100644 index 000000000..4e2c1fdfb --- /dev/null +++ b/src/test/scripts/generateRootAirdrop.ts @@ -0,0 +1,26 @@ +const { MerkleTree } = require("@thirdweb-dev/merkletree"); + +const keccak256 = require("keccak256"); +const { ethers } = require("ethers"); + +const process = require("process"); + +const members = [ + "0x92Bb439374a091c7507bE100183d8D1Ed2c9dAD3", + "0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd", + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", +]; + +let val = process.argv[2]; + +const hashedLeafs = members.map(l => + ethers.utils.solidityKeccak256(["address", "uint256"], [l, val]), +); + +const tree = new MerkleTree(hashedLeafs, keccak256, { + sort: true, + sortLeaves: true, + sortPairs: true, +}); + +process.stdout.write(ethers.utils.defaultAbiCoder.encode(["bytes32"], [tree.getHexRoot()])); diff --git a/src/test/scripts/generateRootAirdrop1155.ts b/src/test/scripts/generateRootAirdrop1155.ts new file mode 100644 index 000000000..d76990fad --- /dev/null +++ b/src/test/scripts/generateRootAirdrop1155.ts @@ -0,0 +1,27 @@ +const { MerkleTree } = require("@thirdweb-dev/merkletree"); + +const keccak256 = require("keccak256"); +const { ethers } = require("ethers"); + +const process = require("process"); + +const members = [ + "0x9999999999999999999999999999999999999999", + "0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd", + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", +]; + +let tokenId = process.argv[2]; +let quantity = process.argv[3]; + +const hashedLeafs = members.map(l => + ethers.utils.solidityKeccak256(["address", "uint256", "uint256"], [l, tokenId, quantity]), +); + +const tree = new MerkleTree(hashedLeafs, keccak256, { + sort: true, + sortLeaves: true, + sortPairs: true, +}); + +process.stdout.write(ethers.utils.defaultAbiCoder.encode(["bytes32"], [tree.getHexRoot()])); diff --git a/src/test/scripts/getCloneAddress.ts b/src/test/scripts/getCloneAddress.ts new file mode 100644 index 000000000..27ade61fd --- /dev/null +++ b/src/test/scripts/getCloneAddress.ts @@ -0,0 +1,23 @@ +const { MerkleTree } = require("@thirdweb-dev/merkletree"); + +const keccak256 = require("keccak256"); +const { ethers } = require("ethers"); + +const process = require("process"); + +let implementationAddress = process.argv[2]; +let signer = process.argv[3]; +let salthash = process.argv[4]; + +const cloneBytecode = [ + "0x3d602d80600a3d3981f3363d3d373d3d3d363d73", + implementationAddress.replace(/0x/, "").toLowerCase(), + "5af43d82803e903d91602b57fd5bf3", +].join(""); + +const initCodeHash = ethers.utils.solidityKeccak256(["bytes"], [cloneBytecode]); + +const create2Address = ethers.utils.getCreate2Address(signer, salthash, initCodeHash); + +process.stdout.write(create2Address); +// process.stdout.write(ethers.utils.defaultAbiCoder.encode(["bytes32"], [create2Address])); diff --git a/src/test/scripts/getProof.ts b/src/test/scripts/getProof.ts index 873db7178..2ef4fd4c5 100644 --- a/src/test/scripts/getProof.ts +++ b/src/test/scripts/getProof.ts @@ -1,4 +1,5 @@ -const { MerkleTree } = require("merkletreejs"); +const { MerkleTree } = require("@thirdweb-dev/merkletree"); + const keccak256 = require("keccak256"); const { ethers } = require("ethers"); @@ -6,13 +7,20 @@ const process = require("process"); const members = [ "0x92Bb439374a091c7507bE100183d8D1Ed2c9dAD3", - "0xD0d82c095d184e6E2c8B72689c9171DE59FFd28d", - "0xFD78F7E2dF2B8c3D5bff0413c96f3237500898B3", + "0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd", + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", ]; let val = process.argv[2]; +let price = process.argv[3]; +let currency = process.argv[4]; -const hashedLeafs = members.map(l => ethers.utils.solidityKeccak256(["address", "uint256"], [l, val])); +const hashedLeafs = members.map(l => + ethers.utils.solidityKeccak256( + ["address", "uint256", "uint256", "address"], + [l, val, price, currency], + ), +); const tree = new MerkleTree(hashedLeafs, keccak256, { sort: true, @@ -20,6 +28,11 @@ const tree = new MerkleTree(hashedLeafs, keccak256, { sortPairs: true, }); -const expectedProof = tree.getHexProof(ethers.utils.solidityKeccak256(["address", "uint256"], [members[0], val])); +const expectedProof = tree.getHexProof( + ethers.utils.solidityKeccak256( + ["address", "uint256", "uint256", "address"], + [members[1], val, price, currency], + ), +); process.stdout.write(ethers.utils.defaultAbiCoder.encode(["bytes32[]"], [expectedProof])); diff --git a/src/test/scripts/getProofAirdrop.ts b/src/test/scripts/getProofAirdrop.ts new file mode 100644 index 000000000..f99582602 --- /dev/null +++ b/src/test/scripts/getProofAirdrop.ts @@ -0,0 +1,30 @@ +const { MerkleTree } = require("@thirdweb-dev/merkletree"); + +const keccak256 = require("keccak256"); +const { ethers } = require("ethers"); + +const process = require("process"); + +const members = [ + "0x92Bb439374a091c7507bE100183d8D1Ed2c9dAD3", + "0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd", + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", +]; + +let val = process.argv[2]; + +const hashedLeafs = members.map(l => + ethers.utils.solidityKeccak256(["address", "uint256"], [l, val]), +); + +const tree = new MerkleTree(hashedLeafs, keccak256, { + sort: true, + sortLeaves: true, + sortPairs: true, +}); + +const expectedProof = tree.getHexProof( + ethers.utils.solidityKeccak256(["address", "uint256"], [members[1], val]), +); + +process.stdout.write(ethers.utils.defaultAbiCoder.encode(["bytes32[]"], [expectedProof])); diff --git a/src/test/scripts/getProofAirdrop1155.ts b/src/test/scripts/getProofAirdrop1155.ts new file mode 100644 index 000000000..6701fc479 --- /dev/null +++ b/src/test/scripts/getProofAirdrop1155.ts @@ -0,0 +1,31 @@ +const { MerkleTree } = require("@thirdweb-dev/merkletree"); + +const keccak256 = require("keccak256"); +const { ethers } = require("ethers"); + +const process = require("process"); + +const members = [ + "0x9999999999999999999999999999999999999999", + "0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd", + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", +]; + +let tokenId = process.argv[2]; +let quantity = process.argv[3]; + +const hashedLeafs = members.map(l => + ethers.utils.solidityKeccak256(["address", "uint256", "uint256"], [l, tokenId, quantity]), +); + +const tree = new MerkleTree(hashedLeafs, keccak256, { + sort: true, + sortLeaves: true, + sortPairs: true, +}); + +const expectedProof = tree.getHexProof( + ethers.utils.solidityKeccak256(["address", "uint256", "uint256"], [members[1], tokenId, quantity]), +); + +process.stdout.write(ethers.utils.defaultAbiCoder.encode(["bytes32[]"], [expectedProof])); diff --git a/src/test/sdk/base/BaseUtilTest.sol b/src/test/sdk/base/BaseUtilTest.sol new file mode 100644 index 000000000..7ae0b458d --- /dev/null +++ b/src/test/sdk/base/BaseUtilTest.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; +import "../../utils/Wallet.sol"; +import "../../mocks/WETH9.sol"; +import "../../mocks/MockERC20.sol"; +import "../../mocks/MockERC721.sol"; +import "../../mocks/MockERC1155.sol"; +import "contracts/infra/forwarder/Forwarder.sol"; + +abstract contract BaseUtilTest is DSTest, Test { + string public constant NAME = "NAME"; + string public constant SYMBOL = "SYMBOL"; + string public constant CONTRACT_URI = "CONTRACT_URI"; + address public constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + MockERC20 public erc20; + MockERC721 public erc721; + MockERC1155 public erc1155; + WETH9 public weth; + + address public forwarder; + + address public deployer = address(0x20000); + address public saleRecipient = address(0x30000); + address public royaltyRecipient = address(0x30001); + address public platformFeeRecipient = address(0x30002); + uint128 public royaltyBps = 500; // 5% + uint128 public platformFeeBps = 500; // 5% + uint256 public constant MAX_BPS = 10_000; // 100% + + uint256 public privateKey = 1234; + address public signer; + + mapping(bytes32 => address) public contracts; + + function setUp() public virtual { + signer = vm.addr(privateKey); + + erc20 = new MockERC20(); + erc721 = new MockERC721(); + erc1155 = new MockERC1155(); + weth = new WETH9(); + forwarder = address(new Forwarder()); + } + + function getActor(uint160 _index) public pure returns (address) { + return address(uint160(0x50000 + _index)); + } + + function getWallet() public returns (Wallet wallet) { + wallet = new Wallet(); + } + + function assertIsOwnerERC721(address _token, address _owner, uint256[] memory _tokenIds) internal { + for (uint256 i = 0; i < _tokenIds.length; i += 1) { + bool isOwnerOfToken = MockERC721(_token).ownerOf(_tokenIds[i]) == _owner; + assertTrue(isOwnerOfToken); + } + } + + function assertIsNotOwnerERC721(address _token, address _owner, uint256[] memory _tokenIds) internal { + for (uint256 i = 0; i < _tokenIds.length; i += 1) { + bool isOwnerOfToken = MockERC721(_token).ownerOf(_tokenIds[i]) == _owner; + assertTrue(!isOwnerOfToken); + } + } + + function assertBalERC1155Eq( + address _token, + address _owner, + uint256[] memory _tokenIds, + uint256[] memory _amounts + ) internal { + require(_tokenIds.length == _amounts.length, "unequal lengths"); + + for (uint256 i = 0; i < _tokenIds.length; i += 1) { + assertEq(MockERC1155(_token).balanceOf(_owner, _tokenIds[i]), _amounts[i]); + } + } + + function assertBalERC1155Gte( + address _token, + address _owner, + uint256[] memory _tokenIds, + uint256[] memory _amounts + ) internal { + require(_tokenIds.length == _amounts.length, "unequal lengths"); + + for (uint256 i = 0; i < _tokenIds.length; i += 1) { + assertTrue(MockERC1155(_token).balanceOf(_owner, _tokenIds[i]) >= _amounts[i]); + } + } + + function assertBalERC20Eq(address _token, address _owner, uint256 _amount) internal { + assertEq(MockERC20(_token).balanceOf(_owner), _amount); + } + + function assertBalERC20Gte(address _token, address _owner, uint256 _amount) internal { + assertTrue(MockERC20(_token).balanceOf(_owner) >= _amount); + } + + function forwarders() public view returns (address[] memory) { + address[] memory _forwarders = new address[](1); + _forwarders[0] = forwarder; + return _forwarders; + } +} diff --git a/src/test/sdk/base/ERC1155Base.t.sol b/src/test/sdk/base/ERC1155Base.t.sol new file mode 100644 index 000000000..f6976a3da --- /dev/null +++ b/src/test/sdk/base/ERC1155Base.t.sol @@ -0,0 +1,498 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { ERC1155Base } from "contracts/base/ERC1155Base.sol"; +import { Strings } from "contracts/lib/Strings.sol"; + +contract ERC1155BaseTest is DSTest, Test { + using Strings for uint256; + + // Target contract + ERC1155Base internal base; + + // Signers + address internal admin; + address internal nftHolder; + + function setUp() public { + admin = address(0x123); + nftHolder = address(0x456); + + vm.prank(admin); + base = new ERC1155Base(admin, "name", "symbol", admin, 0); + } + + // ================== `mintTo` tests ======================== + + function test_state_mintTo_newNFTs() public { + uint256 tokenId = type(uint256).max; + string memory tokenURI = "ipfs://"; + uint256 amount = 100; + + uint256 expectedTokenIdMinted = base.nextTokenIdToMint(); + + vm.prank(admin); + base.mintTo(nftHolder, tokenId, tokenURI, amount); + + assertEq(base.balanceOf(nftHolder, expectedTokenIdMinted), amount); + assertEq(base.totalSupply(expectedTokenIdMinted), amount); + assertEq(base.nextTokenIdToMint(), expectedTokenIdMinted + 1); + assertEq(base.uri(expectedTokenIdMinted), tokenURI); + } + + function test_state_mintTo_existingNFTs() public { + string memory tokenURI = "ipfs://"; + uint256 startAmount = 1; + + uint256 tokenIdMinted = base.nextTokenIdToMint(); + + vm.prank(admin); + base.mintTo(admin, type(uint256).max, tokenURI, startAmount); + + assertEq(base.uri(tokenIdMinted), tokenURI); + assertEq(base.totalSupply(tokenIdMinted), startAmount); + assertEq(base.nextTokenIdToMint(), tokenIdMinted + 1); + + uint256 additionalAmount = 100; + + vm.prank(admin); + base.mintTo(nftHolder, tokenIdMinted, "", additionalAmount); + + assertEq(base.balanceOf(nftHolder, tokenIdMinted), additionalAmount); + assertEq(base.totalSupply(tokenIdMinted), additionalAmount + startAmount); + } + + function test_revert_mintTo_unauthorizedCaller() public { + uint256 tokenId = type(uint256).max; + string memory tokenURI = "ipfs://"; + uint256 amount = 100; + + vm.prank(nftHolder); + vm.expectRevert("Not authorized to mint."); + base.mintTo(nftHolder, tokenId, tokenURI, amount); + } + + function test_revert_mintTo_invalidId() public { + string memory tokenURI = "ipfs://"; + uint256 amount = 100; + + uint256 nextId = base.nextTokenIdToMint(); + + vm.prank(admin); + vm.expectRevert("invalid id"); + base.mintTo(nftHolder, nextId, tokenURI, amount); + } + + // ================== `mintTo` tests ======================== + + function test_state_batchMintTo_newNFTs() public { + uint256 numToMint = 3; + uint256[] memory tokenIds = new uint256[](numToMint); + uint256[] memory amounts = new uint256[](numToMint); + uint256[] memory expectedTokenIds = new uint256[](numToMint); + string memory baseURI = "ipfs://"; + + uint256 nextId = base.nextTokenIdToMint(); + + for (uint256 i = 0; i < numToMint; i += 1) { + tokenIds[i] = type(uint256).max; + amounts[i] = 100; + + expectedTokenIds[i] = nextId; + nextId += 1; + } + + vm.prank(admin); + base.batchMintTo(nftHolder, tokenIds, amounts, baseURI); + + for (uint256 i = 0; i < numToMint; i += 1) { + uint256 id = expectedTokenIds[i]; + + assertEq(base.balanceOf(nftHolder, id), amounts[i]); + assertEq(base.totalSupply(id), amounts[i]); + assertEq(base.uri(id), string(abi.encodePacked(baseURI, id.toString()))); + } + } + + function test_state_batchMintTo_existingNFTs() public { + uint256 numToMint = 3; + uint256[] memory tokenIds = new uint256[](numToMint); + uint256[] memory startAmounts = new uint256[](numToMint); + uint256[] memory nextAmounts = new uint256[](numToMint); + uint256[] memory expectedTokenIds = new uint256[](numToMint); + string memory baseURI = "ipfs://"; + + uint256 nextId = base.nextTokenIdToMint(); + + for (uint256 i = 0; i < numToMint; i += 1) { + tokenIds[i] = type(uint256).max; + startAmounts[i] = 1; + nextAmounts[i] = 99; + + expectedTokenIds[i] = nextId; + nextId += 1; + } + + vm.prank(admin); + base.batchMintTo(admin, tokenIds, startAmounts, baseURI); + + for (uint256 i = 0; i < numToMint; i += 1) { + uint256 id = expectedTokenIds[i]; + + assertEq(base.balanceOf(admin, id), startAmounts[i]); + assertEq(base.totalSupply(id), startAmounts[i]); + assertEq(base.uri(id), string(abi.encodePacked(baseURI, id.toString()))); + } + + vm.prank(admin); + base.batchMintTo(nftHolder, expectedTokenIds, nextAmounts, ""); + + for (uint256 i = 0; i < numToMint; i += 1) { + uint256 id = expectedTokenIds[i]; + + assertEq(base.balanceOf(nftHolder, id), nextAmounts[i]); + assertEq(base.totalSupply(id), startAmounts[i] + nextAmounts[i]); + assertEq(base.uri(id), string(abi.encodePacked(baseURI, id.toString()))); + } + } + + function test_state_batchMintTo_newAndExistingNFTs() public { + uint256 numToMint = 3; + uint256[] memory tokenIds = new uint256[](numToMint); + uint256[] memory startAmounts = new uint256[](numToMint); + uint256[] memory nextAmounts = new uint256[](numToMint); + uint256[] memory expectedTokenIds = new uint256[](numToMint); + string memory baseURI = "ipfs://"; + + uint256 nextId = base.nextTokenIdToMint(); + + for (uint256 i = 0; i < numToMint; i += 1) { + tokenIds[i] = type(uint256).max; + startAmounts[i] = 1; + nextAmounts[i] = 99; + + expectedTokenIds[i] = nextId; + nextId += 1; + } + + vm.prank(admin); + base.batchMintTo(admin, tokenIds, startAmounts, baseURI); + + for (uint256 i = 0; i < numToMint; i += 1) { + uint256 id = expectedTokenIds[i]; + + assertEq(base.balanceOf(admin, id), startAmounts[i]); + assertEq(base.totalSupply(id), startAmounts[i]); + assertEq(base.uri(id), string(abi.encodePacked(baseURI, id.toString()))); + } + + uint256[] memory newAndExistingTokenIds = new uint256[](numToMint + 1); + uint256[] memory newAmounts = new uint256[](numToMint + 1); + for (uint256 i = 0; i < numToMint; i += 1) { + newAndExistingTokenIds[i] = expectedTokenIds[i]; + newAmounts[i] = nextAmounts[i]; + } + newAndExistingTokenIds[numToMint] = type(uint256).max; + newAmounts[numToMint] = 100; + + uint256 expectedNewId = base.nextTokenIdToMint(); + string memory baseURIForNewNFT = "newipfs://"; + + vm.prank(admin); + base.batchMintTo(nftHolder, newAndExistingTokenIds, newAmounts, baseURIForNewNFT); + + for (uint256 i = 0; i < newAndExistingTokenIds.length; i += 1) { + if (i < numToMint) { + uint256 id = newAndExistingTokenIds[i]; + + assertEq(base.balanceOf(nftHolder, id), newAmounts[i]); + assertEq(base.totalSupply(id), startAmounts[i] + newAmounts[i]); + assertEq(base.uri(id), string(abi.encodePacked(baseURI, id.toString()))); + } else { + uint256 id = expectedNewId; + assertEq(base.balanceOf(nftHolder, id), newAmounts[i]); + assertEq(base.totalSupply(id), newAmounts[i]); + assertEq(base.uri(id), string(abi.encodePacked(baseURIForNewNFT, id.toString()))); + } + } + } + + function test_revert_batchMintTo_unauthorizedCaller() public { + uint256 numToMint = 3; + uint256[] memory tokenIds = new uint256[](numToMint); + uint256[] memory amounts = new uint256[](numToMint); + uint256[] memory expectedTokenIds = new uint256[](numToMint); + string memory baseURI = "ipfs://"; + + uint256 nextId = base.nextTokenIdToMint(); + + for (uint256 i = 0; i < numToMint; i += 1) { + tokenIds[i] = type(uint256).max; + amounts[i] = 100; + + expectedTokenIds[i] = nextId; + nextId += 1; + } + + vm.prank(nftHolder); + vm.expectRevert("Not authorized to mint."); + base.batchMintTo(nftHolder, tokenIds, amounts, baseURI); + } + + function test_revert_batchMintTo_mintingZeroTokens() public { + uint256 numToMint = 3; + uint256[] memory tokenIds = new uint256[](numToMint); + uint256[] memory amounts = new uint256[](0); + uint256[] memory expectedTokenIds = new uint256[](numToMint); + string memory baseURI = "ipfs://"; + + uint256 nextId = base.nextTokenIdToMint(); + + for (uint256 i = 0; i < numToMint; i += 1) { + tokenIds[i] = type(uint256).max; + + expectedTokenIds[i] = nextId; + nextId += 1; + } + + vm.prank(admin); + vm.expectRevert("Minting zero tokens."); + base.batchMintTo(nftHolder, tokenIds, amounts, baseURI); + } + + function test_revert_batchMintTo_lengthMismatch() public { + uint256 numToMint = 3; + uint256[] memory tokenIds = new uint256[](numToMint + 1); + uint256[] memory amounts = new uint256[](numToMint); + uint256[] memory expectedTokenIds = new uint256[](numToMint); + string memory baseURI = "ipfs://"; + + uint256 nextId = base.nextTokenIdToMint(); + + for (uint256 i = 0; i < numToMint; i += 1) { + tokenIds[i] = type(uint256).max; + amounts[i] = 100; + + expectedTokenIds[i] = nextId; + nextId += 1; + } + + vm.prank(admin); + vm.expectRevert("Length mismatch."); + base.batchMintTo(nftHolder, tokenIds, amounts, baseURI); + } + + function test_revert_batchMintTo_invalidId() public { + uint256 numToMint = 3; + uint256[] memory tokenIds = new uint256[](numToMint); + uint256[] memory amounts = new uint256[](numToMint); + uint256[] memory expectedTokenIds = new uint256[](numToMint); + string memory baseURI = "ipfs://"; + + uint256 nextId = base.nextTokenIdToMint(); + + for (uint256 i = 0; i < numToMint; i += 1) { + tokenIds[i] = i; + amounts[i] = 100; + + expectedTokenIds[i] = nextId; + nextId += 1; + } + + vm.prank(admin); + vm.expectRevert("invalid id"); + base.batchMintTo(nftHolder, tokenIds, amounts, baseURI); + } + + function test_state_burn() public { + uint256 tokenId = type(uint256).max; + string memory tokenURI = "ipfs://"; + uint256 amount = 100; + + uint256 expectedTokenIdMinted = base.nextTokenIdToMint(); + + vm.prank(admin); + base.mintTo(nftHolder, tokenId, tokenURI, amount); + + assertEq(base.balanceOf(nftHolder, expectedTokenIdMinted), amount); + assertEq(base.totalSupply(expectedTokenIdMinted), amount); + + vm.prank(nftHolder); + base.burn(nftHolder, expectedTokenIdMinted, amount); + + assertEq(base.balanceOf(nftHolder, expectedTokenIdMinted), 0); + assertEq(base.totalSupply(expectedTokenIdMinted), 0); + } + + function test_revert_burn_unapprovedCaller() public { + uint256 tokenId = type(uint256).max; + string memory tokenURI = "ipfs://"; + uint256 amount = 100; + + uint256 expectedTokenIdMinted = base.nextTokenIdToMint(); + + vm.prank(admin); + base.mintTo(nftHolder, tokenId, tokenURI, amount); + + assertEq(base.balanceOf(nftHolder, expectedTokenIdMinted), amount); + assertEq(base.totalSupply(expectedTokenIdMinted), amount); + + vm.prank(admin); + vm.expectRevert("Unapproved caller"); + base.burn(nftHolder, expectedTokenIdMinted, amount); + } + + function test_revert_burn_notEnoughTokensOwned() public { + uint256 tokenId = type(uint256).max; + string memory tokenURI = "ipfs://"; + uint256 amount = 100; + + uint256 expectedTokenIdMinted = base.nextTokenIdToMint(); + + vm.prank(admin); + base.mintTo(nftHolder, tokenId, tokenURI, amount); + + assertEq(base.balanceOf(nftHolder, expectedTokenIdMinted), amount); + assertEq(base.totalSupply(expectedTokenIdMinted), amount); + + vm.prank(nftHolder); + vm.expectRevert("Not enough tokens owned"); + base.burn(nftHolder, expectedTokenIdMinted, amount + 1); + } + + function test_state_burnBatch() public { + uint256 numToMint = 3; + uint256[] memory tokenIds = new uint256[](numToMint); + uint256[] memory amounts = new uint256[](numToMint); + uint256[] memory expectedTokenIds = new uint256[](numToMint); + string memory baseURI = "ipfs://"; + + uint256 nextId = base.nextTokenIdToMint(); + + for (uint256 i = 0; i < numToMint; i += 1) { + tokenIds[i] = type(uint256).max; + amounts[i] = 100; + + expectedTokenIds[i] = nextId; + nextId += 1; + } + + vm.prank(admin); + base.batchMintTo(nftHolder, tokenIds, amounts, baseURI); + + for (uint256 i = 0; i < numToMint; i += 1) { + uint256 id = expectedTokenIds[i]; + + assertEq(base.balanceOf(nftHolder, id), amounts[i]); + assertEq(base.totalSupply(id), amounts[i]); + } + + vm.prank(nftHolder); + base.burnBatch(nftHolder, expectedTokenIds, amounts); + + for (uint256 i = 0; i < numToMint; i += 1) { + uint256 id = expectedTokenIds[i]; + + assertEq(base.balanceOf(nftHolder, id), 0); + assertEq(base.totalSupply(id), 0); + } + } + + function test_revert_burnBatch_unapprovedCaller() public { + uint256 numToMint = 3; + uint256[] memory tokenIds = new uint256[](numToMint); + uint256[] memory amounts = new uint256[](numToMint); + uint256[] memory expectedTokenIds = new uint256[](numToMint); + string memory baseURI = "ipfs://"; + + uint256 nextId = base.nextTokenIdToMint(); + + for (uint256 i = 0; i < numToMint; i += 1) { + tokenIds[i] = type(uint256).max; + amounts[i] = 100; + + expectedTokenIds[i] = nextId; + nextId += 1; + } + + vm.prank(admin); + base.batchMintTo(nftHolder, tokenIds, amounts, baseURI); + + for (uint256 i = 0; i < numToMint; i += 1) { + uint256 id = expectedTokenIds[i]; + + assertEq(base.balanceOf(nftHolder, id), amounts[i]); + assertEq(base.totalSupply(id), amounts[i]); + } + + vm.prank(admin); + vm.expectRevert("Unapproved caller"); + base.burnBatch(nftHolder, expectedTokenIds, amounts); + } + + function test_revert_burnBatch_lengthMismatch() public { + uint256 numToMint = 3; + uint256[] memory tokenIds = new uint256[](numToMint); + uint256[] memory amounts = new uint256[](numToMint); + uint256[] memory mockAmounts = new uint256[](numToMint + 1); + uint256[] memory expectedTokenIds = new uint256[](numToMint); + string memory baseURI = "ipfs://"; + + uint256 nextId = base.nextTokenIdToMint(); + + for (uint256 i = 0; i < numToMint; i += 1) { + tokenIds[i] = type(uint256).max; + amounts[i] = 100; + mockAmounts[i] = 100; + + expectedTokenIds[i] = nextId; + nextId += 1; + } + + vm.prank(admin); + base.batchMintTo(nftHolder, tokenIds, amounts, baseURI); + + for (uint256 i = 0; i < numToMint; i += 1) { + uint256 id = expectedTokenIds[i]; + + assertEq(base.balanceOf(nftHolder, id), amounts[i]); + assertEq(base.totalSupply(id), amounts[i]); + } + + vm.prank(nftHolder); + vm.expectRevert("Length mismatch"); + base.burnBatch(nftHolder, expectedTokenIds, mockAmounts); + } + + function test_revert_burnBatch_notEnoughTokensOwned() public { + uint256 numToMint = 3; + uint256[] memory tokenIds = new uint256[](numToMint); + uint256[] memory amounts = new uint256[](numToMint); + uint256[] memory expectedTokenIds = new uint256[](numToMint); + string memory baseURI = "ipfs://"; + + uint256 nextId = base.nextTokenIdToMint(); + + for (uint256 i = 0; i < numToMint; i += 1) { + tokenIds[i] = type(uint256).max; + amounts[i] = 100; + + expectedTokenIds[i] = nextId; + nextId += 1; + } + + vm.prank(admin); + base.batchMintTo(nftHolder, tokenIds, amounts, baseURI); + + for (uint256 i = 0; i < numToMint; i += 1) { + amounts[i] += 1; + } + + vm.prank(nftHolder); + vm.expectRevert("Not enough tokens owned"); + base.burnBatch(nftHolder, expectedTokenIds, amounts); + } +} diff --git a/src/test/sdk/base/ERC1155DelayedReveal.t.sol b/src/test/sdk/base/ERC1155DelayedReveal.t.sol new file mode 100644 index 000000000..ffc0e1a06 --- /dev/null +++ b/src/test/sdk/base/ERC1155DelayedReveal.t.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { ERC1155DelayedReveal } from "contracts/base/ERC1155DelayedReveal.sol"; +import { Strings } from "contracts/lib/Strings.sol"; + +contract ERC1155DelayedRevealTest is DSTest, Test { + using Strings for uint256; + + // Target contract + ERC1155DelayedReveal internal base; + + // Signers + address internal admin; + address internal nftHolder; + + // Lazy mitning args + uint256 internal lazymintAmount = 10; + string internal baseURI = "ipfs://"; + string internal placeholderURI = "placeholderURI://"; + bytes internal key = "key"; + + function setUp() public { + admin = address(0x123); + nftHolder = address(0x456); + + vm.prank(admin); + base = new ERC1155DelayedReveal(admin, "name", "symbol", admin, 0); + + bytes memory encryptedBaseURI = base.encryptDecrypt(bytes(baseURI), key); + bytes32 provenanceHash = keccak256(abi.encodePacked(baseURI, key, block.chainid)); + vm.prank(admin); + base.lazyMint(lazymintAmount, placeholderURI, abi.encode(encryptedBaseURI, provenanceHash)); + } + + function test_state_reveal() public { + uint256 nextId = base.nextTokenIdToMint(); + + for (uint256 i = 0; i < nextId; i += 1) { + assertEq(base.uri(i), string(abi.encodePacked(placeholderURI, "0"))); + } + + vm.prank(admin); + base.reveal(0, key); + + for (uint256 i = 0; i < nextId; i += 1) { + assertEq(base.uri(i), string(abi.encodePacked(baseURI, i.toString()))); + } + } + + function test_state_reveal_additionalBatch() public { + uint256 nextIdBefore = base.nextTokenIdToMint(); + + string memory newBaseURI = "ipfsNew://"; + string memory newPlaceholderURI = "placeholderURINew://"; + bytes memory newKey = "newkey"; + + bytes memory encryptedBaseURI = base.encryptDecrypt(bytes(newBaseURI), newKey); + bytes32 provenanceHash = keccak256(abi.encodePacked(newBaseURI, newKey, block.chainid)); + vm.prank(admin); + base.lazyMint(lazymintAmount, newPlaceholderURI, abi.encode(encryptedBaseURI, provenanceHash)); + + uint256 nextId = base.nextTokenIdToMint(); + + for (uint256 i = nextIdBefore; i < nextId; i += 1) { + assertEq(base.uri(i), string(abi.encodePacked(newPlaceholderURI, "0"))); + } + + vm.prank(admin); + base.reveal(1, newKey); + + for (uint256 i = nextIdBefore; i < nextId; i += 1) { + assertEq(base.uri(i), string(abi.encodePacked(newBaseURI, i.toString()))); + } + } +} diff --git a/src/test/sdk/base/ERC1155Drop.t.sol b/src/test/sdk/base/ERC1155Drop.t.sol new file mode 100644 index 000000000..580fe3b77 --- /dev/null +++ b/src/test/sdk/base/ERC1155Drop.t.sol @@ -0,0 +1,490 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { ERC1155Drop } from "contracts/base/ERC1155Drop.sol"; +import { Strings } from "contracts/lib/Strings.sol"; + +contract ERC1155DropTest is DSTest, Test { + using Strings for uint256; + + // Target contract + ERC1155Drop internal base; + + // Signers + uint256 internal adminPkey; + uint256 internal nftHolderPkey; + + address internal admin; + address internal nftHolder; + address internal saleRecipient; + + ERC1155Drop.ClaimCondition condition; + ERC1155Drop.AllowlistProof allowlistProof; + + uint256 internal targetTokenId; + + function setUp() public { + adminPkey = 123; + nftHolderPkey = 456; + + admin = vm.addr(adminPkey); + nftHolder = vm.addr(nftHolderPkey); + saleRecipient = address(0x8910); + + vm.deal(nftHolder, 100 ether); + + vm.prank(admin); + base = new ERC1155Drop(admin, "name", "symbol", admin, 0, saleRecipient); + + targetTokenId = base.nextTokenIdToMint(); + + vm.prank(admin); + base.lazyMint(1, "ipfs://", ""); + } + + function test_state_setClaimConditions() public { + condition.startTimestamp = block.timestamp; + condition.maxClaimableSupply = 100; + condition.supplyClaimed = 0; + condition.quantityLimitPerWallet = 5; + condition.merkleRoot = bytes32(0); + condition.pricePerToken = 0; + condition.currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + (, uint256 maxClaimableSupplyBefore, , , , , , ) = base.claimCondition(targetTokenId); + + assertEq(maxClaimableSupplyBefore, 0); + + vm.prank(admin); + base.setClaimConditions(targetTokenId, condition, true); + + (, uint256 maxClaimable, , uint256 quantityLimitPerWallet, , , address currency, ) = base.claimCondition( + targetTokenId + ); + + assertEq(maxClaimable, 100); + assertEq(quantityLimitPerWallet, 5); + assertEq(currency, 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); + } + + function test_state_setClaimConditions_resetEligibility() public { + condition.startTimestamp = block.timestamp; + condition.maxClaimableSupply = 100; + condition.supplyClaimed = 0; + condition.quantityLimitPerWallet = 5; + condition.merkleRoot = bytes32(0); + condition.pricePerToken = 0; + condition.currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + vm.prank(admin); + base.setClaimConditions(targetTokenId, condition, false); + + vm.prank(nftHolder, nftHolder); + base.claim(nftHolder, targetTokenId, 1, condition.currency, condition.pricePerToken, allowlistProof, ""); + + (, , uint256 supplyClaimedBefore, , , , , ) = base.claimCondition(targetTokenId); + assertEq(supplyClaimedBefore, 1); + + vm.prank(admin); + base.setClaimConditions(targetTokenId, condition, false); + + (, , uint256 supplyClaimedAfter, , , , , ) = base.claimCondition(targetTokenId); + assertEq(supplyClaimedBefore, supplyClaimedAfter); + } + + function test_revert_setClaimConditions_unauthorizedCaller() public { + condition.startTimestamp = block.timestamp; + condition.maxClaimableSupply = 100; + condition.supplyClaimed = 0; + condition.quantityLimitPerWallet = 5; + condition.merkleRoot = bytes32(0); + condition.pricePerToken = 0; + condition.currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + (, uint256 maxClaimableSupplyBefore, , , , , , ) = base.claimCondition(targetTokenId); + + assertEq(maxClaimableSupplyBefore, 0); + + vm.prank(nftHolder); + vm.expectRevert("Not authorized"); + base.setClaimConditions(targetTokenId, condition, true); + } + + function test_revert_setClaimConditions_supplyClaimedAlready() public { + condition.startTimestamp = block.timestamp; + condition.maxClaimableSupply = 100; + condition.supplyClaimed = 0; + condition.quantityLimitPerWallet = 100; + condition.merkleRoot = bytes32(0); + condition.pricePerToken = 0; + condition.currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + vm.prank(admin); + base.setClaimConditions(targetTokenId, condition, false); + + vm.prank(nftHolder, nftHolder); + base.claim( + nftHolder, + targetTokenId, + condition.quantityLimitPerWallet, + condition.currency, + condition.pricePerToken, + allowlistProof, + "" + ); + + condition.maxClaimableSupply = 50; + + vm.prank(admin); + vm.expectRevert("max supply claimed"); + base.setClaimConditions(targetTokenId, condition, false); + } + + function test_state_claim() public { + condition.startTimestamp = block.timestamp; + condition.maxClaimableSupply = 100; + condition.supplyClaimed = 0; + condition.quantityLimitPerWallet = 5; + condition.merkleRoot = bytes32(0); + condition.pricePerToken = 0; + condition.currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + (, uint256 maxClaimableSupplyBefore, , , , , , ) = base.claimCondition(targetTokenId); + + assertEq(maxClaimableSupplyBefore, 0); + + vm.prank(admin); + base.setClaimConditions(targetTokenId, condition, true); + + uint256 quantityToClaim = 5; + vm.prank(nftHolder, nftHolder); + base.claim( + nftHolder, + targetTokenId, + quantityToClaim, + condition.currency, + condition.pricePerToken, + allowlistProof, + "" + ); + + (, , uint256 supplyClaimed, , , , , ) = base.claimCondition(targetTokenId); + assertEq(supplyClaimed, quantityToClaim); + + assertEq(base.balanceOf(nftHolder, targetTokenId), quantityToClaim); + assertEq(base.totalSupply(targetTokenId), quantityToClaim); + } + + function test_state_claim_withPrice() public { + condition.startTimestamp = block.timestamp; + condition.maxClaimableSupply = 100; + condition.supplyClaimed = 0; + condition.quantityLimitPerWallet = 5; + condition.merkleRoot = bytes32(0); + condition.pricePerToken = 0.01 ether; + condition.currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + (, uint256 maxClaimableSupplyBefore, , , , , , ) = base.claimCondition(targetTokenId); + + assertEq(maxClaimableSupplyBefore, 0); + + vm.prank(admin); + base.setClaimConditions(targetTokenId, condition, true); + + uint256 quantityToClaim = 5; + uint256 totalPrice = quantityToClaim * condition.pricePerToken; + + uint256 saleRecipientBalBefore = saleRecipient.balance; + + vm.prank(nftHolder, nftHolder); + base.claim{ value: totalPrice }( + nftHolder, + targetTokenId, + quantityToClaim, + condition.currency, + condition.pricePerToken, + allowlistProof, + "" + ); + + assertEq(saleRecipient.balance, saleRecipientBalBefore + totalPrice); + } + + function test_state_claim_withAllowlist() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "1"; + inputs[3] = "0"; + inputs[4] = "0x0000000000000000000000000000000000000000"; + + bytes memory result = vm.ffi(inputs); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + address claimer = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + + condition.startTimestamp = block.timestamp; + condition.maxClaimableSupply = 100; + condition.supplyClaimed = 0; + condition.quantityLimitPerWallet = 5; + condition.merkleRoot = root; + condition.pricePerToken = 0; + condition.currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + (, uint256 maxClaimableSupplyBefore, , , , , , ) = base.claimCondition(targetTokenId); + + assertEq(maxClaimableSupplyBefore, 0); + + vm.prank(admin); + base.setClaimConditions(targetTokenId, condition, true); + + allowlistProof.proof = proofs; + allowlistProof.quantityLimitPerWallet = 1; + allowlistProof.pricePerToken = 0; + allowlistProof.currency = address(0); + + uint256 quantityToClaim = allowlistProof.quantityLimitPerWallet; + + vm.prank(claimer, claimer); + base.claim( + nftHolder, + targetTokenId, + quantityToClaim, + condition.currency, + condition.pricePerToken, + allowlistProof, + "" + ); + + (, , uint256 supplyClaimed, , , , , ) = base.claimCondition(targetTokenId); + assertEq(supplyClaimed, quantityToClaim); + + assertEq(base.balanceOf(nftHolder, targetTokenId), quantityToClaim); + assertEq(base.totalSupply(targetTokenId), quantityToClaim); + } + + function test_revert_claim_invalidQtyProof() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "1"; + inputs[3] = "0"; + inputs[4] = "0x0000000000000000000000000000000000000000"; + + bytes memory result = vm.ffi(inputs); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + address claimer = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + + condition.startTimestamp = block.timestamp; + condition.maxClaimableSupply = 100; + condition.supplyClaimed = 0; + condition.quantityLimitPerWallet = 5; + condition.merkleRoot = root; + condition.pricePerToken = 0; + condition.currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + (, uint256 maxClaimableSupplyBefore, , , , , , ) = base.claimCondition(targetTokenId); + + assertEq(maxClaimableSupplyBefore, 0); + + vm.prank(admin); + base.setClaimConditions(targetTokenId, condition, true); + + allowlistProof.proof = proofs; + allowlistProof.quantityLimitPerWallet = 1; + allowlistProof.pricePerToken = 0; + allowlistProof.currency = address(0); + + uint256 quantityToClaim = allowlistProof.quantityLimitPerWallet + 1; + + bytes memory errorQty = "!Qty"; + + vm.prank(claimer, claimer); + vm.expectRevert(errorQty); + base.claim( + nftHolder, + targetTokenId, + quantityToClaim, + condition.currency, + condition.pricePerToken, + allowlistProof, + "" + ); + } + + function test_revert_claim_invalidPrice() public { + condition.startTimestamp = block.timestamp; + condition.maxClaimableSupply = 100; + condition.supplyClaimed = 0; + condition.quantityLimitPerWallet = 5; + condition.merkleRoot = bytes32(0); + condition.pricePerToken = 0.01 ether; + condition.currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + (, uint256 maxClaimableSupplyBefore, , , , , , ) = base.claimCondition(targetTokenId); + + assertEq(maxClaimableSupplyBefore, 0); + + vm.prank(admin); + base.setClaimConditions(targetTokenId, condition, true); + + uint256 quantityToClaim = 5; + uint256 totalPrice = quantityToClaim * condition.pricePerToken; + + vm.prank(nftHolder, nftHolder); + vm.expectRevert("!PriceOrCurrency"); + base.claim{ value: totalPrice - 1 }( + nftHolder, + targetTokenId, + quantityToClaim, + condition.currency, + 0, + allowlistProof, + "" + ); + } + + function test_revert_claim_insufficientPrice() public { + condition.startTimestamp = block.timestamp; + condition.maxClaimableSupply = 100; + condition.supplyClaimed = 0; + condition.quantityLimitPerWallet = 5; + condition.merkleRoot = bytes32(0); + condition.pricePerToken = 0.01 ether; + condition.currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + (, uint256 maxClaimableSupplyBefore, , , , , , ) = base.claimCondition(targetTokenId); + + assertEq(maxClaimableSupplyBefore, 0); + + vm.prank(admin); + base.setClaimConditions(targetTokenId, condition, true); + + uint256 quantityToClaim = 5; + uint256 totalPrice = quantityToClaim * condition.pricePerToken; + + vm.prank(nftHolder, nftHolder); + vm.expectRevert("Invalid msg value"); + base.claim{ value: totalPrice - 1 }( + nftHolder, + targetTokenId, + quantityToClaim, + condition.currency, + condition.pricePerToken, + allowlistProof, + "" + ); + } + + function test_revert_claim_invalidCurrency() public { + condition.startTimestamp = block.timestamp; + condition.maxClaimableSupply = 100; + condition.supplyClaimed = 0; + condition.quantityLimitPerWallet = 5; + condition.merkleRoot = bytes32(0); + condition.pricePerToken = 0.01 ether; + condition.currency = address(0x123); + + (, uint256 maxClaimableSupplyBefore, , , , , , ) = base.claimCondition(targetTokenId); + + assertEq(maxClaimableSupplyBefore, 0); + + vm.prank(admin); + base.setClaimConditions(targetTokenId, condition, true); + + uint256 quantityToClaim = 5; + uint256 totalPrice = quantityToClaim * condition.pricePerToken; + + vm.prank(nftHolder, nftHolder); + vm.expectRevert("!PriceOrCurrency"); + base.claim{ value: totalPrice }( + nftHolder, + targetTokenId, + quantityToClaim, + 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, + condition.pricePerToken, + allowlistProof, + "" + ); + } + + function test_revert_claim_invalidQuantity() public { + condition.startTimestamp = block.timestamp; + condition.maxClaimableSupply = 100; + condition.supplyClaimed = 0; + condition.quantityLimitPerWallet = 5; + condition.merkleRoot = bytes32(0); + condition.pricePerToken = 0.01 ether; + condition.currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + (, uint256 maxClaimableSupplyBefore, , , , , , ) = base.claimCondition(targetTokenId); + + assertEq(maxClaimableSupplyBefore, 0); + + vm.prank(admin); + base.setClaimConditions(targetTokenId, condition, true); + + uint256 quantityToClaim = condition.quantityLimitPerWallet + 1; + uint256 totalPrice = quantityToClaim * condition.pricePerToken; + + bytes memory errorQty = "!Qty"; + + vm.prank(nftHolder, nftHolder); + vm.expectRevert(errorQty); + base.claim{ value: totalPrice }( + nftHolder, + targetTokenId, + quantityToClaim, + condition.currency, + condition.pricePerToken, + allowlistProof, + "" + ); + } + + function test_revert_claim_exceedsMaxSupply() public { + condition.startTimestamp = block.timestamp; + condition.maxClaimableSupply = 100; + condition.supplyClaimed = 0; + condition.quantityLimitPerWallet = 101; + condition.merkleRoot = bytes32(0); + condition.pricePerToken = 0.01 ether; + condition.currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + (, uint256 maxClaimableSupplyBefore, , , , , , ) = base.claimCondition(targetTokenId); + + assertEq(maxClaimableSupplyBefore, 0); + + vm.prank(admin); + base.setClaimConditions(targetTokenId, condition, true); + + uint256 quantityToClaim = condition.quantityLimitPerWallet; + uint256 totalPrice = quantityToClaim * condition.pricePerToken; + + vm.prank(nftHolder, nftHolder); + vm.expectRevert("!MaxSupply"); + base.claim{ value: totalPrice }( + nftHolder, + targetTokenId, + quantityToClaim, + condition.currency, + condition.pricePerToken, + allowlistProof, + "" + ); + } +} diff --git a/src/test/sdk/base/ERC1155LazyMint.t.sol b/src/test/sdk/base/ERC1155LazyMint.t.sol new file mode 100644 index 000000000..3d83c1b94 --- /dev/null +++ b/src/test/sdk/base/ERC1155LazyMint.t.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { ERC1155LazyMint } from "contracts/base/ERC1155LazyMint.sol"; +import { Strings } from "contracts/lib/Strings.sol"; + +contract ERC1155LazyMintTest is DSTest, Test { + using Strings for uint256; + + // Target contract + ERC1155LazyMint internal base; + + // Signers + address internal admin; + address internal nftHolder; + + // Lazy mitning args + uint256 internal lazymintAmount = 10; + string internal baseURI = "ipfs://"; + + function setUp() public { + admin = address(0x123); + nftHolder = address(0x456); + + vm.prank(admin); + base = new ERC1155LazyMint(admin, "name", "symbol", admin, 0); + + // Lazy mint tokens + vm.prank(admin); + base.lazyMint(lazymintAmount, baseURI, ""); + + assertEq(base.nextTokenIdToMint(), lazymintAmount); + } + + function test_state_claim() public { + uint256 tokenId = 0; + uint256 amount = 100; + + vm.prank(nftHolder); + base.claim(nftHolder, tokenId, amount); + + assertEq(base.balanceOf(nftHolder, tokenId), amount); + assertEq(base.totalSupply(tokenId), amount); + assertEq(base.uri(tokenId), string(abi.encodePacked(baseURI, tokenId.toString()))); + } + + function test_revert_mintTo_invalidId() public { + uint256 tokenId = base.nextTokenIdToMint(); + uint256 amount = 100; + + vm.prank(nftHolder); + vm.expectRevert("invalid id"); + base.claim(nftHolder, tokenId, amount); + } +} diff --git a/src/test/sdk/base/ERC1155SignatureMint.t.sol b/src/test/sdk/base/ERC1155SignatureMint.t.sol new file mode 100644 index 000000000..77433ba98 --- /dev/null +++ b/src/test/sdk/base/ERC1155SignatureMint.t.sol @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { ERC1155SignatureMint } from "contracts/base/ERC1155SignatureMint.sol"; +import { Strings } from "contracts/lib/Strings.sol"; + +contract ERC1155SignatureMintTest is DSTest, Test { + using Strings for uint256; + + // Target contract + ERC1155SignatureMint internal base; + + // Signers + uint256 internal adminPkey; + uint256 internal nftHolderPkey; + + address internal admin; + address internal nftHolder; + address internal saleRecipient; + + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + ERC1155SignatureMint.MintRequest req; + + function signMintRequest( + ERC1155SignatureMint.MintRequest memory _request, + uint256 privateKey + ) internal view returns (bytes memory) { + bytes memory encodedRequest = bytes.concat( + abi.encode( + typehashMintRequest, + _request.to, + _request.royaltyRecipient, + _request.royaltyBps, + _request.primarySaleRecipient, + _request.tokenId, + keccak256(bytes(_request.uri)) + ), + abi.encode( + _request.quantity, + _request.pricePerToken, + _request.currency, + _request.validityStartTimestamp, + _request.validityEndTimestamp, + _request.uid + ) + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, typedDataHash); + bytes memory sig = abi.encodePacked(r, s, v); + + return sig; + } + + function setUp() public { + adminPkey = 123; + nftHolderPkey = 456; + + admin = vm.addr(adminPkey); + nftHolder = vm.addr(nftHolderPkey); + saleRecipient = address(0x8910); + + vm.deal(nftHolder, 100 ether); + + vm.prank(admin); + base = new ERC1155SignatureMint(admin, "name", "symbol", admin, 0, saleRecipient); + + typehashMintRequest = keccak256( + "MintRequest(address to,address royaltyRecipient,uint256 royaltyBps,address primarySaleRecipient,uint256 tokenId,string uri,uint256 quantity,uint256 pricePerToken,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + nameHash = keccak256(bytes("SignatureMintERC1155")); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256(abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(base))); + } + + function test_state_mintWithSignature_newNFTs() public { + req.to = nftHolder; + req.royaltyRecipient = admin; + req.royaltyBps = 0; + req.primarySaleRecipient = saleRecipient; + req.tokenId = type(uint256).max; + req.uri = "ipfs://"; + req.quantity = 100; + req.pricePerToken = 0; + req.currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + req.validityStartTimestamp = 0; + req.validityEndTimestamp = type(uint128).max; + req.uid = keccak256("uid"); + + bytes memory signature = signMintRequest(req, adminPkey); + + uint256 tokenId = base.nextTokenIdToMint(); + assertEq(base.totalSupply(tokenId), 0); + + vm.prank(nftHolder); + base.mintWithSignature(req, signature); + + assertEq(base.balanceOf(nftHolder, tokenId), req.quantity); + assertEq(base.totalSupply(tokenId), req.quantity); + assertEq(base.uri(tokenId), req.uri); + } + + function test_state_mintWithSignature_existingNFTs() public { + req.to = nftHolder; + req.royaltyRecipient = admin; + req.royaltyBps = 0; + req.primarySaleRecipient = saleRecipient; + req.tokenId = type(uint256).max; + req.uri = "ipfs://"; + req.quantity = 100; + req.pricePerToken = 0; + req.currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + req.validityStartTimestamp = 0; + req.validityEndTimestamp = type(uint128).max; + req.uid = keccak256("uid"); + + bytes memory signature = signMintRequest(req, adminPkey); + + uint256 tokenId = base.nextTokenIdToMint(); + assertEq(base.totalSupply(tokenId), 0); + + vm.prank(nftHolder); + base.mintWithSignature(req, signature); + + assertEq(base.balanceOf(nftHolder, tokenId), req.quantity); + assertEq(base.totalSupply(tokenId), req.quantity); + + req.tokenId = tokenId; + string memory originalURI = req.uri; + req.uri = "wrongURI://"; + req.uid = keccak256("new uid"); + + bytes memory signature2 = signMintRequest(req, adminPkey); + vm.prank(nftHolder); + base.mintWithSignature(req, signature2); + + assertEq(base.balanceOf(nftHolder, tokenId), req.quantity * 2); + assertEq(base.totalSupply(tokenId), req.quantity * 2); + assertEq(base.uri(tokenId), originalURI); + } + + function test_state_mintWithSignature_withPrice() public { + req.to = nftHolder; + req.royaltyRecipient = admin; + req.royaltyBps = 0; + req.primarySaleRecipient = saleRecipient; + req.tokenId = type(uint256).max; + req.uri = "ipfs://"; + req.quantity = 100; + req.pricePerToken = 0.01 ether; + req.currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + req.validityStartTimestamp = 0; + req.validityEndTimestamp = type(uint128).max; + req.uid = keccak256("uid"); + + uint256 saleRecipientBalBefore = saleRecipient.balance; + uint256 totalPrice = req.pricePerToken * req.quantity; + + bytes memory signature = signMintRequest(req, adminPkey); + vm.prank(nftHolder); + base.mintWithSignature{ value: totalPrice }(req, signature); + + assertEq(saleRecipient.balance, saleRecipientBalBefore + totalPrice); + } + + function test_revert_mintWithSignature_withPrice_incorrectPrice() public { + req.to = nftHolder; + req.royaltyRecipient = admin; + req.royaltyBps = 0; + req.primarySaleRecipient = saleRecipient; + req.tokenId = type(uint256).max; + req.uri = "ipfs://"; + req.quantity = 100; + req.pricePerToken = 0.01 ether; + req.currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + req.validityStartTimestamp = 0; + req.validityEndTimestamp = type(uint128).max; + req.uid = keccak256("uid"); + + uint256 totalPrice = req.pricePerToken * req.quantity; + bytes memory signature = signMintRequest(req, adminPkey); + vm.prank(nftHolder); + vm.expectRevert("Invalid msg value"); + base.mintWithSignature{ value: totalPrice - 1 }(req, signature); + } + + function test_revert_mintWithSignature_mintingZeroTokens() public { + req.to = nftHolder; + req.royaltyRecipient = admin; + req.royaltyBps = 0; + req.primarySaleRecipient = saleRecipient; + req.tokenId = type(uint256).max; + req.uri = "ipfs://"; + req.quantity = 0; + req.pricePerToken = 0; + req.currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + req.validityStartTimestamp = 0; + req.validityEndTimestamp = type(uint128).max; + req.uid = keccak256("uid"); + + bytes memory signature = signMintRequest(req, adminPkey); + vm.prank(nftHolder); + vm.expectRevert("Minting zero tokens."); + base.mintWithSignature(req, signature); + } + + function test_revert_mintWithSignature_invalidId() public { + uint256 nextId = base.nextTokenIdToMint(); + + req.to = nftHolder; + req.royaltyRecipient = admin; + req.royaltyBps = 0; + req.primarySaleRecipient = saleRecipient; + req.tokenId = nextId; + req.uri = "ipfs://"; + req.quantity = 100; + req.pricePerToken = 0; + req.currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + req.validityStartTimestamp = 0; + req.validityEndTimestamp = type(uint128).max; + req.uid = keccak256("uid"); + + bytes memory signature = signMintRequest(req, adminPkey); + vm.prank(nftHolder); + vm.expectRevert("invalid id"); + base.mintWithSignature(req, signature); + } +} diff --git a/src/test/sdk/base/ERC20Base.t.sol b/src/test/sdk/base/ERC20Base.t.sol new file mode 100644 index 000000000..ec8131cfd --- /dev/null +++ b/src/test/sdk/base/ERC20Base.t.sol @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import "./BaseUtilTest.sol"; +import { ERC20Base } from "contracts/base/ERC20Base.sol"; + +contract BaseERC20BaseTest is BaseUtilTest { + ERC20Base internal base; + using Strings for uint256; + + bytes32 internal permitTypeHash; + bytes32 internal permitNameHash; + bytes32 internal permitVersionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + uint256 public recipientPrivateKey = 5678; + address public recipient; + + function setUp() public override { + super.setUp(); + vm.prank(deployer); + base = new ERC20Base(deployer, NAME, SYMBOL); + + recipient = vm.addr(recipientPrivateKey); + + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + + // permit related inputs + permitTypeHash = keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ); + permitNameHash = keccak256(bytes(NAME)); + permitVersionHash = keccak256("1"); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `mint` + //////////////////////////////////////////////////////////////*/ + + function test_state_mint() public { + uint256 amount = 5 ether; + + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + + vm.prank(deployer); + base.mintTo(recipient, amount); + + assertEq(base.totalSupply(), currentTotalSupply + amount); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient + amount); + } + + function test_revert_mint_NotAuthorized() public { + uint256 amount = 5 ether; + + vm.expectRevert("Not authorized to mint."); + vm.prank(address(0x1)); + base.mintTo(recipient, amount); + } + + function test_revert_mint_MintingZeroTokens() public { + uint256 amount = 0; + + vm.expectRevert("Minting zero tokens."); + vm.prank(deployer); + base.mintTo(recipient, amount); + } + + function test_revert_mint_MintToZeroAddress() public { + uint256 amount = 1; + + vm.expectRevert("ERC20: mint to the zero address"); + vm.prank(deployer); + base.mintTo(address(0), amount); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `burn` + //////////////////////////////////////////////////////////////*/ + + function test_state_burn() public { + uint256 amount = 5 ether; + + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + + vm.prank(deployer); + base.mintTo(recipient, amount); + + assertEq(base.totalSupply(), currentTotalSupply + amount); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient + amount); + + // burn minted tokens + currentTotalSupply = base.totalSupply(); + currentBalanceOfRecipient = base.balanceOf(recipient); + vm.prank(recipient); + base.burn(amount); + + assertEq(base.totalSupply(), currentTotalSupply - amount); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient - amount); + } + + function test_revert_burn_NotEnoughBalance() public { + uint256 amount = 5 ether; + + vm.prank(deployer); + base.mintTo(recipient, amount); + + vm.expectRevert("not enough balance"); + vm.prank(recipient); + base.burn(amount + 1); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `permit` + //////////////////////////////////////////////////////////////*/ + + function test_state_permit() public { + uint256 amount = 5 ether; + + // mint amount to recipient + vm.prank(deployer); + base.mintTo(recipient, amount); + + // generate permit signature + address _owner = recipient; + address _spender = address(0x789); + uint256 _value = 1 ether; + uint256 _deadline = 1000; + + uint256 _nonce = base.nonces(_owner); + + domainSeparator = keccak256( + abi.encode(typehashEip712, permitNameHash, permitVersionHash, block.chainid, address(base)) + ); + bytes32 structHash = keccak256(abi.encode(permitTypeHash, _owner, _spender, _value, _nonce, _deadline)); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(recipientPrivateKey, typedDataHash); + + // call permit to approve _value to _spender + base.permit(_owner, _spender, _value, _deadline, v, r, s); + + // check allowance + uint256 _allowance = base.allowance(_owner, _spender); + + assertEq(_allowance, _value); + assertEq(base.nonces(_owner), _nonce + 1); + } + + function test_revert_permit_IncorrectKey() public { + uint256 amount = 5 ether; + uint256 wrongPrivateKey = 2345; + + // mint amount to recipient + vm.prank(deployer); + base.mintTo(recipient, amount); + + // generate permit signature + address _owner = recipient; + address _spender = address(0x789); + uint256 _value = 1 ether; + uint256 _deadline = 1000; + + uint256 _nonce = base.nonces(_owner); + + domainSeparator = keccak256( + abi.encode(typehashEip712, permitNameHash, permitVersionHash, block.chainid, address(base)) + ); + bytes32 structHash = keccak256(abi.encode(permitTypeHash, _owner, _spender, _value, _nonce, _deadline)); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wrongPrivateKey, typedDataHash); // sign with wrong key + + // call permit to approve _value to _spender + vm.expectRevert("ERC20Permit: invalid signature"); + base.permit(_owner, _spender, _value, _deadline, v, r, s); + } + + function test_revert_permit_UsedNonce() public { + uint256 amount = 5 ether; + + // mint amount to recipient + vm.prank(deployer); + base.mintTo(recipient, amount); + + // generate permit signature + address _owner = recipient; + address _spender = address(0x789); + uint256 _value = 1 ether; + uint256 _deadline = 1000; + + uint256 _nonce = base.nonces(_owner); + + domainSeparator = keccak256( + abi.encode(typehashEip712, permitNameHash, permitVersionHash, block.chainid, address(base)) + ); + bytes32 structHash = keccak256(abi.encode(permitTypeHash, _owner, _spender, _value, _nonce, _deadline)); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(recipientPrivateKey, typedDataHash); + + // call permit to approve _value to _spender + base.permit(_owner, _spender, _value, _deadline, v, r, s); + + // sign again with same nonce + (v, r, s) = vm.sign(recipientPrivateKey, typedDataHash); + + vm.expectRevert("ERC20Permit: invalid signature"); + base.permit(_owner, _spender, _value, _deadline, v, r, s); + } + + function test_revert_permit_ExpiredDeadline() public { + uint256 amount = 5 ether; + // uint256 wrongPrivateKey = 2345; + + // mint amount to recipient + vm.prank(deployer); + base.mintTo(recipient, amount); + + // generate permit signature + address _owner = recipient; + address _spender = address(0x789); + uint256 _value = 1 ether; + uint256 _deadline = 1000; + + uint256 _nonce = base.nonces(_owner); + + domainSeparator = keccak256( + abi.encode(typehashEip712, permitNameHash, permitVersionHash, block.chainid, address(base)) + ); + bytes32 structHash = keccak256(abi.encode(permitTypeHash, _owner, _spender, _value, _nonce, _deadline)); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(recipientPrivateKey, typedDataHash); + + // call permit to approve _value to _spender + vm.warp(_deadline + 1); + vm.expectRevert("ERC20Permit: expired deadline"); + base.permit(_owner, _spender, _value, _deadline, v, r, s); + } +} diff --git a/src/test/sdk/base/ERC20Drop.t.sol b/src/test/sdk/base/ERC20Drop.t.sol new file mode 100644 index 000000000..cb4457298 --- /dev/null +++ b/src/test/sdk/base/ERC20Drop.t.sol @@ -0,0 +1,300 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import "./BaseUtilTest.sol"; +import { ERC20Drop } from "contracts/base/ERC20Drop.sol"; + +contract BaseERC20DropTest is BaseUtilTest { + ERC20Drop internal base; + using Strings for uint256; + + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + // permit + bytes32 internal permitTypeHash; + bytes32 internal permitNameHash; + bytes32 internal permitVersionHash; + + uint256 public recipientPrivateKey = 5678; + address public recipient; + + function setUp() public override { + super.setUp(); + vm.prank(signer); + base = new ERC20Drop(signer, NAME, SYMBOL, saleRecipient); + + recipient = vm.addr(recipientPrivateKey); + erc20.mint(recipient, 1_000_000 ether); + vm.deal(recipient, 1_000_000 ether); + + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + + // permit related inputs + permitTypeHash = keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ); + permitNameHash = keccak256(bytes(NAME)); + permitVersionHash = keccak256("1"); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `claim` + //////////////////////////////////////////////////////////////*/ + + function test_state_claim_ZeroPrice() public { + vm.warp(1); + + address claimer = address(0x345); + uint256 _quantity = 10; + + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + + bytes32[] memory proofs = new bytes32[](0); + + ERC20Drop.AllowlistProof memory alp; + alp.proof = proofs; + + ERC20Drop.ClaimCondition[] memory conditions = new ERC20Drop.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(signer); + base.setClaimConditions(conditions[0], false); + + vm.prank(claimer, claimer); + base.claim(recipient, _quantity, address(0), 0, alp, ""); + + assertEq(base.totalSupply(), currentTotalSupply + _quantity); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient + _quantity); + } + + function test_state_claim_NonZeroPrice_ERC20() public { + vm.warp(1); + + address claimer = address(0x345); + uint256 _quantity = 10 ether; + + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + + bytes32[] memory proofs = new bytes32[](0); + + ERC20Drop.AllowlistProof memory alp; + alp.proof = proofs; + + ERC20Drop.ClaimCondition[] memory conditions = new ERC20Drop.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100 ether; + conditions[0].quantityLimitPerWallet = 100 ether; + + // set price and currency + conditions[0].pricePerToken = 1 ether; + conditions[0].currency = address(erc20); + + uint256 totalPrice = (conditions[0].pricePerToken * _quantity) / 1 ether; + + vm.prank(signer); + base.setClaimConditions(conditions[0], false); + + // mint erc20 to claimer, and approve to base + erc20.mint(claimer, 1000 ether); + vm.prank(claimer); + erc20.approve(address(base), totalPrice); + + vm.prank(claimer, claimer); + base.claim(recipient, _quantity, address(erc20), 1 ether, alp, ""); + + assertEq(base.totalSupply(), currentTotalSupply + _quantity); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient + _quantity); + } + + function test_state_claim_NonZeroPrice_NativeToken() public { + vm.warp(1); + + address claimer = address(0x345); + uint256 _quantity = 10 ether; + + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + + bytes32[] memory proofs = new bytes32[](0); + + ERC20Drop.AllowlistProof memory alp; + alp.proof = proofs; + + ERC20Drop.ClaimCondition[] memory conditions = new ERC20Drop.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100 ether; + conditions[0].quantityLimitPerWallet = 100 ether; + + // set price and currency + conditions[0].pricePerToken = 1 ether; + conditions[0].currency = address(NATIVE_TOKEN); + + uint256 totalPrice = (conditions[0].pricePerToken * _quantity) / 1 ether; + + vm.prank(signer); + base.setClaimConditions(conditions[0], false); + + // deal NATIVE_TOKEN to claimer + vm.deal(claimer, 1_000 ether); + + vm.prank(claimer, claimer); + base.claim{ value: totalPrice }(recipient, _quantity, address(NATIVE_TOKEN), 1 ether, alp, ""); + + assertEq(base.totalSupply(), currentTotalSupply + _quantity); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient + _quantity); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `burn` + //////////////////////////////////////////////////////////////*/ + + function test_state_burn() public { + address claimer = address(0x345); + uint256 _quantity = 10 ether; + + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + + bytes32[] memory proofs = new bytes32[](0); + + ERC20Drop.AllowlistProof memory alp; + alp.proof = proofs; + + ERC20Drop.ClaimCondition[] memory conditions = new ERC20Drop.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100 ether; + conditions[0].quantityLimitPerWallet = 100 ether; + + vm.prank(signer); + base.setClaimConditions(conditions[0], false); + + vm.prank(claimer, claimer); + base.claim(recipient, _quantity, address(0), 0, alp, ""); + + // burn minted tokens + currentTotalSupply = base.totalSupply(); + currentBalanceOfRecipient = base.balanceOf(recipient); + vm.prank(recipient); + base.burn(_quantity); + + assertEq(base.totalSupply(), currentTotalSupply - _quantity); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient - _quantity); + } + + function test_revert_burn_NotEnoughBalance() public { + vm.expectRevert("not enough balance"); + vm.prank(recipient); + base.burn(1); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `permit` + //////////////////////////////////////////////////////////////*/ + + function test_state_permit() public { + // generate permit signature + address _owner = recipient; + address _spender = address(0x789); + uint256 _value = 1 ether; + uint256 _deadline = 1000; + + uint256 _nonce = base.nonces(_owner); + + domainSeparator = keccak256( + abi.encode(typehashEip712, permitNameHash, permitVersionHash, block.chainid, address(base)) + ); + bytes32 structHash = keccak256(abi.encode(permitTypeHash, _owner, _spender, _value, _nonce, _deadline)); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(recipientPrivateKey, typedDataHash); + + // call permit to approve _value to _spender + base.permit(_owner, _spender, _value, _deadline, v, r, s); + + // check allowance + uint256 _allowance = base.allowance(_owner, _spender); + + assertEq(_allowance, _value); + assertEq(base.nonces(_owner), _nonce + 1); + } + + function test_revert_permit_IncorrectKey() public { + uint256 wrongPrivateKey = 2345; + + // generate permit signature + address _owner = recipient; + address _spender = address(0x789); + uint256 _value = 1 ether; + uint256 _deadline = 1000; + + uint256 _nonce = base.nonces(_owner); + + domainSeparator = keccak256( + abi.encode(typehashEip712, permitNameHash, permitVersionHash, block.chainid, address(base)) + ); + bytes32 structHash = keccak256(abi.encode(permitTypeHash, _owner, _spender, _value, _nonce, _deadline)); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wrongPrivateKey, typedDataHash); // sign with wrong key + + // call permit to approve _value to _spender + vm.expectRevert("ERC20Permit: invalid signature"); + base.permit(_owner, _spender, _value, _deadline, v, r, s); + } + + function test_revert_permit_UsedNonce() public { + // generate permit signature + address _owner = recipient; + address _spender = address(0x789); + uint256 _value = 1 ether; + uint256 _deadline = 1000; + + uint256 _nonce = base.nonces(_owner); + + domainSeparator = keccak256( + abi.encode(typehashEip712, permitNameHash, permitVersionHash, block.chainid, address(base)) + ); + bytes32 structHash = keccak256(abi.encode(permitTypeHash, _owner, _spender, _value, _nonce, _deadline)); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(recipientPrivateKey, typedDataHash); + + // call permit to approve _value to _spender + base.permit(_owner, _spender, _value, _deadline, v, r, s); + + // sign again with same nonce + (v, r, s) = vm.sign(recipientPrivateKey, typedDataHash); + + vm.expectRevert("ERC20Permit: invalid signature"); + base.permit(_owner, _spender, _value, _deadline, v, r, s); + } + + function test_revert_permit_ExpiredDeadline() public { + // generate permit signature + address _owner = recipient; + address _spender = address(0x789); + uint256 _value = 1 ether; + uint256 _deadline = 1000; + + uint256 _nonce = base.nonces(_owner); + + domainSeparator = keccak256( + abi.encode(typehashEip712, permitNameHash, permitVersionHash, block.chainid, address(base)) + ); + bytes32 structHash = keccak256(abi.encode(permitTypeHash, _owner, _spender, _value, _nonce, _deadline)); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(recipientPrivateKey, typedDataHash); + + // call permit to approve _value to _spender + vm.warp(_deadline + 1); + vm.expectRevert("ERC20Permit: expired deadline"); + base.permit(_owner, _spender, _value, _deadline, v, r, s); + } +} diff --git a/src/test/sdk/base/ERC20DropVote.t.sol b/src/test/sdk/base/ERC20DropVote.t.sol new file mode 100644 index 000000000..fd32af1de --- /dev/null +++ b/src/test/sdk/base/ERC20DropVote.t.sol @@ -0,0 +1,377 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import "./BaseUtilTest.sol"; +import { ERC20DropVote } from "contracts/base/ERC20DropVote.sol"; + +contract BaseERC20DropVoteTest is BaseUtilTest { + ERC20DropVote internal base; + using Strings for uint256; + + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + // permit and vote + bytes32 internal permitTypeHash; + bytes32 internal delegationTypeHash; + bytes32 internal permitNameHash; + bytes32 internal permitVersionHash; + + uint256 public recipientPrivateKey = 5678; + address public recipient; + + function setUp() public override { + super.setUp(); + vm.prank(signer); + base = new ERC20DropVote(signer, NAME, SYMBOL, saleRecipient); + + recipient = vm.addr(recipientPrivateKey); + erc20.mint(recipient, 1_000_000 ether); + vm.deal(recipient, 1_000_000 ether); + + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + + // permit related inputs + permitTypeHash = keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ); + permitNameHash = keccak256(bytes(NAME)); + permitVersionHash = keccak256("1"); + + // vote-delegation related inputs + delegationTypeHash = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `claim` + //////////////////////////////////////////////////////////////*/ + + function test_state_claim_ZeroPrice() public { + vm.warp(1); + + address claimer = address(0x345); + uint256 _quantity = 10; + + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + + bytes32[] memory proofs = new bytes32[](0); + + ERC20DropVote.AllowlistProof memory alp; + alp.proof = proofs; + + ERC20DropVote.ClaimCondition[] memory conditions = new ERC20DropVote.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(signer); + base.setClaimConditions(conditions[0], false); + + vm.prank(claimer, claimer); + base.claim(recipient, _quantity, address(0), 0, alp, ""); + + assertEq(base.totalSupply(), currentTotalSupply + _quantity); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient + _quantity); + } + + function test_state_claim_NonZeroPrice_ERC20() public { + vm.warp(1); + + address claimer = address(0x345); + uint256 _quantity = 10 ether; + + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + + bytes32[] memory proofs = new bytes32[](0); + + ERC20DropVote.AllowlistProof memory alp; + alp.proof = proofs; + + ERC20DropVote.ClaimCondition[] memory conditions = new ERC20DropVote.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100 ether; + conditions[0].quantityLimitPerWallet = 100 ether; + + // set price and currency + conditions[0].pricePerToken = 1 ether; + conditions[0].currency = address(erc20); + + // uint256 totalPrice = (conditions[0].pricePerToken * _quantity) / 1 ether; + + vm.prank(signer); + base.setClaimConditions(conditions[0], false); + + // mint erc20 to claimer, and approve to base + erc20.mint(claimer, 1000 ether); + vm.prank(claimer); + erc20.approve(address(base), 1_000 ether); + + vm.prank(claimer, claimer); + base.claim(recipient, _quantity, address(erc20), 1 ether, alp, ""); + + assertEq(base.totalSupply(), currentTotalSupply + _quantity); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient + _quantity); + } + + function test_state_claim_NonZeroPrice_NativeToken() public { + vm.warp(1); + + address claimer = address(0x345); + uint256 _quantity = 10 ether; + + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + + bytes32[] memory proofs = new bytes32[](0); + + ERC20DropVote.AllowlistProof memory alp; + alp.proof = proofs; + + ERC20DropVote.ClaimCondition[] memory conditions = new ERC20DropVote.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100 ether; + conditions[0].quantityLimitPerWallet = 100 ether; + + // set price and currency + conditions[0].pricePerToken = 1 ether; + conditions[0].currency = address(NATIVE_TOKEN); + + uint256 totalPrice = (conditions[0].pricePerToken * _quantity) / 1 ether; + + vm.prank(signer); + base.setClaimConditions(conditions[0], false); + + // deal NATIVE_TOKEN to claimer + vm.deal(claimer, 1_000 ether); + + vm.prank(claimer, claimer); + base.claim{ value: totalPrice }(recipient, _quantity, address(NATIVE_TOKEN), 1 ether, alp, ""); + + assertEq(base.totalSupply(), currentTotalSupply + _quantity); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient + _quantity); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `burn` + //////////////////////////////////////////////////////////////*/ + + function test_state_burn() public { + address claimer = address(0x345); + uint256 _quantity = 10 ether; + + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + + bytes32[] memory proofs = new bytes32[](0); + + ERC20DropVote.AllowlistProof memory alp; + alp.proof = proofs; + + ERC20DropVote.ClaimCondition[] memory conditions = new ERC20DropVote.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100 ether; + conditions[0].quantityLimitPerWallet = 100 ether; + + vm.prank(signer); + base.setClaimConditions(conditions[0], false); + + vm.prank(claimer, claimer); + base.claim(recipient, _quantity, address(0), 0, alp, ""); + + // burn minted tokens + currentTotalSupply = base.totalSupply(); + currentBalanceOfRecipient = base.balanceOf(recipient); + vm.prank(recipient); + base.burn(_quantity); + + assertEq(base.totalSupply(), currentTotalSupply - _quantity); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient - _quantity); + } + + function test_revert_burn_NotEnoughBalance() public { + vm.expectRevert("not enough balance"); + vm.prank(recipient); + base.burn(1); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `permit` + //////////////////////////////////////////////////////////////*/ + + function test_state_permit() public { + // generate permit signature + address _owner = recipient; + address _spender = address(0x789); + uint256 _value = 1 ether; + uint256 _deadline = 1000; + + uint256 _nonce = base.nonces(_owner); + + domainSeparator = keccak256( + abi.encode(typehashEip712, permitNameHash, permitVersionHash, block.chainid, address(base)) + ); + bytes32 structHash = keccak256(abi.encode(permitTypeHash, _owner, _spender, _value, _nonce, _deadline)); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(recipientPrivateKey, typedDataHash); + + // call permit to approve _value to _spender + base.permit(_owner, _spender, _value, _deadline, v, r, s); + + // check allowance + uint256 _allowance = base.allowance(_owner, _spender); + + assertEq(_allowance, _value); + assertEq(base.nonces(_owner), _nonce + 1); + } + + function test_revert_permit_IncorrectKey() public { + uint256 wrongPrivateKey = 2345; + + // generate permit signature + address _owner = recipient; + address _spender = address(0x789); + uint256 _value = 1 ether; + uint256 _deadline = 1000; + + uint256 _nonce = base.nonces(_owner); + + domainSeparator = keccak256( + abi.encode(typehashEip712, permitNameHash, permitVersionHash, block.chainid, address(base)) + ); + bytes32 structHash = keccak256(abi.encode(permitTypeHash, _owner, _spender, _value, _nonce, _deadline)); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wrongPrivateKey, typedDataHash); // sign with wrong key + + // call permit to approve _value to _spender + vm.expectRevert("ERC20Permit: invalid signature"); + base.permit(_owner, _spender, _value, _deadline, v, r, s); + } + + function test_revert_permit_UsedNonce() public { + // generate permit signature + address _owner = recipient; + address _spender = address(0x789); + uint256 _value = 1 ether; + uint256 _deadline = 1000; + + uint256 _nonce = base.nonces(_owner); + + domainSeparator = keccak256( + abi.encode(typehashEip712, permitNameHash, permitVersionHash, block.chainid, address(base)) + ); + bytes32 structHash = keccak256(abi.encode(permitTypeHash, _owner, _spender, _value, _nonce, _deadline)); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(recipientPrivateKey, typedDataHash); + + // call permit to approve _value to _spender + base.permit(_owner, _spender, _value, _deadline, v, r, s); + + // sign again with same nonce + (v, r, s) = vm.sign(recipientPrivateKey, typedDataHash); + + vm.expectRevert("ERC20Permit: invalid signature"); + base.permit(_owner, _spender, _value, _deadline, v, r, s); + } + + function test_revert_permit_ExpiredDeadline() public { + // generate permit signature + address _owner = recipient; + address _spender = address(0x789); + uint256 _value = 1 ether; + uint256 _deadline = 1000; + + uint256 _nonce = base.nonces(_owner); + + domainSeparator = keccak256( + abi.encode(typehashEip712, permitNameHash, permitVersionHash, block.chainid, address(base)) + ); + bytes32 structHash = keccak256(abi.encode(permitTypeHash, _owner, _spender, _value, _nonce, _deadline)); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(recipientPrivateKey, typedDataHash); + + // call permit to approve _value to _spender + vm.warp(_deadline + 1); + vm.expectRevert("ERC20Permit: expired deadline"); + base.permit(_owner, _spender, _value, _deadline, v, r, s); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `delegateBySig` + //////////////////////////////////////////////////////////////*/ + + function test_state_delegateBySig() public { + // generate delegation signature + address _delegatee = address(0x789); + uint256 _nonce = base.nonces(recipient); + uint256 _expiry = 1000; + + // vote extends permit, so name-hash and version-hash are same for both + domainSeparator = keccak256( + abi.encode(typehashEip712, permitNameHash, permitVersionHash, block.chainid, address(base)) + ); + bytes32 structHash = keccak256(abi.encode(delegationTypeHash, _delegatee, _nonce, _expiry)); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(recipientPrivateKey, typedDataHash); + + // call delegationBySig to delegate votes from recipient address to delegatee address + base.delegateBySig(_delegatee, _nonce, _expiry, v, r, s); + + assertEq(base.delegates(recipient), _delegatee); + } + + function test_revert_delegateBySig_InvalidNonce() public { + // generate delegation signature + address _delegatee = address(0x789); + uint256 _nonce = base.nonces(recipient); + uint256 _expiry = 1000; + + // vote extends permit, so name-hash and version-hash are same for both + domainSeparator = keccak256( + abi.encode(typehashEip712, permitNameHash, permitVersionHash, block.chainid, address(base)) + ); + bytes32 structHash = keccak256( + abi.encode( + delegationTypeHash, + _delegatee, + _nonce + 1, // invalid nonce + _expiry + ) + ); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(recipientPrivateKey, typedDataHash); + + // call delegationBySig to delegate votes from recipient address to delegatee address + vm.expectRevert("ERC20Votes: invalid nonce"); + base.delegateBySig(_delegatee, _nonce + 1, _expiry, v, r, s); + } + + function test_revert_delegateBySig_SignatureExpired() public { + // generate delegation signature + address _delegatee = address(0x789); + uint256 _nonce = base.nonces(recipient); + uint256 _expiry = 1000; + + // vote extends permit, so name-hash and version-hash are same for both + domainSeparator = keccak256( + abi.encode(typehashEip712, permitNameHash, permitVersionHash, block.chainid, address(base)) + ); + bytes32 structHash = keccak256(abi.encode(delegationTypeHash, _delegatee, _nonce, _expiry)); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(recipientPrivateKey, typedDataHash); + + // call delegationBySig to delegate votes from recipient address to delegatee address + vm.warp(_expiry + 1); + vm.expectRevert("ERC20Votes: signature expired"); + base.delegateBySig(_delegatee, _nonce, _expiry, v, r, s); + } +} diff --git a/src/test/sdk/base/ERC20SignatureMint.t.sol b/src/test/sdk/base/ERC20SignatureMint.t.sol new file mode 100644 index 000000000..9c4560868 --- /dev/null +++ b/src/test/sdk/base/ERC20SignatureMint.t.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import "./BaseUtilTest.sol"; +import { ERC20SignatureMint } from "contracts/base/ERC20SignatureMint.sol"; + +contract BaseERC20SignatureMintTest is BaseUtilTest { + ERC20SignatureMint internal base; + using Strings for uint256; + + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + ERC20SignatureMint.MintRequest _mintrequest; + bytes _signature; + + address recipient; + + function setUp() public override { + super.setUp(); + vm.prank(signer); + base = new ERC20SignatureMint(signer, NAME, SYMBOL, saleRecipient); + + recipient = address(0x123); + erc20.mint(recipient, 1_000_000 ether); + vm.deal(recipient, 1_000_000 ether); + + typehashMintRequest = keccak256( + "MintRequest(address to,address primarySaleRecipient,uint256 quantity,uint256 price,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + nameHash = keccak256(bytes("SignatureMintERC20")); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256(abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(base))); + + _mintrequest.to = recipient; + _mintrequest.primarySaleRecipient = saleRecipient; + _mintrequest.quantity = 100 ether; + _mintrequest.price = 0; + _mintrequest.currency = address(0); + _mintrequest.validityStartTimestamp = 1000; + _mintrequest.validityEndTimestamp = 2000; + _mintrequest.uid = bytes32(0); + + _signature = signMintRequest(_mintrequest, privateKey); + } + + function signMintRequest( + ERC20SignatureMint.MintRequest memory _request, + uint256 _privateKey + ) internal view returns (bytes memory) { + bytes memory encodedRequest = abi.encode( + typehashMintRequest, + _request.to, + _request.primarySaleRecipient, + _request.quantity, + _request.price, + _request.currency, + _request.validityStartTimestamp, + _request.validityEndTimestamp, + _request.uid + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, typedDataHash); + bytes memory sig = abi.encodePacked(r, s, v); + + return sig; + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `mintWithSignature` + //////////////////////////////////////////////////////////////*/ + + function test_state_mintWithSignature_ZeroPrice() public { + vm.warp(1000); + + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + + address recoveredSigner = base.mintWithSignature(_mintrequest, _signature); + + assertEq(base.totalSupply(), currentTotalSupply + _mintrequest.quantity); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient + _mintrequest.quantity); + assertEq(signer, recoveredSigner); + } + + function test_state_mintWithSignature_NonZeroPrice_ERC20() public { + vm.warp(1000); + + _mintrequest.price = 1; + _mintrequest.currency = address(erc20); + _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(recipient); + erc20.approve(address(base), _mintrequest.price); + + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + uint256 erc20BalanceOfRecipient = erc20.balanceOf(recipient); + uint256 erc20BalanceOfSeller = erc20.balanceOf(saleRecipient); + + uint256 totalPrice = _mintrequest.price; + + vm.prank(recipient); + base.mintWithSignature(_mintrequest, _signature); + + // check token balances + assertEq(base.totalSupply(), currentTotalSupply + _mintrequest.quantity); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient + _mintrequest.quantity); + + // check erc20 currency balances + assertEq(erc20.balanceOf(recipient), erc20BalanceOfRecipient - totalPrice); + assertEq(erc20.balanceOf(saleRecipient), erc20BalanceOfSeller + totalPrice); + } + + function test_state_mintWithSignature_NonZeroPrice_NativeToken() public { + vm.warp(1000); + + _mintrequest.price = 1 ether; + _mintrequest.currency = address(NATIVE_TOKEN); + _signature = signMintRequest(_mintrequest, privateKey); + + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + uint256 etherBalanceOfRecipient = recipient.balance; + uint256 etherBalanceOfSeller = saleRecipient.balance; + + uint256 totalPrice = _mintrequest.price; + + vm.prank(recipient); + base.mintWithSignature{ value: totalPrice }(_mintrequest, _signature); + + assertEq(base.totalSupply(), currentTotalSupply + _mintrequest.quantity); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient + _mintrequest.quantity); + + // check native-token balances + assertEq(recipient.balance, etherBalanceOfRecipient - totalPrice); + assertEq(saleRecipient.balance, etherBalanceOfSeller + totalPrice); + } + + function test_revert_mintWithSignature_MustSendTotalPrice() public { + vm.warp(1000); + + _mintrequest.price = 1; + _mintrequest.currency = address(NATIVE_TOKEN); + _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(recipient); + vm.expectRevert("Must send total price."); + base.mintWithSignature{ value: 0 }(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_MintingZeroTokens() public { + vm.warp(1000); + + _mintrequest.quantity = 0; + _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("Minting zero tokens."); + base.mintWithSignature(_mintrequest, _signature); + } +} diff --git a/src/test/sdk/base/ERC20SignatureMintVote.t.sol b/src/test/sdk/base/ERC20SignatureMintVote.t.sol new file mode 100644 index 000000000..f705aba8e --- /dev/null +++ b/src/test/sdk/base/ERC20SignatureMintVote.t.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import "./BaseUtilTest.sol"; +import { ERC20SignatureMintVote } from "contracts/base/ERC20SignatureMintVote.sol"; + +contract BaseERC20SignatureMintVoteTest is BaseUtilTest { + ERC20SignatureMintVote internal base; + using Strings for uint256; + + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + ERC20SignatureMintVote.MintRequest _mintrequest; + bytes _signature; + + address recipient; + + function setUp() public override { + super.setUp(); + vm.prank(signer); + base = new ERC20SignatureMintVote(signer, NAME, SYMBOL, saleRecipient); + + recipient = address(0x123); + erc20.mint(recipient, 1_000 ether); + vm.deal(recipient, 1_000 ether); + + typehashMintRequest = keccak256( + "MintRequest(address to,address primarySaleRecipient,uint256 quantity,uint256 price,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + nameHash = keccak256(bytes("SignatureMintERC20")); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256(abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(base))); + + _mintrequest.to = recipient; + _mintrequest.primarySaleRecipient = saleRecipient; + _mintrequest.quantity = 100 ether; + _mintrequest.price = 0; + _mintrequest.currency = address(0); + _mintrequest.validityStartTimestamp = 1000; + _mintrequest.validityEndTimestamp = 2000; + _mintrequest.uid = bytes32(0); + + _signature = signMintRequest(_mintrequest, privateKey); + } + + function signMintRequest( + ERC20SignatureMintVote.MintRequest memory _request, + uint256 _privateKey + ) internal view returns (bytes memory) { + bytes memory encodedRequest = abi.encode( + typehashMintRequest, + _request.to, + _request.primarySaleRecipient, + _request.quantity, + _request.price, + _request.currency, + _request.validityStartTimestamp, + _request.validityEndTimestamp, + _request.uid + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, typedDataHash); + bytes memory sig = abi.encodePacked(r, s, v); + + return sig; + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `mintWithSignature` + //////////////////////////////////////////////////////////////*/ + + function test_state_mintWithSignature_ZeroPrice() public { + vm.warp(1000); + + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + + address recoveredSigner = base.mintWithSignature(_mintrequest, _signature); + + assertEq(base.totalSupply(), currentTotalSupply + _mintrequest.quantity); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient + _mintrequest.quantity); + assertEq(signer, recoveredSigner); + } + + function test_state_mintWithSignature_NonZeroPrice_ERC20() public { + vm.warp(1000); + + _mintrequest.price = 1; + _mintrequest.currency = address(erc20); + _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(recipient); + erc20.approve(address(base), _mintrequest.price); + + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + uint256 erc20BalanceOfRecipient = erc20.balanceOf(recipient); + uint256 erc20BalanceOfSeller = erc20.balanceOf(saleRecipient); + + uint256 totalPrice = _mintrequest.price; + + vm.prank(recipient); + base.mintWithSignature(_mintrequest, _signature); + + // check token balances + assertEq(base.totalSupply(), currentTotalSupply + _mintrequest.quantity); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient + _mintrequest.quantity); + + // check erc20 currency balances + assertEq(erc20.balanceOf(recipient), erc20BalanceOfRecipient - totalPrice); + assertEq(erc20.balanceOf(saleRecipient), erc20BalanceOfSeller + totalPrice); + } + + function test_state_mintWithSignature_NonZeroPrice_NativeToken() public { + vm.warp(1000); + + _mintrequest.price = 1; + _mintrequest.currency = address(NATIVE_TOKEN); + _signature = signMintRequest(_mintrequest, privateKey); + + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + uint256 etherBalanceOfRecipient = recipient.balance; + uint256 etherBalanceOfSeller = saleRecipient.balance; + + uint256 totalPrice = _mintrequest.price; + + vm.prank(recipient); + base.mintWithSignature{ value: totalPrice }(_mintrequest, _signature); + + assertEq(base.totalSupply(), currentTotalSupply + _mintrequest.quantity); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient + _mintrequest.quantity); + + // check native-token balances + assertEq(recipient.balance, etherBalanceOfRecipient - totalPrice); + assertEq(saleRecipient.balance, etherBalanceOfSeller + totalPrice); + } + + function test_revert_mintWithSignature_MustSendTotalPrice() public { + vm.warp(1000); + + _mintrequest.price = 1; + _mintrequest.currency = address(NATIVE_TOKEN); + _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(recipient); + vm.expectRevert("Must send total price."); + base.mintWithSignature{ value: 0 }(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_MintingZeroTokens() public { + vm.warp(1000); + + _mintrequest.quantity = 0; + _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("Minting zero tokens."); + base.mintWithSignature(_mintrequest, _signature); + } +} diff --git a/src/test/sdk/base/ERC20Vote.t.sol b/src/test/sdk/base/ERC20Vote.t.sol new file mode 100644 index 000000000..ca57c1c76 --- /dev/null +++ b/src/test/sdk/base/ERC20Vote.t.sol @@ -0,0 +1,325 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import "./BaseUtilTest.sol"; +import { ERC20Vote } from "contracts/base/ERC20Vote.sol"; + +contract BaseERC20VoteTest is BaseUtilTest { + ERC20Vote internal base; + using Strings for uint256; + + bytes32 internal permitTypeHash; + bytes32 internal delegationTypeHash; + bytes32 internal permitNameHash; + bytes32 internal permitVersionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + uint256 public recipientPrivateKey = 5678; + address public recipient; + + function setUp() public override { + super.setUp(); + vm.prank(deployer); + base = new ERC20Vote(deployer, NAME, SYMBOL); + + recipient = vm.addr(recipientPrivateKey); + + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + + // permit related inputs + permitTypeHash = keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ); + permitNameHash = keccak256(bytes(NAME)); + permitVersionHash = keccak256("1"); + + // vote-delegation related inputs + delegationTypeHash = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `mint` + //////////////////////////////////////////////////////////////*/ + + function test_state_mint() public { + uint256 amount = 5 ether; + + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + + vm.prank(deployer); + base.mintTo(recipient, amount); + + assertEq(base.totalSupply(), currentTotalSupply + amount); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient + amount); + } + + function test_revert_mint_NotAuthorized() public { + uint256 amount = 5 ether; + + vm.expectRevert("Not authorized to mint."); + vm.prank(address(0x1)); + base.mintTo(recipient, amount); + } + + function test_revert_mint_MintingZeroTokens() public { + uint256 amount = 0; + + vm.expectRevert("Minting zero tokens."); + vm.prank(deployer); + base.mintTo(recipient, amount); + } + + function test_revert_mint_MintToZeroAddress() public { + uint256 amount = 1; + + vm.expectRevert("ERC20: mint to the zero address"); + vm.prank(deployer); + base.mintTo(address(0), amount); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `burn` + //////////////////////////////////////////////////////////////*/ + + function test_state_burn() public { + uint256 amount = 5 ether; + + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + + vm.prank(deployer); + base.mintTo(recipient, amount); + + assertEq(base.totalSupply(), currentTotalSupply + amount); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient + amount); + + // burn minted tokens + currentTotalSupply = base.totalSupply(); + currentBalanceOfRecipient = base.balanceOf(recipient); + vm.prank(recipient); + base.burn(amount); + + assertEq(base.totalSupply(), currentTotalSupply - amount); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient - amount); + } + + function test_revert_burn_NotEnoughBalance() public { + uint256 amount = 5 ether; + + vm.prank(deployer); + base.mintTo(recipient, amount); + + vm.expectRevert("not enough balance"); + vm.prank(recipient); + base.burn(amount + 1); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `permit` + //////////////////////////////////////////////////////////////*/ + + function test_state_permit() public { + uint256 amount = 5 ether; + + // mint amount to recipient + vm.prank(deployer); + base.mintTo(recipient, amount); + + // generate permit signature + address _owner = recipient; + address _spender = address(0x789); + uint256 _value = 1 ether; + uint256 _deadline = 1000; + + uint256 _nonce = base.nonces(_owner); + + domainSeparator = keccak256( + abi.encode(typehashEip712, permitNameHash, permitVersionHash, block.chainid, address(base)) + ); + bytes32 structHash = keccak256(abi.encode(permitTypeHash, _owner, _spender, _value, _nonce, _deadline)); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(recipientPrivateKey, typedDataHash); + + // call permit to approve _value to _spender + base.permit(_owner, _spender, _value, _deadline, v, r, s); + + // check allowance + uint256 _allowance = base.allowance(_owner, _spender); + + assertEq(_allowance, _value); + assertEq(base.nonces(_owner), _nonce + 1); + } + + function test_revert_permit_IncorrectKey() public { + uint256 amount = 5 ether; + uint256 wrongPrivateKey = 2345; + + // mint amount to recipient + vm.prank(deployer); + base.mintTo(recipient, amount); + + // generate permit signature + address _owner = recipient; + address _spender = address(0x789); + uint256 _value = 1 ether; + uint256 _deadline = 1000; + + uint256 _nonce = base.nonces(_owner); + + domainSeparator = keccak256( + abi.encode(typehashEip712, permitNameHash, permitVersionHash, block.chainid, address(base)) + ); + bytes32 structHash = keccak256(abi.encode(permitTypeHash, _owner, _spender, _value, _nonce, _deadline)); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wrongPrivateKey, typedDataHash); // sign with wrong key + + // call permit to approve _value to _spender + vm.expectRevert("ERC20Permit: invalid signature"); + base.permit(_owner, _spender, _value, _deadline, v, r, s); + } + + function test_revert_permit_UsedNonce() public { + uint256 amount = 5 ether; + + // mint amount to recipient + vm.prank(deployer); + base.mintTo(recipient, amount); + + // generate permit signature + address _owner = recipient; + address _spender = address(0x789); + uint256 _value = 1 ether; + uint256 _deadline = 1000; + + uint256 _nonce = base.nonces(_owner); + + domainSeparator = keccak256( + abi.encode(typehashEip712, permitNameHash, permitVersionHash, block.chainid, address(base)) + ); + bytes32 structHash = keccak256(abi.encode(permitTypeHash, _owner, _spender, _value, _nonce, _deadline)); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(recipientPrivateKey, typedDataHash); + + // call permit to approve _value to _spender + base.permit(_owner, _spender, _value, _deadline, v, r, s); + + // sign again with same nonce + (v, r, s) = vm.sign(recipientPrivateKey, typedDataHash); + + vm.expectRevert("ERC20Permit: invalid signature"); + base.permit(_owner, _spender, _value, _deadline, v, r, s); + } + + function test_revert_permit_ExpiredDeadline() public { + uint256 amount = 5 ether; + uint256 wrongPrivateKey = 2345; + + // mint amount to recipient + vm.prank(deployer); + base.mintTo(recipient, amount); + + // generate permit signature + address _owner = recipient; + address _spender = address(0x789); + uint256 _value = 1 ether; + uint256 _deadline = 1000; + + uint256 _nonce = base.nonces(_owner); + + domainSeparator = keccak256( + abi.encode(typehashEip712, permitNameHash, permitVersionHash, block.chainid, address(base)) + ); + bytes32 structHash = keccak256(abi.encode(permitTypeHash, _owner, _spender, _value, _nonce, _deadline)); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wrongPrivateKey, typedDataHash); // sign with wrong key + + // call permit to approve _value to _spender + vm.warp(_deadline + 1); + vm.expectRevert("ERC20Permit: expired deadline"); + base.permit(_owner, _spender, _value, _deadline, v, r, s); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `delegateBySig` + //////////////////////////////////////////////////////////////*/ + + function test_state_delegateBySig() public { + // generate delegation signature + address _delegatee = address(0x789); + uint256 _nonce = base.nonces(recipient); + uint256 _expiry = 1000; + + // vote extends permit, so name-hash and version-hash are same for both + domainSeparator = keccak256( + abi.encode(typehashEip712, permitNameHash, permitVersionHash, block.chainid, address(base)) + ); + bytes32 structHash = keccak256(abi.encode(delegationTypeHash, _delegatee, _nonce, _expiry)); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(recipientPrivateKey, typedDataHash); + + // call delegationBySig to delegate votes from recipient address to delegatee address + base.delegateBySig(_delegatee, _nonce, _expiry, v, r, s); + + assertEq(base.delegates(recipient), _delegatee); + } + + function test_revert_delegateBySig_InvalidNonce() public { + // generate delegation signature + address _delegatee = address(0x789); + uint256 _nonce = base.nonces(recipient); + uint256 _expiry = 1000; + + // vote extends permit, so name-hash and version-hash are same for both + domainSeparator = keccak256( + abi.encode(typehashEip712, permitNameHash, permitVersionHash, block.chainid, address(base)) + ); + bytes32 structHash = keccak256( + abi.encode( + delegationTypeHash, + _delegatee, + _nonce + 1, // invalid nonce + _expiry + ) + ); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(recipientPrivateKey, typedDataHash); + + // call delegationBySig to delegate votes from recipient address to delegatee address + vm.expectRevert("ERC20Votes: invalid nonce"); + base.delegateBySig(_delegatee, _nonce + 1, _expiry, v, r, s); + } + + function test_revert_delegateBySig_SignatureExpired() public { + // generate delegation signature + address _delegatee = address(0x789); + uint256 _nonce = base.nonces(recipient); + uint256 _expiry = 1000; + + // vote extends permit, so name-hash and version-hash are same for both + domainSeparator = keccak256( + abi.encode(typehashEip712, permitNameHash, permitVersionHash, block.chainid, address(base)) + ); + bytes32 structHash = keccak256(abi.encode(delegationTypeHash, _delegatee, _nonce, _expiry)); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(recipientPrivateKey, typedDataHash); + + // call delegationBySig to delegate votes from recipient address to delegatee address + vm.warp(_expiry + 1); + vm.expectRevert("ERC20Votes: signature expired"); + base.delegateBySig(_delegatee, _nonce, _expiry, v, r, s); + } +} diff --git a/src/test/sdk/base/ERC721Base.t.sol b/src/test/sdk/base/ERC721Base.t.sol new file mode 100644 index 000000000..7eaf4b2d2 --- /dev/null +++ b/src/test/sdk/base/ERC721Base.t.sol @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import "./BaseUtilTest.sol"; +import { ERC721Base } from "contracts/base/ERC721Base.sol"; + +contract BaseERC721BaseTest is BaseUtilTest { + ERC721Base internal base; + using Strings for uint256; + + function setUp() public override { + vm.prank(deployer); + base = new ERC721Base(deployer, NAME, SYMBOL, royaltyRecipient, royaltyBps); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `mintTo` + //////////////////////////////////////////////////////////////*/ + + function test_state_mintTo() public { + address recipient = address(0x123); + string memory _tokenURI = "tokenURI"; + + uint256 nextTokenId = base.nextTokenIdToMint(); + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + + vm.prank(deployer); + base.mintTo(recipient, _tokenURI); + + assertEq(base.nextTokenIdToMint(), nextTokenId + 1); + assertEq(base.tokenURI(nextTokenId), _tokenURI); + assertEq(base.totalSupply(), currentTotalSupply + 1); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient + 1); + assertEq(base.ownerOf(nextTokenId), recipient); + } + + function test_revert_mintTo_NotAuthorized() public { + address recipient = address(0x123); + string memory _tokenURI = "tokenURI"; + + vm.expectRevert("Not authorized to mint."); + vm.prank(address(0x1)); + base.mintTo(recipient, _tokenURI); + } + + function test_revert_mintTo_MintToZeroAddress() public { + string memory _tokenURI = "tokenURI"; + + vm.expectRevert(bytes4(abi.encodeWithSignature("MintToZeroAddress()"))); + vm.prank(deployer); + base.mintTo(address(0), _tokenURI); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `batchMintTo` + //////////////////////////////////////////////////////////////*/ + + function test_state_batchMintTo() public { + address recipient = address(0x123); + uint256 _quantity = 100; + string memory _baseURI = "baseURI/"; + + uint256 nextTokenId = base.nextTokenIdToMint(); + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + + vm.prank(deployer); + base.batchMintTo(recipient, _quantity, _baseURI, ""); + + assertEq(base.nextTokenIdToMint(), nextTokenId + _quantity); + assertEq(base.totalSupply(), currentTotalSupply + _quantity); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient + _quantity); + for (uint256 i = nextTokenId; i < _quantity; i += 1) { + assertEq(base.tokenURI(i), string(abi.encodePacked(_baseURI, i.toString()))); + assertEq(base.ownerOf(i), recipient); + } + } + + function test_revert_batchMintTo_NotAuthorized() public { + address recipient = address(0x123); + uint256 _quantity = 100; + string memory _baseURI = "baseURI/"; + + vm.expectRevert("Not authorized to mint."); + vm.prank(address(0x1)); + base.batchMintTo(recipient, _quantity, _baseURI, ""); + } + + function test_revert_batchMintTo_MintToZeroAddress() public { + uint256 _quantity = 100; + string memory _baseURI = "baseURI/"; + + vm.expectRevert(bytes4(abi.encodeWithSignature("MintToZeroAddress()"))); + vm.prank(deployer); + base.batchMintTo(address(0), _quantity, _baseURI, ""); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `burn` + //////////////////////////////////////////////////////////////*/ + + function test_state_burn_Owner() public { + address recipient = address(0x123); + string memory _tokenURI = "tokenURI"; + + uint256 nextTokenId = base.nextTokenIdToMint(); + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + + vm.prank(deployer); + base.mintTo(recipient, _tokenURI); + + vm.prank(recipient); + base.burn(nextTokenId); + assertEq(base.nextTokenIdToMint(), nextTokenId + 1); + assertEq(base.tokenURI(nextTokenId), _tokenURI); + assertEq(base.totalSupply(), currentTotalSupply); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient); + + vm.expectRevert(bytes4(abi.encodeWithSignature("OwnerQueryForNonexistentToken()"))); + assertEq(base.ownerOf(nextTokenId), address(0)); + } + + function test_state_burn_Approved() public { + address recipient = address(0x123); + string memory _tokenURI = "tokenURI"; + + address operator = address(0x789); + + uint256 nextTokenId = base.nextTokenIdToMint(); + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + + vm.prank(deployer); + base.mintTo(recipient, _tokenURI); + + vm.prank(recipient); + base.setApprovalForAll(operator, true); + + vm.prank(operator); + base.burn(nextTokenId); + assertEq(base.nextTokenIdToMint(), nextTokenId + 1); + assertEq(base.tokenURI(nextTokenId), _tokenURI); + assertEq(base.totalSupply(), currentTotalSupply); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient); + + vm.expectRevert(bytes4(abi.encodeWithSignature("OwnerQueryForNonexistentToken()"))); + assertEq(base.ownerOf(nextTokenId), address(0)); + } + + function test_revert_burn_NotOwnerNorApproved() public { + address recipient = address(0x123); + string memory _tokenURI = "tokenURI"; + + uint256 nextTokenId = base.nextTokenIdToMint(); + + vm.prank(deployer); + base.mintTo(recipient, _tokenURI); + + vm.prank(address(0x789)); + vm.expectRevert(bytes4(abi.encodeWithSignature("TransferCallerNotOwnerNorApproved()"))); + base.burn(nextTokenId); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `isApprovedOrOwner` + //////////////////////////////////////////////////////////////*/ + + function test_isApprovedOrOwner() public { + address recipient = address(0x123); + string memory _tokenURI = "tokenURI"; + + address operator = address(0x789); + + uint256 nextTokenId = base.nextTokenIdToMint(); + + vm.prank(deployer); + base.mintTo(recipient, _tokenURI); + + assertFalse(base.isApprovedOrOwner(operator, nextTokenId)); + assertEq(base.isApprovedOrOwner(recipient, nextTokenId), true); + + vm.prank(recipient); + base.approve(operator, nextTokenId); + + assertEq(base.isApprovedOrOwner(operator, nextTokenId), true); + } +} diff --git a/src/test/sdk/base/ERC721DelayedReveal.t.sol b/src/test/sdk/base/ERC721DelayedReveal.t.sol new file mode 100644 index 000000000..dce4da3c2 --- /dev/null +++ b/src/test/sdk/base/ERC721DelayedReveal.t.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import "./BaseUtilTest.sol"; +import { ERC721DelayedReveal, BatchMintMetadata } from "contracts/base/ERC721DelayedReveal.sol"; + +contract BaseERC721DelayedRevealTest is BaseUtilTest { + ERC721DelayedReveal internal base; + using Strings for uint256; + + function setUp() public override { + vm.prank(deployer); + base = new ERC721DelayedReveal(deployer, NAME, SYMBOL, royaltyRecipient, royaltyBps); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `lazyMint` + //////////////////////////////////////////////////////////////*/ + + function test_state_lazyMint_noEncryptedURI() public { + uint256 _amount = 100; + string memory _baseURIForTokens = "baseURI/"; + bytes memory _encryptedBaseURI = ""; + + uint256 nextTokenId = base.nextTokenIdToMint(); + + vm.startPrank(deployer); + uint256 batchId = base.lazyMint(_amount, _baseURIForTokens, _encryptedBaseURI); + + assertEq(nextTokenId + _amount, base.nextTokenIdToMint()); + assertEq(nextTokenId + _amount, batchId); + + for (uint256 i = 0; i < _amount; i += 1) { + string memory _tokenURI = base.tokenURI(i); + assertEq(_tokenURI, string(abi.encodePacked(_baseURIForTokens, i.toString()))); + } + + vm.stopPrank(); + } + + function test_state_lazyMint_withEncryptedURI() public { + uint256 _amount = 100; + string memory _baseURIForTokens = "baseURI/"; + string memory secretURI = "secretURI/"; + bytes memory key = "key"; + bytes memory _encryptedBaseURI = base.encryptDecrypt(bytes(secretURI), key); + bytes32 provenanceHash = keccak256(abi.encodePacked(secretURI, key, block.chainid)); + + uint256 nextTokenId = base.nextTokenIdToMint(); + + vm.startPrank(deployer); + uint256 batchId = base.lazyMint(_amount, _baseURIForTokens, abi.encode(_encryptedBaseURI, provenanceHash)); + + assertEq(nextTokenId + _amount, base.nextTokenIdToMint()); + assertEq(nextTokenId + _amount, batchId); + + for (uint256 i = 0; i < _amount; i += 1) { + string memory _tokenURI = base.tokenURI(i); + assertEq(_tokenURI, string(abi.encodePacked(_baseURIForTokens, "0"))); + } + + vm.stopPrank(); + } + + function test_revert_lazyMint_URIForNonExistentId() public { + uint256 _amount = 100; + string memory _baseURIForTokens = "baseURI/"; + + bytes memory key = "key"; + string memory secretURI = "secretURI/"; + bytes memory _encryptedBaseURI = base.encryptDecrypt(bytes(secretURI), key); + bytes32 provenanceHash = keccak256(abi.encodePacked(secretURI, key, block.chainid)); + + vm.startPrank(deployer); + base.lazyMint(_amount, _baseURIForTokens, abi.encode(_encryptedBaseURI, provenanceHash)); + + vm.expectRevert(abi.encodeWithSelector(BatchMintMetadata.BatchMintInvalidTokenId.selector, 100)); + base.tokenURI(100); + + vm.stopPrank(); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `reveal` + //////////////////////////////////////////////////////////////*/ + + function test_state_reveal() public { + uint256 _amount = 100; + string memory _tempURIForTokens = "tempURI/"; + string memory _baseURIForTokens = "baseURI/"; + bytes memory key = "key"; + bytes memory _encryptedBaseURI = base.encryptDecrypt(bytes(_baseURIForTokens), key); + bytes32 provenanceHash = keccak256(abi.encodePacked(_baseURIForTokens, key, block.chainid)); + + vm.startPrank(deployer); + base.lazyMint(_amount, _tempURIForTokens, abi.encode(_encryptedBaseURI, provenanceHash)); + + for (uint256 i = 0; i < _amount; i += 1) { + string memory _tokenURI = base.tokenURI(i); + assertEq(_tokenURI, string(abi.encodePacked(_tempURIForTokens, "0"))); + } + + base.reveal(0, "key"); + + for (uint256 i = 0; i < _amount; i += 1) { + string memory _tokenURI = base.tokenURI(i); + assertEq(_tokenURI, string(abi.encodePacked(_baseURIForTokens, i.toString()))); + } + + vm.stopPrank(); + } + + function test_revert_reveal_NotAuthorized() public { + uint256 _amount = 100; + string memory _tempURIForTokens = "tempURI/"; + string memory _baseURIForTokens = "baseURI/"; + bytes memory key = "key"; + bytes memory _encryptedBaseURI = base.encryptDecrypt(bytes(_baseURIForTokens), key); + bytes32 provenanceHash = keccak256(abi.encodePacked(_baseURIForTokens, key, block.chainid)); + + vm.prank(deployer); + base.lazyMint(_amount, _tempURIForTokens, abi.encode(_encryptedBaseURI, provenanceHash)); + + for (uint256 i = 0; i < _amount; i += 1) { + string memory _tokenURI = base.tokenURI(i); + assertEq(_tokenURI, string(abi.encodePacked(_tempURIForTokens, "0"))); + } + + vm.prank(address(0x345)); + vm.expectRevert("Not authorized"); + base.reveal(0, "key"); + } +} diff --git a/src/test/sdk/base/ERC721Drop.t.sol b/src/test/sdk/base/ERC721Drop.t.sol new file mode 100644 index 000000000..663485705 --- /dev/null +++ b/src/test/sdk/base/ERC721Drop.t.sol @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import "./BaseUtilTest.sol"; +import { ERC721Drop } from "contracts/base/ERC721Drop.sol"; + +contract BaseERC721DropTest is BaseUtilTest { + ERC721Drop internal base; + using Strings for uint256; + + address recipient; + + function setUp() public override { + super.setUp(); + + recipient = address(0x123); + + vm.prank(signer); + base = new ERC721Drop(signer, NAME, SYMBOL, royaltyRecipient, royaltyBps, saleRecipient); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `claim` + //////////////////////////////////////////////////////////////*/ + + function test_state_claim_ZeroPrice() public { + vm.warp(1); + + address receiver = address(0x123); + address claimer = address(0x345); + string memory _baseURI = "baseURI/"; + uint256 _quantity = 10; + + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(receiver); + + bytes32[] memory proofs = new bytes32[](0); + + ERC721Drop.AllowlistProof memory alp; + alp.proof = proofs; + + ERC721Drop.ClaimCondition[] memory conditions = new ERC721Drop.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(signer); + base.lazyMint(100, _baseURI, ""); + + vm.prank(signer); + base.setClaimConditions(conditions[0], false); + + vm.prank(claimer, claimer); + base.claim(receiver, _quantity, address(0), 0, alp, ""); + + assertEq(base.totalSupply(), currentTotalSupply + _quantity); + assertEq(base.balanceOf(receiver), currentBalanceOfRecipient + _quantity); + + for (uint256 i = 0; i < _quantity; i += 1) { + string memory _tokenURI = base.tokenURI(i); + assertEq(_tokenURI, string(abi.encodePacked(_baseURI, i.toString()))); + assertEq(base.ownerOf(i), receiver); + } + } + + function test_state_claim_NonZeroPrice_ERC20() public { + vm.warp(1); + + address receiver = address(0x123); + address claimer = address(0x345); + string memory _baseURI = "baseURI/"; + uint256 _quantity = 10; + + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(receiver); + + bytes32[] memory proofs = new bytes32[](0); + + ERC721Drop.AllowlistProof memory alp; + alp.proof = proofs; + + ERC721Drop.ClaimCondition[] memory conditions = new ERC721Drop.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + // set price and currency + conditions[0].pricePerToken = 1; + conditions[0].currency = address(erc20); + + vm.prank(signer); + base.lazyMint(100, _baseURI, ""); + + vm.prank(signer); + base.setClaimConditions(conditions[0], false); + + // mint erc20 to claimer, and approve to base + erc20.mint(claimer, 1_000); + vm.prank(claimer); + erc20.approve(address(base), 10); + + vm.prank(claimer, claimer); + base.claim(receiver, _quantity, address(erc20), 1, alp, ""); + + assertEq(base.totalSupply(), currentTotalSupply + _quantity); + assertEq(base.balanceOf(receiver), currentBalanceOfRecipient + _quantity); + + for (uint256 i = 0; i < _quantity; i += 1) { + string memory _tokenURI = base.tokenURI(i); + assertEq(_tokenURI, string(abi.encodePacked(_baseURI, i.toString()))); + assertEq(base.ownerOf(i), receiver); + } + } + + function test_state_claim_NonZeroPrice_NativeToken() public { + vm.warp(1); + + address receiver = address(0x123); + address claimer = address(0x345); + string memory _baseURI = "baseURI/"; + uint256 _quantity = 10; + + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(receiver); + + bytes32[] memory proofs = new bytes32[](0); + + ERC721Drop.AllowlistProof memory alp; + alp.proof = proofs; + + ERC721Drop.ClaimCondition[] memory conditions = new ERC721Drop.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + // set price and currency + conditions[0].pricePerToken = 1; + conditions[0].currency = address(NATIVE_TOKEN); + + vm.prank(signer); + base.lazyMint(100, _baseURI, ""); + + vm.prank(signer); + base.setClaimConditions(conditions[0], false); + + // deal NATIVE_TOKEN to claimer + vm.deal(claimer, 1_000); + + vm.prank(claimer, claimer); + base.claim{ value: 10 }(receiver, _quantity, address(NATIVE_TOKEN), 1, alp, ""); + + assertEq(base.totalSupply(), currentTotalSupply + _quantity); + assertEq(base.balanceOf(receiver), currentBalanceOfRecipient + _quantity); + + for (uint256 i = 0; i < _quantity; i += 1) { + string memory _tokenURI = base.tokenURI(i); + assertEq(_tokenURI, string(abi.encodePacked(_baseURI, i.toString()))); + assertEq(base.ownerOf(i), receiver); + } + } + + function test_revert_claim_NotEnoughMintedTokens() public { + vm.warp(1); + + address receiver = address(0x123); + address claimer = address(0x345); + string memory _baseURI = "baseURI/"; + uint256 _quantity = 10; + + bytes32[] memory proofs = new bytes32[](0); + + ERC721Drop.AllowlistProof memory alp; + alp.proof = proofs; + + ERC721Drop.ClaimCondition[] memory conditions = new ERC721Drop.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(signer); + base.lazyMint(100, _baseURI, ""); + + vm.prank(signer); + base.setClaimConditions(conditions[0], false); + + vm.expectRevert("Not enough minted tokens"); + vm.prank(claimer, claimer); + base.claim(receiver, _quantity + 1000, address(0), 0, alp, ""); + } +} diff --git a/src/test/sdk/base/ERC721LazyMint.t.sol b/src/test/sdk/base/ERC721LazyMint.t.sol new file mode 100644 index 000000000..241d82116 --- /dev/null +++ b/src/test/sdk/base/ERC721LazyMint.t.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import "./BaseUtilTest.sol"; +import { ERC721LazyMint } from "contracts/base/ERC721LazyMint.sol"; + +contract BaseERC721LazyMintTest is BaseUtilTest { + ERC721LazyMint internal base; + using Strings for uint256; + + uint256 _amount; + string _baseURIForTokens; + bytes _encryptedBaseURI; + + function setUp() public override { + vm.prank(deployer); + base = new ERC721LazyMint(deployer, NAME, SYMBOL, royaltyRecipient, royaltyBps); + + _amount = 10; + _baseURIForTokens = "baseURI/"; + _encryptedBaseURI = ""; + + vm.prank(deployer); + base.lazyMint(_amount, _baseURIForTokens, _encryptedBaseURI); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `claim` + //////////////////////////////////////////////////////////////*/ + + function test_state_claim() public { + address recipient = address(0x123); + uint256 quantity = 5; + + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + + vm.startPrank(recipient); + + base.claim(recipient, quantity); + + assertEq(base.totalSupply(), currentTotalSupply + quantity); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient + quantity); + + for (uint256 i = 0; i < quantity; i += 1) { + string memory _tokenURI = base.tokenURI(i); + assertEq(_tokenURI, string(abi.encodePacked(_baseURIForTokens, i.toString()))); + assertEq(base.ownerOf(i), recipient); + } + + vm.stopPrank(); + } + + function test_revert_claim_NotEnoughTokens() public { + address recipient = address(0x123); + + vm.startPrank(recipient); + + vm.expectRevert("Not enough lazy minted tokens."); + base.claim(recipient, _amount + 1); + + vm.stopPrank(); + } +} diff --git a/src/test/sdk/base/ERC721Multiwrap.t.sol b/src/test/sdk/base/ERC721Multiwrap.t.sol new file mode 100644 index 000000000..30e718d23 --- /dev/null +++ b/src/test/sdk/base/ERC721Multiwrap.t.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import "./BaseUtilTest.sol"; +import { ERC721Multiwrap } from "contracts/base/ERC721Multiwrap.sol"; +import { CurrencyTransferLib } from "contracts/lib/CurrencyTransferLib.sol"; +import { ITokenBundle } from "contracts/extension/interface/ITokenBundle.sol"; + +contract BaseERC721MultiwrapTest is BaseUtilTest { + ERC721Multiwrap internal base; + using Strings for uint256; + + Wallet internal tokenOwner; + string internal uriForWrappedToken; + ITokenBundle.Token[] internal wrappedContent; + + function setUp() public override { + super.setUp(); + + vm.prank(deployer); + base = new ERC721Multiwrap( + deployer, + NAME, + SYMBOL, + royaltyRecipient, + royaltyBps, + CurrencyTransferLib.NATIVE_TOKEN + ); + + tokenOwner = getWallet(); + uriForWrappedToken = "ipfs://baseURI/"; + + wrappedContent.push( + ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 10 ether + }) + ); + wrappedContent.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 0, + totalAmount: 1 + }) + ); + wrappedContent.push( + ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 0, + totalAmount: 100 + }) + ); + + erc20.mint(address(tokenOwner), 10 ether); + erc721.mint(address(tokenOwner), 1); + erc1155.mint(address(tokenOwner), 0, 100); + + tokenOwner.setAllowanceERC20(address(erc20), address(base), type(uint256).max); + tokenOwner.setApprovalForAllERC721(address(erc721), address(base), true); + tokenOwner.setApprovalForAllERC1155(address(erc1155), address(base), true); + + vm.prank(deployer); + base.grantRole(keccak256("MINTER_ROLE"), address(tokenOwner)); + } + + function getWrappedContents(uint256 _tokenId) public view returns (ITokenBundle.Token[] memory contents) { + uint256 total = base.getTokenCountOfBundle(_tokenId); + contents = new ITokenBundle.Token[](total); + + for (uint256 i = 0; i < total; i += 1) { + contents[i] = base.getTokenOfBundle(_tokenId, i); + } + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `wrap` + //////////////////////////////////////////////////////////////*/ + + function test_state_wrap() public { + uint256 expectedIdForWrappedToken = base.nextTokenIdToMint(); + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + base.wrap(wrappedContent, uriForWrappedToken, recipient); + + assertEq(expectedIdForWrappedToken + 1, base.nextTokenIdToMint()); + + ITokenBundle.Token[] memory contentsOfWrappedToken = getWrappedContents(expectedIdForWrappedToken); + assertEq(contentsOfWrappedToken.length, wrappedContent.length); + for (uint256 i = 0; i < contentsOfWrappedToken.length; i += 1) { + assertEq(contentsOfWrappedToken[i].assetContract, wrappedContent[i].assetContract); + assertEq(uint256(contentsOfWrappedToken[i].tokenType), uint256(wrappedContent[i].tokenType)); + assertEq(contentsOfWrappedToken[i].tokenId, wrappedContent[i].tokenId); + assertEq(contentsOfWrappedToken[i].totalAmount, wrappedContent[i].totalAmount); + } + + assertEq(uriForWrappedToken, base.tokenURI(expectedIdForWrappedToken)); + } +} diff --git a/src/test/sdk/base/ERC721SignatureMint.t.sol b/src/test/sdk/base/ERC721SignatureMint.t.sol new file mode 100644 index 000000000..0dad51e65 --- /dev/null +++ b/src/test/sdk/base/ERC721SignatureMint.t.sol @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import "./BaseUtilTest.sol"; +import { ERC721SignatureMint } from "contracts/base/ERC721SignatureMint.sol"; + +contract BaseERC721SignatureMintTest is BaseUtilTest { + ERC721SignatureMint internal base; + using Strings for uint256; + + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + ERC721SignatureMint.MintRequest _mintrequest; + bytes _signature; + + address recipient; + + function setUp() public override { + super.setUp(); + vm.prank(signer); + base = new ERC721SignatureMint(signer, NAME, SYMBOL, royaltyRecipient, royaltyBps, saleRecipient); + + recipient = address(0x123); + erc20.mint(recipient, 1_000); + vm.deal(recipient, 1_000); + + typehashMintRequest = keccak256( + "MintRequest(address to,address royaltyRecipient,uint256 royaltyBps,address primarySaleRecipient,string uri,uint256 quantity,uint256 pricePerToken,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + nameHash = keccak256(bytes("SignatureMintERC721")); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256(abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(base))); + + _mintrequest.to = recipient; + _mintrequest.royaltyRecipient = royaltyRecipient; + _mintrequest.royaltyBps = royaltyBps; + _mintrequest.primarySaleRecipient = saleRecipient; + _mintrequest.uri = "ipfs://"; + _mintrequest.quantity = 1; + _mintrequest.pricePerToken = 0; + _mintrequest.currency = address(0); + _mintrequest.validityStartTimestamp = 1000; + _mintrequest.validityEndTimestamp = 2000; + _mintrequest.uid = bytes32(0); + + _signature = signMintRequest(_mintrequest, privateKey); + } + + function signMintRequest( + ERC721SignatureMint.MintRequest memory _request, + uint256 _privateKey + ) internal view returns (bytes memory) { + bytes memory encodedRequest = abi.encode( + typehashMintRequest, + _request.to, + _request.royaltyRecipient, + _request.royaltyBps, + _request.primarySaleRecipient, + keccak256(bytes(_request.uri)), + _request.quantity, + _request.pricePerToken, + _request.currency, + _request.validityStartTimestamp, + _request.validityEndTimestamp, + _request.uid + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, typedDataHash); + bytes memory sig = abi.encodePacked(r, s, v); + + return sig; + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `mintWithSignature` + //////////////////////////////////////////////////////////////*/ + + function test_state_mintWithSignature_ZeroPrice() public { + vm.warp(1000); + + uint256 nextTokenId = base.nextTokenIdToMint(); + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + + base.mintWithSignature(_mintrequest, _signature); + + assertEq(base.nextTokenIdToMint(), nextTokenId + _mintrequest.quantity); + assertEq(base.tokenURI(nextTokenId), string(abi.encodePacked(_mintrequest.uri))); + assertEq(base.totalSupply(), currentTotalSupply + _mintrequest.quantity); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient + _mintrequest.quantity); + assertEq(base.ownerOf(nextTokenId), recipient); + } + + function test_state_mintWithSignature_NonZeroPrice_ERC20() public { + vm.warp(1000); + + _mintrequest.pricePerToken = 1; + _mintrequest.currency = address(erc20); + _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(recipient); + erc20.approve(address(base), 1); + + uint256 nextTokenId = base.nextTokenIdToMint(); + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + + vm.prank(recipient); + base.mintWithSignature(_mintrequest, _signature); + + assertEq(base.nextTokenIdToMint(), nextTokenId + _mintrequest.quantity); + assertEq(base.tokenURI(nextTokenId), string(abi.encodePacked(_mintrequest.uri))); + assertEq(base.totalSupply(), currentTotalSupply + _mintrequest.quantity); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient + _mintrequest.quantity); + assertEq(base.ownerOf(nextTokenId), recipient); + } + + function test_state_mintWithSignature_NonZeroPrice_NativeToken() public { + vm.warp(1000); + + _mintrequest.pricePerToken = 1; + _mintrequest.currency = address(NATIVE_TOKEN); + _signature = signMintRequest(_mintrequest, privateKey); + + uint256 nextTokenId = base.nextTokenIdToMint(); + uint256 currentTotalSupply = base.totalSupply(); + uint256 currentBalanceOfRecipient = base.balanceOf(recipient); + + vm.deal(recipient, 1); + + vm.prank(recipient); + base.mintWithSignature{ value: 1 }(_mintrequest, _signature); + + assertEq(base.nextTokenIdToMint(), nextTokenId + _mintrequest.quantity); + assertEq(base.tokenURI(nextTokenId), string(abi.encodePacked(_mintrequest.uri))); + assertEq(base.totalSupply(), currentTotalSupply + _mintrequest.quantity); + assertEq(base.balanceOf(recipient), currentBalanceOfRecipient + _mintrequest.quantity); + assertEq(base.ownerOf(nextTokenId), recipient); + } + + function test_revert_mintWithSignature_MustSendTotalPrice() public { + vm.warp(1000); + + _mintrequest.pricePerToken = 1; + _mintrequest.currency = address(NATIVE_TOKEN); + _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(recipient); + vm.expectRevert("Invalid msg value"); + base.mintWithSignature{ value: 0 }(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_QuantityNotOne() public { + vm.warp(1000); + + _mintrequest.quantity = 0; + _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("quantiy must be 1"); + base.mintWithSignature(_mintrequest, _signature); + } +} diff --git a/src/test/sdk/extension/BatchMintMetadata.t.sol b/src/test/sdk/extension/BatchMintMetadata.t.sol new file mode 100644 index 000000000..31da1aa4d --- /dev/null +++ b/src/test/sdk/extension/BatchMintMetadata.t.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BatchMintMetadata } from "contracts/extension/BatchMintMetadata.sol"; + +contract MyBatchMintMetadata is BatchMintMetadata { + function setBaseURI(uint256 _batchId, string memory _baseURI) external { + _setBaseURI(_batchId, _baseURI); + } + + function batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) external returns (uint256 nextTokenIdToMint, uint256 batchId) { + (nextTokenIdToMint, batchId) = _batchMintMetadata(_startId, _amountToMint, _baseURIForTokens); + } + + function viewBaseURI(uint256 _tokenId) external view returns (string memory) { + return _getBaseURI(_tokenId); + } + + function freezeBaseURI(uint256 _batchId) external { + _freezeBaseURI(_batchId); + } +} + +contract ExtensionBatchMintMetadata is DSTest, Test { + MyBatchMintMetadata internal ext; + + function setUp() public { + ext = new MyBatchMintMetadata(); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `batchMintMetadata` + //////////////////////////////////////////////////////////////*/ + + function test_state_batchMintMetadata() public { + (uint256 nextTokenIdToMint, uint256 batchId) = ext.batchMintMetadata(0, 100, ""); + assertEq(nextTokenIdToMint, 100); + assertEq(batchId, 100); + + (nextTokenIdToMint, batchId) = ext.batchMintMetadata(100, 100, ""); + assertEq(nextTokenIdToMint, 200); + assertEq(batchId, 200); + + assertEq(2, ext.getBaseURICount()); + + assertEq(100, ext.getBatchIdAtIndex(0)); + assertEq(200, ext.getBatchIdAtIndex(1)); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `setBaseURI` + //////////////////////////////////////////////////////////////*/ + + function test_state_setBaseURI() public { + string memory baseUriOne = "one"; + string memory baseUriTwo = "two"; + + (, uint256 batchId) = ext.batchMintMetadata(0, 100, baseUriOne); + + assertEq(baseUriOne, ext.viewBaseURI(10)); + + ext.setBaseURI(batchId, baseUriTwo); + assertEq(baseUriTwo, ext.viewBaseURI(10)); + } + + function test_setBaseURI_revert_frozen() public { + string memory baseUriOne = "one"; + (, uint256 batchId) = ext.batchMintMetadata(0, 100, baseUriOne); + + ext.freezeBaseURI(batchId); + + vm.expectRevert(abi.encodeWithSelector(BatchMintMetadata.BatchMintMetadataFrozen.selector, batchId)); + string memory baseUri = "one"; + ext.setBaseURI(batchId, baseUri); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `freezeBaseURI` + //////////////////////////////////////////////////////////////*/ + + function test_state_freezeBaseURI() public { + string memory baseUriOne = "one"; + (, uint256 batchId) = ext.batchMintMetadata(0, 100, baseUriOne); + + ext.freezeBaseURI(batchId); + assertEq(ext.batchFrozen(batchId), true); + } + + function test_freezeBaseURI_revert_invalidBatch() public { + vm.expectRevert(abi.encodeWithSelector(BatchMintMetadata.BatchMintInvalidBatchId.selector, 100)); + ext.freezeBaseURI(100); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `viewBaseURI` + //////////////////////////////////////////////////////////////*/ + + function test_viewBaseURI_revert_invalidTokenId() public { + vm.expectRevert(abi.encodeWithSelector(BatchMintMetadata.BatchMintInvalidTokenId.selector, 100)); + ext.viewBaseURI(100); + } +} diff --git a/src/test/sdk/extension/ContractMetadata.t.sol b/src/test/sdk/extension/ContractMetadata.t.sol new file mode 100644 index 000000000..ad6e7060d --- /dev/null +++ b/src/test/sdk/extension/ContractMetadata.t.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { ContractMetadata } from "contracts/extension/ContractMetadata.sol"; + +contract MyContractMetadata is ContractMetadata { + bool condition; + + function setCondition(bool _condition) external { + condition = _condition; + } + + function _canSetContractURI() internal view override returns (bool) { + return condition; + } +} + +contract ExtensionContractMetadataTest is DSTest, Test { + MyContractMetadata internal ext; + event ContractURIUpdated(string prevURI, string newURI); + + function setUp() public { + ext = new MyContractMetadata(); + } + + function test_state_setContractURI() public { + ext.setCondition(true); + + string memory uri = "uri_string"; + ext.setContractURI(uri); + + string memory contractURI = ext.contractURI(); + + assertEq(contractURI, uri); + } + + function test_revert_setContractURI() public { + vm.expectRevert(abi.encodeWithSelector(ContractMetadata.ContractMetadataUnauthorized.selector)); + ext.setContractURI(""); + } + + function test_event_setContractURI() public { + ext.setCondition(true); + string memory uri = "uri_string"; + + vm.expectEmit(true, true, true, true); + emit ContractURIUpdated("", uri); + + ext.setContractURI(uri); + } +} diff --git a/src/test/sdk/extension/DelayedReveal.t.sol b/src/test/sdk/extension/DelayedReveal.t.sol new file mode 100644 index 000000000..12576b227 --- /dev/null +++ b/src/test/sdk/extension/DelayedReveal.t.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { DelayedReveal } from "contracts/extension/DelayedReveal.sol"; + +contract MyDelayedReveal is DelayedReveal { + function setEncryptedData(uint256 _batchId, bytes memory _encryptedData) external { + _setEncryptedData(_batchId, _encryptedData); + } + + function reveal(uint256 identifier, bytes calldata key) external returns (string memory revealedURI) {} +} + +contract ExtensionDelayedReveal is DSTest, Test { + MyDelayedReveal internal ext; + + function setUp() public { + ext = new MyDelayedReveal(); + } + + function test_state_setEncryptedData() public { + string memory uriToEncrypt = "uri_string"; + bytes memory key = "key"; + + bytes memory encryptedUri = ext.encryptDecrypt(bytes(uriToEncrypt), key); + bytes32 provenanceHash = keccak256(abi.encodePacked(uriToEncrypt, key, block.chainid)); + + bytes memory data = abi.encode(encryptedUri, provenanceHash); + + ext.setEncryptedData(0, data); + + assertEq(true, ext.isEncryptedBatch(0)); + } + + function test_state_getRevealURI() public { + string memory uriToEncrypt = "uri_string"; + bytes memory key = "key"; + + bytes memory encryptedUri = ext.encryptDecrypt(bytes(uriToEncrypt), key); + bytes32 provenanceHash = keccak256(abi.encodePacked(uriToEncrypt, key, block.chainid)); + + bytes memory data = abi.encode(encryptedUri, provenanceHash); + + ext.setEncryptedData(0, data); + + string memory revealedURI = ext.getRevealURI(0, key); + + assertEq(uriToEncrypt, revealedURI); + } + + function test_revert_getRevealURI_IncorrectKey() public { + string memory uriToEncrypt = "uri_string"; + bytes memory key = "key"; + bytes memory incorrectKey = "incorrect key"; + + bytes memory encryptedUri = ext.encryptDecrypt(bytes(uriToEncrypt), key); + bytes32 provenanceHash = keccak256(abi.encodePacked(uriToEncrypt, key, block.chainid)); + string memory incorrectURI = string(ext.encryptDecrypt(encryptedUri, incorrectKey)); + + bytes memory data = abi.encode(encryptedUri, provenanceHash); + + ext.setEncryptedData(0, data); + + vm.expectRevert( + abi.encodeWithSelector( + DelayedReveal.DelayedRevealIncorrectResultHash.selector, + provenanceHash, + keccak256(abi.encodePacked(incorrectURI, incorrectKey, block.chainid)) + ) + ); + ext.getRevealURI(0, incorrectKey); + } + + function test_revert_getRevealURI_NothingToReveal() public { + string memory uriToEncrypt = "uri_string"; + bytes memory key = "key"; + + bytes memory encryptedUri = ext.encryptDecrypt(bytes(uriToEncrypt), key); + bytes32 provenanceHash = keccak256(abi.encodePacked(uriToEncrypt, key, block.chainid)); + + bytes memory data = abi.encode(encryptedUri, provenanceHash); + + ext.setEncryptedData(0, data); + assertEq(true, ext.isEncryptedBatch(0)); + + ext.setEncryptedData(0, ""); + assertFalse(ext.isEncryptedBatch(0)); + + vm.expectRevert(abi.encodeWithSelector(DelayedReveal.DelayedRevealNothingToReveal.selector)); + ext.getRevealURI(0, key); + } +} diff --git a/src/test/sdk/extension/DropSinglePhase.t.sol b/src/test/sdk/extension/DropSinglePhase.t.sol new file mode 100644 index 000000000..51d684eb6 --- /dev/null +++ b/src/test/sdk/extension/DropSinglePhase.t.sol @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; +import { Strings } from "contracts/lib/Strings.sol"; + +import { DropSinglePhase } from "contracts/extension/DropSinglePhase.sol"; + +contract MyDropSinglePhase is DropSinglePhase { + bool condition; + + function setCondition(bool _condition) external { + condition = _condition; + } + + function _canSetClaimConditions() internal view override returns (bool) { + return condition; + } + + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override {} + + function _transferTokensOnClaim( + address _to, + uint256 _quantityBeingClaimed + ) internal override returns (uint256 startTokenId) {} +} + +contract ExtensionDropSinglePhase is DSTest, Test { + using Strings for uint256; + MyDropSinglePhase internal ext; + + event TokensClaimed( + address indexed claimer, + address indexed receiver, + uint256 indexed startTokenId, + uint256 quantityClaimed + ); + event ClaimConditionUpdated(MyDropSinglePhase.ClaimCondition condition, bool resetEligibility); + + function setUp() public { + ext = new MyDropSinglePhase(); + } + + /*/////////////////////////////////////////////////////////////// + Claim Tests + //////////////////////////////////////////////////////////////*/ + + /** + * note: Testing revert condition; exceed max claimable supply. + */ + function test_revert_claimCondition_exceedMaxClaimableSupply() public { + ext.setCondition(true); + vm.warp(1); + + address receiver = address(0x123); + address claimer1 = address(0x345); + address claimer2 = address(0x567); + bytes32[] memory proofs = new bytes32[](0); + + MyDropSinglePhase.AllowlistProof memory alp; + alp.proof = proofs; + + MyDropSinglePhase.ClaimCondition[] memory conditions = new MyDropSinglePhase.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + ext.setClaimConditions(conditions[0], false); + + vm.prank(claimer1, claimer1); + ext.claim(receiver, 100, address(0), 0, alp, ""); + + vm.expectRevert( + abi.encodeWithSelector( + DropSinglePhase.DropClaimExceedMaxSupply.selector, + conditions[0].maxClaimableSupply, + 1 + conditions[0].maxClaimableSupply + ) + ); + vm.prank(claimer2, claimer2); + ext.claim(receiver, 1, address(0), 0, alp, ""); + } + + /** + * note: Testing quantity limit restriction when no allowlist present. + */ + function test_fuzz_claim_noAllowlist(uint256 x) public { + ext.setCondition(true); + vm.assume(x != 0); + vm.warp(1); + + address receiver = address(0x123); + address claimer = address(0x345); + bytes32[] memory proofs = new bytes32[](0); + + MyDropSinglePhase.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = x; + + MyDropSinglePhase.ClaimCondition[] memory conditions = new MyDropSinglePhase.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 100; + + ext.setClaimConditions(conditions[0], false); + + vm.prank(claimer, claimer); + vm.expectRevert( + abi.encodeWithSelector( + DropSinglePhase.DropClaimExceedLimit.selector, + conditions[0].quantityLimitPerWallet, + 101 + ) + ); + ext.claim(receiver, 101, address(0), 0, alp, ""); + + ext.setClaimConditions(conditions[0], true); + + vm.prank(claimer, claimer); + vm.expectRevert( + abi.encodeWithSelector( + DropSinglePhase.DropClaimExceedLimit.selector, + conditions[0].quantityLimitPerWallet, + 101 + ) + ); + ext.claim(receiver, 101, address(0), 0, alp, ""); + } + + function test_fuzz_claim_merkleProof(uint256 x) public { + ext.setCondition(true); + vm.assume(x > 10 && x < 500); + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = Strings.toString(x); + inputs[3] = "0"; + inputs[4] = "0x0000000000000000000000000000000000000000"; + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + MyDropSinglePhase.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = x; + alp.pricePerToken = 0; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + + // bytes32[] memory proofs = new bytes32[](0); + + MyDropSinglePhase.ClaimCondition[] memory conditions = new MyDropSinglePhase.ClaimCondition[](1); + conditions[0].maxClaimableSupply = x; + conditions[0].quantityLimitPerWallet = 1; + conditions[0].merkleRoot = root; + + ext.setClaimConditions(conditions[0], false); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + ext.claim(receiver, x - 5, address(0), 0, alp, ""); + assertEq(ext.getSupplyClaimedByWallet(receiver), x - 5); + + vm.prank(receiver, receiver); + vm.expectRevert( + abi.encodeWithSelector(DropSinglePhase.DropClaimExceedLimit.selector, alp.quantityLimitPerWallet, x + 1) + ); + ext.claim(receiver, 6, address(0), 0, alp, ""); + + vm.prank(receiver, receiver); + ext.claim(receiver, 5, address(0), 0, alp, ""); + assertEq(ext.getSupplyClaimedByWallet(receiver), x); + + vm.prank(receiver, receiver); + vm.expectRevert( + abi.encodeWithSelector(DropSinglePhase.DropClaimExceedLimit.selector, alp.quantityLimitPerWallet, x + 5) + ); + ext.claim(receiver, 5, address(0), 0, alp, ""); + } + + /** + * note: Testing event emission on setClaimConditions. + */ + function test_event_setClaimConditions() public { + ext.setCondition(true); + vm.warp(1); + + bytes32[] memory proofs = new bytes32[](0); + + MyDropSinglePhase.AllowlistProof memory alp; + alp.proof = proofs; + + MyDropSinglePhase.ClaimCondition[] memory conditions = new MyDropSinglePhase.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.expectEmit(true, true, true, true); + emit ClaimConditionUpdated(conditions[0], false); + + ext.setClaimConditions(conditions[0], false); + } + + /** + * note: Testing event emission on claim. + */ + function test_event_claim() public { + ext.setCondition(true); + vm.warp(1); + + address receiver = address(0x123); + address claimer = address(0x345); + bytes32[] memory proofs = new bytes32[](0); + + MyDropSinglePhase.AllowlistProof memory alp; + alp.proof = proofs; + + MyDropSinglePhase.ClaimCondition[] memory conditions = new MyDropSinglePhase.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + ext.setClaimConditions(conditions[0], false); + + vm.startPrank(claimer, claimer); + + vm.expectEmit(true, true, true, true); + emit TokensClaimed(claimer, receiver, 0, 1); + + ext.claim(receiver, 1, address(0), 0, alp, ""); + } +} diff --git a/src/test/sdk/extension/DropSinglePhase1155.t.sol b/src/test/sdk/extension/DropSinglePhase1155.t.sol new file mode 100644 index 000000000..dc6e02689 --- /dev/null +++ b/src/test/sdk/extension/DropSinglePhase1155.t.sol @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { DropSinglePhase1155 } from "contracts/extension/DropSinglePhase1155.sol"; + +contract MyDropSinglePhase1155 is DropSinglePhase1155 { + bool condition; + + function setCondition(bool _condition) external { + condition = _condition; + } + + function _canSetClaimConditions() internal view override returns (bool) { + return condition; + } + + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override {} + + function _transferTokensOnClaim(address _to, uint256 _tokenId, uint256 _quantityBeingClaimed) internal override {} +} + +contract ExtensionDropSinglePhase1155 is DSTest, Test { + MyDropSinglePhase1155 internal ext; + + event TokensClaimed( + address indexed claimer, + address indexed receiver, + uint256 indexed tokenId, + uint256 quantityClaimed + ); + event ClaimConditionUpdated( + uint256 indexed tokenId, + MyDropSinglePhase1155.ClaimCondition condition, + bool resetEligibility + ); + + function setUp() public { + ext = new MyDropSinglePhase1155(); + } + + /*/////////////////////////////////////////////////////////////// + Claim Tests + //////////////////////////////////////////////////////////////*/ + + /** + * note: Testing revert condition; exceed max claimable supply. + */ + function test_revert_claimCondition_exceedMaxClaimableSupply() public { + ext.setCondition(true); + vm.warp(1); + + address receiver = address(0x123); + address claimer1 = address(0x345); + address claimer2 = address(0x567); + bytes32[] memory proofs = new bytes32[](0); + uint256 _tokenId = 0; + + MyDropSinglePhase1155.AllowlistProof memory alp; + alp.proof = proofs; + + MyDropSinglePhase1155.ClaimCondition[] memory conditions = new MyDropSinglePhase1155.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + ext.setClaimConditions(_tokenId, conditions[0], false); + + vm.prank(claimer1, claimer1); + ext.claim(receiver, _tokenId, 100, address(0), 0, alp, ""); + + vm.expectRevert("!MaxSupply"); + vm.prank(claimer2, claimer2); + ext.claim(receiver, _tokenId, 1, address(0), 0, alp, ""); + } + + /** + * note: Testing quantity limit restriction when no allowlist present. + */ + function test_fuzz_claim_noAllowlist(uint256 x) public { + ext.setCondition(true); + vm.assume(x != 0); + vm.warp(1); + + address receiver = address(0x123); + address claimer = address(0x345); + bytes32[] memory proofs = new bytes32[](0); + uint256 _tokenId = 0; + + MyDropSinglePhase1155.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = x; + + MyDropSinglePhase1155.ClaimCondition[] memory conditions = new MyDropSinglePhase1155.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 100; + + ext.setClaimConditions(_tokenId, conditions[0], false); + + bytes memory errorQty = "!Qty"; + + vm.prank(claimer, claimer); + vm.expectRevert(errorQty); + ext.claim(receiver, _tokenId, 101, address(0), 0, alp, ""); + + ext.setClaimConditions(_tokenId, conditions[0], true); + + vm.prank(claimer, claimer); + vm.expectRevert(errorQty); + ext.claim(receiver, _tokenId, 101, address(0), 0, alp, ""); + } + + /** + * note: Testing event emission on setClaimConditions. + */ + function test_event_setClaimConditions() public { + ext.setCondition(true); + vm.warp(1); + + bytes32[] memory proofs = new bytes32[](0); + uint256 _tokenId = 0; + + MyDropSinglePhase1155.AllowlistProof memory alp; + alp.proof = proofs; + + MyDropSinglePhase1155.ClaimCondition[] memory conditions = new MyDropSinglePhase1155.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.expectEmit(true, true, true, true); + emit ClaimConditionUpdated(_tokenId, conditions[0], false); + + ext.setClaimConditions(_tokenId, conditions[0], false); + } + + /** + * note: Testing event emission on claim. + */ + function test_event_claim() public { + ext.setCondition(true); + vm.warp(1); + + address receiver = address(0x123); + address claimer = address(0x345); + bytes32[] memory proofs = new bytes32[](0); + uint256 _tokenId = 0; + + MyDropSinglePhase1155.AllowlistProof memory alp; + alp.proof = proofs; + + MyDropSinglePhase1155.ClaimCondition[] memory conditions = new MyDropSinglePhase1155.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + ext.setClaimConditions(_tokenId, conditions[0], false); + + vm.startPrank(claimer, claimer); + + vm.expectEmit(true, true, true, true); + emit TokensClaimed(claimer, receiver, _tokenId, 1); + + ext.claim(receiver, _tokenId, 1, address(0), 0, alp, ""); + } + + function test_claimCondition_resetEligibility_quantityLimitPerWallet() public { + ext.setCondition(true); + vm.warp(1); + + address receiver = address(0x123); + bytes32[] memory proofs = new bytes32[](0); + + MyDropSinglePhase1155.AllowlistProof memory alp; + alp.proof = proofs; + + MyDropSinglePhase1155.ClaimCondition[] memory conditions = new MyDropSinglePhase1155.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + ext.setClaimConditions(0, conditions[0], false); + + vm.prank(receiver, receiver); + ext.claim(receiver, 0, 10, address(0), 0, alp, ""); + assertEq(ext.getSupplyClaimedByWallet(0, receiver), 10); + + vm.roll(100); + ext.setClaimConditions(0, conditions[0], true); + assertEq(ext.getSupplyClaimedByWallet(0, receiver), 0); + + vm.prank(receiver, receiver); + ext.claim(receiver, 0, 10, address(0), 0, alp, ""); + assertEq(ext.getSupplyClaimedByWallet(0, receiver), 10); + } + + /** + * note: Testing state; unique condition Id for every token. + */ + function test_state_claimCondition_uniqueConditionId() public { + ext.setCondition(true); + vm.warp(1); + + address receiver = address(0x123); + address claimer1 = address(0x345); + bytes32[] memory proofs = new bytes32[](0); + uint256 _tokenId = 0; + + MyDropSinglePhase1155.AllowlistProof memory alp; + alp.proof = proofs; + + MyDropSinglePhase1155.ClaimCondition[] memory conditions = new MyDropSinglePhase1155.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + ext.setClaimConditions(_tokenId, conditions[0], false); + + vm.prank(claimer1, claimer1); + ext.claim(receiver, _tokenId, 100, address(0), 0, alp, ""); + + assertEq(ext.getSupplyClaimedByWallet(_tokenId, claimer1), 100); + + // supply claimed for other tokenIds should be 0 + assertEq(ext.getSupplyClaimedByWallet(1, claimer1), 0); + assertEq(ext.getSupplyClaimedByWallet(2, claimer1), 0); + } +} diff --git a/src/test/sdk/extension/ExtensionUtilTest.sol b/src/test/sdk/extension/ExtensionUtilTest.sol new file mode 100644 index 000000000..694ab6946 --- /dev/null +++ b/src/test/sdk/extension/ExtensionUtilTest.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; +import "../../utils/Wallet.sol"; +import "../../mocks/WETH9.sol"; +import "../../mocks/MockERC20.sol"; +import "../../mocks/MockERC721.sol"; +import "../../mocks/MockERC1155.sol"; +import { MockERC721NonBurnable } from "../../mocks/MockERC721NonBurnable.sol"; +import { MockERC1155NonBurnable } from "../../mocks/MockERC1155NonBurnable.sol"; +import "contracts/infra/forwarder/Forwarder.sol"; + +abstract contract ExtensionUtilTest is DSTest, Test { + string public constant NAME = "NAME"; + string public constant SYMBOL = "SYMBOL"; + string public constant CONTRACT_URI = "CONTRACT_URI"; + address public constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + MockERC20 public erc20; + MockERC721 public erc721; + MockERC1155 public erc1155; + MockERC721NonBurnable public erc721NonBurnable; + MockERC1155NonBurnable public erc1155NonBurnable; + WETH9 public weth; + + address public forwarder; + + address public deployer = address(0x20000); + address public saleRecipient = address(0x30000); + address public royaltyRecipient = address(0x30001); + address public platformFeeRecipient = address(0x30002); + uint128 public royaltyBps = 500; // 5% + uint128 public platformFeeBps = 500; // 5% + uint256 public constant MAX_BPS = 10_000; // 100% + + uint256 public privateKey = 1234; + address public signer; + + mapping(bytes32 => address) public contracts; + + function setUp() public virtual { + signer = vm.addr(privateKey); + + erc20 = new MockERC20(); + erc721 = new MockERC721(); + erc1155 = new MockERC1155(); + erc721NonBurnable = new MockERC721NonBurnable(); + erc1155NonBurnable = new MockERC1155NonBurnable(); + weth = new WETH9(); + forwarder = address(new Forwarder()); + } + + function getActor(uint160 _index) public pure returns (address) { + return address(uint160(0x50000 + _index)); + } + + function getWallet() public returns (Wallet wallet) { + wallet = new Wallet(); + } + + function assertIsOwnerERC721(address _token, address _owner, uint256[] memory _tokenIds) internal { + for (uint256 i = 0; i < _tokenIds.length; i += 1) { + bool isOwnerOfToken = MockERC721(_token).ownerOf(_tokenIds[i]) == _owner; + assertTrue(isOwnerOfToken); + } + } + + function assertIsNotOwnerERC721(address _token, address _owner, uint256[] memory _tokenIds) internal { + for (uint256 i = 0; i < _tokenIds.length; i += 1) { + bool isOwnerOfToken = MockERC721(_token).ownerOf(_tokenIds[i]) == _owner; + assertTrue(!isOwnerOfToken); + } + } + + function assertBalERC1155Eq( + address _token, + address _owner, + uint256[] memory _tokenIds, + uint256[] memory _amounts + ) internal { + require(_tokenIds.length == _amounts.length, "unequal lengths"); + + for (uint256 i = 0; i < _tokenIds.length; i += 1) { + assertEq(MockERC1155(_token).balanceOf(_owner, _tokenIds[i]), _amounts[i]); + } + } + + function assertBalERC1155Gte( + address _token, + address _owner, + uint256[] memory _tokenIds, + uint256[] memory _amounts + ) internal { + require(_tokenIds.length == _amounts.length, "unequal lengths"); + + for (uint256 i = 0; i < _tokenIds.length; i += 1) { + assertTrue(MockERC1155(_token).balanceOf(_owner, _tokenIds[i]) >= _amounts[i]); + } + } + + function assertBalERC20Eq(address _token, address _owner, uint256 _amount) internal { + assertEq(MockERC20(_token).balanceOf(_owner), _amount); + } + + function assertBalERC20Gte(address _token, address _owner, uint256 _amount) internal { + assertTrue(MockERC20(_token).balanceOf(_owner) >= _amount); + } + + function forwarders() public view returns (address[] memory) { + address[] memory _forwarders = new address[](1); + _forwarders[0] = forwarder; + return _forwarders; + } +} diff --git a/src/test/sdk/extension/LazyMint.t.sol b/src/test/sdk/extension/LazyMint.t.sol new file mode 100644 index 000000000..ae152d1e8 --- /dev/null +++ b/src/test/sdk/extension/LazyMint.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { LazyMint } from "contracts/extension/LazyMint.sol"; + +contract MyLazyMint is LazyMint { + bool condition; + + function setCondition(bool _condition) external { + condition = _condition; + } + + function _canLazyMint() internal view override returns (bool) { + return condition; + } +} + +contract ExtensionLazyMint is DSTest, Test { + MyLazyMint internal ext; + event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI); + + function setUp() public { + ext = new MyLazyMint(); + } + + function test_state_lazyMint() public { + ext.setCondition(true); + + string memory uri = "uri_string"; + uint256 batchId = ext.lazyMint(100, uri, ""); + + assertEq(batchId, 100); + assertEq(1, ext.getBaseURICount()); + + batchId = ext.lazyMint(200, uri, ""); + + assertEq(batchId, 300); + assertEq(2, ext.getBaseURICount()); + } + + function test_state_lazyMint_NotAuthorized() public { + vm.expectRevert(abi.encodeWithSelector(LazyMint.LazyMintUnauthorized.selector)); + ext.lazyMint(100, "", ""); + } + + function test_state_lazyMint_ZeroAmount() public { + ext.setCondition(true); + vm.expectRevert(abi.encodeWithSelector(LazyMint.LazyMintInvalidAmount.selector)); + ext.lazyMint(0, "", ""); + } + + function test_event_lazyMint() public { + ext.setCondition(true); + + vm.expectEmit(true, true, true, true); + emit TokensLazyMinted(0, 99, "", ""); + ext.lazyMint(100, "", ""); + } +} diff --git a/src/test/sdk/extension/NFTMetadata.t.sol b/src/test/sdk/extension/NFTMetadata.t.sol new file mode 100644 index 000000000..819921433 --- /dev/null +++ b/src/test/sdk/extension/NFTMetadata.t.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { NFTMetadata } from "contracts/extension/NFTMetadata.sol"; + +contract NFTMetadataHarness is NFTMetadata { + address private authorized; + + constructor() { + authorized = msg.sender; + } + + function _canSetMetadata() internal view override returns (bool) { + if (msg.sender == authorized) return true; + return false; + } + + function _canFreezeMetadata() internal view override returns (bool) { + if (msg.sender == authorized) return true; + return false; + } + + function getTokenURI(uint256 _tokenId) external view returns (string memory) { + return _getTokenURI(_tokenId); + } + + function URIStatus() external view returns (bool) { + return uriFrozen; + } + + function supportsInterface(bytes4 interfaceId) external view override returns (bool) {} +} + +contract ExtensionNFTMetadata is DSTest, Test { + NFTMetadataHarness internal ext; + + function setUp() public { + ext = new NFTMetadataHarness(); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `setTokenURI` + //////////////////////////////////////////////////////////////*/ + + function test_setTokenURI_state() public { + string memory uri = "test"; + ext.setTokenURI(0, uri); + assertEq(ext.getTokenURI(0), uri); + + string memory uri2 = "test2"; + ext.setTokenURI(0, uri2); + assertEq(ext.getTokenURI(0), uri2); + } + + function test_setTokenURI_revert_notAuthorized() public { + vm.startPrank(address(0x1)); + string memory uri = "test"; + vm.expectRevert(abi.encodeWithSelector(NFTMetadata.NFTMetadataUnauthorized.selector)); + ext.setTokenURI(1, uri); + } + + function test_setTokenURI_revert_emptyMetadata() public { + string memory uri = ""; + vm.expectRevert(abi.encodeWithSelector(NFTMetadata.NFTMetadataInvalidUrl.selector)); + ext.setTokenURI(1, uri); + } + + function test_setTokenURI_revert_frozen() public { + ext.freezeMetadata(); + string memory uri = "test"; + vm.expectRevert(abi.encodeWithSelector(NFTMetadata.NFTMetadataFrozen.selector, 2)); + ext.setTokenURI(2, uri); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `freezeMetadata` + //////////////////////////////////////////////////////////////*/ + + function test_freezeMetadata_state() public { + ext.freezeMetadata(); + assertEq(ext.URIStatus(), true); + } + + function test_freezeMetadata_revert_notAuthorized() public { + vm.startPrank(address(0x1)); + vm.expectRevert(abi.encodeWithSelector(NFTMetadata.NFTMetadataUnauthorized.selector)); + ext.freezeMetadata(); + } +} diff --git a/src/test/sdk/extension/Ownable.t.sol b/src/test/sdk/extension/Ownable.t.sol new file mode 100644 index 000000000..ca8edbe64 --- /dev/null +++ b/src/test/sdk/extension/Ownable.t.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Ownable } from "contracts/extension/Ownable.sol"; + +contract MyOwnable is Ownable { + bool condition; + + function setCondition(bool _condition) external { + condition = _condition; + } + + function _canSetOwner() internal view override returns (bool) { + return condition; + } +} + +contract ExtensionOwnableTest is DSTest, Test { + MyOwnable internal ext; + event OwnerUpdated(address indexed prevOwner, address indexed newOwner); + + function setUp() public { + ext = new MyOwnable(); + } + + function test_state_setOwner() public { + ext.setCondition(true); + + address owner = address(0x123); + ext.setOwner(owner); + + address currentOwner = ext.owner(); + assertEq(currentOwner, owner); + } + + function test_revert_setOwner() public { + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorized.selector)); + ext.setOwner(address(0x1234)); + } + + function test_event_setOwner() public { + ext.setCondition(true); + + address owner = address(0x123); + + vm.expectEmit(true, true, true, true); + emit OwnerUpdated(address(0), owner); + + ext.setOwner(owner); + } +} diff --git a/src/test/sdk/extension/Permissions.t.sol b/src/test/sdk/extension/Permissions.t.sol new file mode 100644 index 000000000..0c8bfdfbd --- /dev/null +++ b/src/test/sdk/extension/Permissions.t.sol @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Permissions, Strings } from "contracts/extension/Permissions.sol"; + +contract MyPermissions is Permissions { + constructor() { + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); + } + + function setRoleAdmin(bytes32 role, bytes32 adminRole) external { + _setRoleAdmin(role, adminRole); + } + + function checkModifier() external view onlyRole(DEFAULT_ADMIN_ROLE) {} +} + +contract ExtensionPermissions is DSTest, Test { + MyPermissions internal ext; + event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole); + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender); + + address defaultAdmin; + + function setUp() public { + defaultAdmin = address(0x123); + + vm.prank(defaultAdmin); + ext = new MyPermissions(); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `setRoleAdmin` + //////////////////////////////////////////////////////////////*/ + + function test_state_setRoleAdmin() public { + bytes32 role1 = "ROLE_1"; + bytes32 role2 = "ROLE_2"; + + bytes32 adminRole1 = "ADMIN_ROLE_1"; + bytes32 currentDefaultAdmin = ext.DEFAULT_ADMIN_ROLE(); + + ext.setRoleAdmin(role1, adminRole1); + + assertEq(adminRole1, ext.getRoleAdmin(role1)); + assertEq(currentDefaultAdmin, ext.getRoleAdmin(role2)); + } + + function test_event_roleAdminChanged() public { + bytes32 role1 = keccak256("ROLE_1"); + bytes32 adminRole1 = keccak256("ADMIN_ROLE_1"); + + bytes32 previousAdmin = ext.getRoleAdmin(role1); + + vm.expectEmit(true, true, true, true); + emit RoleAdminChanged(role1, previousAdmin, adminRole1); + ext.setRoleAdmin(role1, adminRole1); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `grantRole` + //////////////////////////////////////////////////////////////*/ + + function test_state_grantRole() public { + bytes32 role1 = "ROLE_1"; + bytes32 role2 = "ROLE_2"; + + bytes32 adminRole1 = "ADMIN_ROLE_1"; + address adminOne = address(0x1); + + ext.setRoleAdmin(role1, adminRole1); + + vm.prank(defaultAdmin); + ext.grantRole(adminRole1, adminOne); + + vm.prank(adminOne); + ext.grantRole(role1, address(0x567)); + + vm.prank(defaultAdmin); + ext.grantRole(role2, address(0x567)); + + assertTrue(ext.hasRole(role1, address(0x567))); + assertTrue(ext.hasRole(role2, address(0x567))); + } + + function test_revert_grantRole_missingRole() public { + address caller = address(0x345); + + vm.startPrank(caller); + vm.expectRevert( + abi.encodeWithSelector( + Permissions.PermissionsUnauthorizedAccount.selector, + caller, + ext.DEFAULT_ADMIN_ROLE() + ) + ); + ext.grantRole(keccak256("role"), address(0x1)); + } + + function test_revert_grantRole_grantToHolder() public { + vm.startPrank(defaultAdmin); + ext.grantRole(keccak256("role"), address(0x1)); + + vm.expectRevert( + abi.encodeWithSelector(Permissions.PermissionsAlreadyGranted.selector, address(0x1), keccak256("role")) + ); + ext.grantRole(keccak256("role"), address(0x1)); + } + + function test_event_grantRole() public { + vm.startPrank(defaultAdmin); + + vm.expectEmit(true, true, true, true); + emit RoleGranted(keccak256("role"), address(0x1), defaultAdmin); + ext.grantRole(keccak256("role"), address(0x1)); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `revokeRole` + //////////////////////////////////////////////////////////////*/ + + function test_state_revokeRole() public { + vm.startPrank(defaultAdmin); + + ext.grantRole(keccak256("role"), address(0x567)); + assertTrue(ext.hasRole(keccak256("role"), address(0x567))); + + ext.revokeRole(keccak256("role"), address(0x567)); + assertFalse(ext.hasRole(keccak256("role"), address(0x567))); + } + + function test_revert_revokeRole_missingRole() public { + vm.prank(defaultAdmin); + ext.grantRole(keccak256("role"), address(0x567)); + assertTrue(ext.hasRole(keccak256("role"), address(0x567))); + + vm.startPrank(address(0x345)); + vm.expectRevert( + abi.encodeWithSelector( + Permissions.PermissionsUnauthorizedAccount.selector, + address(0x345), + ext.DEFAULT_ADMIN_ROLE() + ) + ); + ext.revokeRole(keccak256("role"), address(0x567)); + vm.stopPrank(); + + vm.startPrank(defaultAdmin); + vm.expectRevert( + abi.encodeWithSelector( + Permissions.PermissionsUnauthorizedAccount.selector, + address(0x789), + keccak256("role") + ) + ); + ext.revokeRole(keccak256("role"), address(0x789)); + vm.stopPrank(); + } + + function test_event_revokeRole() public { + vm.startPrank(defaultAdmin); + + ext.grantRole(keccak256("role"), address(0x1)); + + vm.expectEmit(true, true, true, true); + emit RoleRevoked(keccak256("role"), address(0x1), defaultAdmin); + ext.revokeRole(keccak256("role"), address(0x1)); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `renounceRole` + //////////////////////////////////////////////////////////////*/ + + function test_state_renounceRole() public { + vm.prank(defaultAdmin); + ext.grantRole(keccak256("role"), address(0x567)); + assertTrue(ext.hasRole(keccak256("role"), address(0x567))); + + vm.prank(address(0x567)); + ext.renounceRole(keccak256("role"), address(0x567)); + + assertFalse(ext.hasRole(keccak256("role"), address(0x567))); + } + + function test_revert_renounceRole_missingRole() public { + vm.startPrank(defaultAdmin); + vm.expectRevert( + abi.encodeWithSelector(Permissions.PermissionsUnauthorizedAccount.selector, defaultAdmin, keccak256("role")) + ); + ext.renounceRole(keccak256("role"), defaultAdmin); + vm.stopPrank(); + } + + function test_revert_renounceRole_renounceForOthers() public { + vm.startPrank(defaultAdmin); + ext.grantRole(keccak256("role"), address(0x567)); + assertTrue(ext.hasRole(keccak256("role"), address(0x567))); + + vm.expectRevert( + abi.encodeWithSelector(Permissions.PermissionsInvalidPermission.selector, defaultAdmin, address(0x567)) + ); + ext.renounceRole(keccak256("role"), address(0x567)); + vm.stopPrank(); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `onlyRole` modifier + //////////////////////////////////////////////////////////////*/ + + function test_modifier_onlyRole() public { + vm.startPrank(address(0x345)); + vm.expectRevert( + abi.encodeWithSelector( + Permissions.PermissionsUnauthorizedAccount.selector, + address(0x345), + ext.DEFAULT_ADMIN_ROLE() + ) + ); + ext.checkModifier(); + } +} diff --git a/src/test/sdk/extension/PermissionsEnumerable.t.sol b/src/test/sdk/extension/PermissionsEnumerable.t.sol new file mode 100644 index 000000000..5a20187d4 --- /dev/null +++ b/src/test/sdk/extension/PermissionsEnumerable.t.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { PermissionsEnumerable, Strings } from "contracts/extension/PermissionsEnumerable.sol"; + +contract MyPermissionsEnumerable is PermissionsEnumerable { + constructor() { + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); + } +} + +contract ExtensionPermissionsEnumerable is DSTest, Test { + MyPermissionsEnumerable internal ext; + + address defaultAdmin; + + function setUp() public { + defaultAdmin = address(0x123); + + vm.prank(defaultAdmin); + ext = new MyPermissionsEnumerable(); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `grantRole` + //////////////////////////////////////////////////////////////*/ + + function test_state_grantRole() public { + bytes32 role1 = keccak256("ROLE_1"); + + address[] memory members = new address[](3); + + members[0] = address(0); + members[1] = address(1); + members[2] = address(2); + + vm.startPrank(defaultAdmin); + + ext.grantRole(role1, members[0]); + assertEq(1, ext.getRoleMemberCount(role1)); + + ext.grantRole(role1, members[1]); + assertEq(2, ext.getRoleMemberCount(role1)); + + ext.grantRole(role1, members[2]); + assertEq(3, ext.getRoleMemberCount(role1)); + + for (uint256 i = 0; i < members.length; i++) { + assertEq(members[i], ext.getRoleMember(role1, i)); + } + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `revokeRole` + //////////////////////////////////////////////////////////////*/ + + function test_state_revokeRole() public { + bytes32 role1 = keccak256("ROLE_1"); + + address[] memory members = new address[](3); + + members[0] = address(0); + members[1] = address(1); + members[2] = address(2); + + vm.startPrank(defaultAdmin); + + ext.grantRole(role1, members[0]); + assertEq(1, ext.getRoleMemberCount(role1)); + + ext.grantRole(role1, members[1]); + assertEq(2, ext.getRoleMemberCount(role1)); + + ext.grantRole(role1, members[2]); + assertEq(3, ext.getRoleMemberCount(role1)); + + for (uint256 i = 0; i < members.length; i++) { + assertEq(members[i], ext.getRoleMember(role1, i)); + } + + // revoke roles, and check updated list of members + ext.revokeRole(role1, members[1]); + assertEq(2, ext.getRoleMemberCount(role1)); + assertEq(members[2], ext.getRoleMember(role1, 1)); + + ext.revokeRole(role1, members[0]); + assertEq(1, ext.getRoleMemberCount(role1)); + assertEq(members[2], ext.getRoleMember(role1, 0)); + + // re-grant roles, and check updated list of members + ext.grantRole(role1, members[0]); + assertEq(2, ext.getRoleMemberCount(role1)); + assertEq(members[2], ext.getRoleMember(role1, 0)); + assertEq(members[0], ext.getRoleMember(role1, 1)); + + ext.grantRole(role1, members[1]); + assertEq(3, ext.getRoleMemberCount(role1)); + assertEq(members[2], ext.getRoleMember(role1, 0)); + assertEq(members[0], ext.getRoleMember(role1, 1)); + assertEq(members[1], ext.getRoleMember(role1, 2)); + } +} diff --git a/src/test/sdk/extension/PlatformFee.t.sol b/src/test/sdk/extension/PlatformFee.t.sol new file mode 100644 index 000000000..9b5fd0128 --- /dev/null +++ b/src/test/sdk/extension/PlatformFee.t.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { PlatformFee } from "contracts/extension/PlatformFee.sol"; + +contract MyPlatformFee is PlatformFee { + bool condition; + + function setCondition(bool _condition) external { + condition = _condition; + } + + function _canSetPlatformFeeInfo() internal view override returns (bool) { + return condition; + } +} + +contract ExtensionPlatformFee is DSTest, Test { + MyPlatformFee internal ext; + event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps); + + function setUp() public { + ext = new MyPlatformFee(); + } + + function test_state_setPlatformFeeInfo() public { + ext.setCondition(true); + + address _platformFeeRecipient = address(0x123); + uint256 _platformFeeBps = 1000; + ext.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + + (address recipient, uint16 bps) = ext.getPlatformFeeInfo(); + assertEq(_platformFeeRecipient, recipient); + assertEq(_platformFeeBps, bps); + } + + function test_revert_setPlatformFeeInfo_ExceedsMaxBps() public { + ext.setCondition(true); + + address _platformFeeRecipient = address(0x123); + uint256 _platformFeeBps = 10001; + + vm.expectRevert( + abi.encodeWithSelector(PlatformFee.PlatformFeeExceededMaxFeeBps.selector, 10_000, _platformFeeBps) + ); + ext.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } + + function test_revert_setPlatformFeeInfo_NotAuthorized() public { + vm.expectRevert(abi.encodeWithSelector(PlatformFee.PlatformFeeUnauthorized.selector)); + ext.setPlatformFeeInfo(address(1), 1000); + } + + function test_event_platformFeeInfo() public { + ext.setCondition(true); + + address _platformFeeRecipient = address(0x123); + uint256 _platformFeeBps = 1000; + + vm.expectEmit(true, true, true, true); + emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); + + ext.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } +} diff --git a/src/test/sdk/extension/PrimarySale.t.sol b/src/test/sdk/extension/PrimarySale.t.sol new file mode 100644 index 000000000..acb30f37e --- /dev/null +++ b/src/test/sdk/extension/PrimarySale.t.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { PrimarySale } from "contracts/extension/PrimarySale.sol"; + +contract MyPrimarySale is PrimarySale { + bool condition; + + function setCondition(bool _condition) external { + condition = _condition; + } + + function _canSetPrimarySaleRecipient() internal view override returns (bool) { + return condition; + } +} + +contract ExtensionPrimarySale is DSTest, Test { + MyPrimarySale internal ext; + event PrimarySaleRecipientUpdated(address indexed recipient); + + function setUp() public { + ext = new MyPrimarySale(); + } + + function test_state_setPrimarySaleRecipient() public { + ext.setCondition(true); + + address _primarySaleRecipient = address(0x123); + ext.setPrimarySaleRecipient(_primarySaleRecipient); + + address recipient = ext.primarySaleRecipient(); + assertEq(recipient, _primarySaleRecipient); + } + + function test_revert_setPrimarySaleRecipient_NotAuthorized() public { + address _primarySaleRecipient = address(0x123); + + vm.expectRevert(abi.encodeWithSelector(PrimarySale.PrimarySaleUnauthorized.selector)); + ext.setPrimarySaleRecipient(_primarySaleRecipient); + } + + function test_event_setPrimarySaleRecipient() public { + ext.setCondition(true); + + address _primarySaleRecipient = address(0x123); + + vm.expectEmit(true, true, true, true); + emit PrimarySaleRecipientUpdated(_primarySaleRecipient); + + ext.setPrimarySaleRecipient(_primarySaleRecipient); + } +} diff --git a/src/test/sdk/extension/Royalty.t.sol b/src/test/sdk/extension/Royalty.t.sol new file mode 100644 index 000000000..1c3a888a7 --- /dev/null +++ b/src/test/sdk/extension/Royalty.t.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Royalty } from "contracts/extension/Royalty.sol"; + +contract MyRoyalty is Royalty { + bool condition; + + function setCondition(bool _condition) external { + condition = _condition; + } + + function _canSetRoyaltyInfo() internal view override returns (bool) { + return condition; + } + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == 0x01ffc9a7; + } +} + +contract ExtensionRoyaltyTest is DSTest, Test { + MyRoyalty internal ext; + event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps); + event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, uint256 royaltyBps); + + function setUp() public { + ext = new MyRoyalty(); + } + + function test_state_setDefaultRoyaltyInfo() public { + ext.setCondition(true); + + address _royaltyRecipient = address(0x123); + uint256 _royaltyBps = 1000; + ext.setDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + + (address royaltyRecipient, uint256 royaltyBps) = ext.getDefaultRoyaltyInfo(); + assertEq(royaltyRecipient, _royaltyRecipient); + assertEq(royaltyBps, _royaltyBps); + + (address receiver, uint256 royaltyAmount) = ext.royaltyInfo(0, 100); + assertEq(receiver, _royaltyRecipient); + assertEq(royaltyAmount, (100 * 1000) / 10_000); + } + + function test_revert_setDefaultRoyaltyInfo_ExceedsMaxBps() public { + ext.setCondition(true); + + address _royaltyRecipient = address(0x123); + uint256 _royaltyBps = 10001; + + vm.expectRevert(abi.encodeWithSelector(Royalty.RoyaltyExceededMaxFeeBps.selector, 10_000, _royaltyBps)); + ext.setDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + } + + function test_state_setRoyaltyInfoForToken() public { + ext.setCondition(true); + + uint256 _tokenId = 1; + address _recipient = address(0x123); + uint256 _bps = 1000; + ext.setRoyaltyInfoForToken(_tokenId, _recipient, _bps); + + (address receiver, uint256 royaltyAmount) = ext.royaltyInfo(_tokenId, 100); + assertEq(receiver, _recipient); + assertEq(royaltyAmount, (100 * 1000) / 10_000); + } + + function test_revert_setRoyaltyInfo_NotAuthorized() public { + vm.expectRevert(abi.encodeWithSelector(Royalty.RoyaltyUnauthorized.selector)); + ext.setRoyaltyInfoForToken(0, address(1), 1000); + } + + function test_revert_setRoyaltyInfoForToken_ExceedsMaxBps() public { + ext.setCondition(true); + + uint256 _tokenId = 1; + address _recipient = address(0x123); + uint256 _bps = 10001; + + vm.expectRevert(abi.encodeWithSelector(Royalty.RoyaltyExceededMaxFeeBps.selector, 10_000, _bps)); + ext.setRoyaltyInfoForToken(_tokenId, _recipient, _bps); + } + + function test_event_defaultRoyalty() public { + ext.setCondition(true); + + address _royaltyRecipient = address(0x123); + uint256 _royaltyBps = 1000; + + vm.expectEmit(true, true, true, true); + emit DefaultRoyalty(_royaltyRecipient, _royaltyBps); + + ext.setDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + } + + function test_event_royaltyForToken() public { + ext.setCondition(true); + + uint256 _tokenId = 1; + address _recipient = address(0x123); + uint256 _bps = 1000; + + vm.expectEmit(true, true, true, true); + emit RoyaltyForToken(_tokenId, _recipient, _bps); + + ext.setRoyaltyInfoForToken(_tokenId, _recipient, _bps); + } +} diff --git a/src/test/sdk/extension/SignatureMintERC1155.t.sol b/src/test/sdk/extension/SignatureMintERC1155.t.sol new file mode 100644 index 000000000..9de73c188 --- /dev/null +++ b/src/test/sdk/extension/SignatureMintERC1155.t.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { SignatureMintERC1155 } from "contracts/extension/SignatureMintERC1155.sol"; + +contract MySigMint1155 is SignatureMintERC1155 { + bool condition; + + function setCondition(bool _condition) external { + condition = _condition; + } + + function _canSignMintRequest(address) internal view override returns (bool) { + return condition; + } + + function mintWithSignature( + MintRequest calldata req, + bytes calldata signature + ) external payable returns (address signer) { + if (!_canSignMintRequest(msg.sender)) { + revert("not authorized"); + } + + signer = _processRequest(req, signature); + } +} + +contract ExtensionSignatureMintERC1155 is DSTest, Test { + MySigMint1155 internal ext; + + uint256 public privateKey = 1234; + address public signer; + + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + MySigMint1155.MintRequest _mintrequest; + bytes _signature; + + function setUp() public { + ext = new MySigMint1155(); + + signer = vm.addr(privateKey); + + typehashMintRequest = keccak256( + "MintRequest(address to,address royaltyRecipient,uint256 royaltyBps,address primarySaleRecipient,uint256 tokenId,string uri,uint256 quantity,uint256 pricePerToken,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + nameHash = keccak256(bytes("SignatureMintERC1155")); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256(abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(ext))); + + _mintrequest.to = address(1); + _mintrequest.royaltyRecipient = address(2); + _mintrequest.royaltyBps = 0; + _mintrequest.primarySaleRecipient = address(2); + _mintrequest.tokenId = 0; + _mintrequest.uri = "ipfs://"; + _mintrequest.quantity = 1; + _mintrequest.pricePerToken = 1; + _mintrequest.currency = address(0x111); + _mintrequest.validityStartTimestamp = 1000; + _mintrequest.validityEndTimestamp = 2000; + _mintrequest.uid = bytes32(0); + + _signature = signMintRequest(_mintrequest, privateKey); + } + + function signMintRequest( + MySigMint1155.MintRequest memory _request, + uint256 _privateKey + ) internal view returns (bytes memory) { + bytes memory encodedRequest = bytes.concat( + abi.encode( + typehashMintRequest, + _request.to, + _request.royaltyRecipient, + _request.royaltyBps, + _request.primarySaleRecipient, + _request.tokenId, + keccak256(bytes(_request.uri)) + ), + abi.encode( + _request.quantity, + _request.pricePerToken, + _request.currency, + _request.validityStartTimestamp, + _request.validityEndTimestamp, + _request.uid + ) + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, typedDataHash); + bytes memory sig = abi.encodePacked(r, s, v); + + return sig; + } + + function test_state_mintWithSignature() public { + vm.warp(1000); + ext.setCondition(true); + vm.prank(signer); + address recoveredSigner = ext.mintWithSignature(_mintrequest, _signature); + + assertEq(signer, recoveredSigner); + } + + function test_revert_mintWithSignature_NotAuthorized() public { + vm.expectRevert("not authorized"); + ext.mintWithSignature(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_InvalidReq() public { + vm.warp(1000); + ext.setCondition(true); + + vm.prank(signer); + ext.mintWithSignature(_mintrequest, _signature); + + vm.expectRevert("Invalid request"); + ext.mintWithSignature(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_RequestExpired() public { + vm.warp(3000); + ext.setCondition(true); + + vm.prank(signer); + vm.expectRevert("Request expired"); + ext.mintWithSignature(_mintrequest, _signature); + } +} diff --git a/src/test/sdk/extension/SignatureMintERC20.t.sol b/src/test/sdk/extension/SignatureMintERC20.t.sol new file mode 100644 index 000000000..452c28a29 --- /dev/null +++ b/src/test/sdk/extension/SignatureMintERC20.t.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { SignatureMintERC20 } from "contracts/extension/SignatureMintERC20.sol"; + +contract MySigMint20 is SignatureMintERC20 { + bool condition; + + function setCondition(bool _condition) external { + condition = _condition; + } + + function _canSignMintRequest(address) internal view override returns (bool) { + return condition; + } + + function mintWithSignature( + MintRequest calldata req, + bytes calldata signature + ) external payable returns (address signer) { + if (!_canSignMintRequest(msg.sender)) { + revert("not authorized"); + } + + signer = _processRequest(req, signature); + } +} + +contract ExtensionSignatureMintERC20 is DSTest, Test { + MySigMint20 internal ext; + + uint256 public privateKey = 1234; + address public signer; + + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + MySigMint20.MintRequest _mintrequest; + bytes _signature; + + function setUp() public { + ext = new MySigMint20(); + + signer = vm.addr(privateKey); + + typehashMintRequest = keccak256( + "MintRequest(address to,address primarySaleRecipient,uint256 quantity,uint256 price,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + nameHash = keccak256(bytes("SignatureMintERC20")); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256(abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(ext))); + + _mintrequest.to = address(1); + _mintrequest.primarySaleRecipient = address(2); + _mintrequest.quantity = 1; + _mintrequest.price = 1; + _mintrequest.currency = address(0x111); + _mintrequest.validityStartTimestamp = 1000; + _mintrequest.validityEndTimestamp = 2000; + _mintrequest.uid = bytes32(0); + + _signature = signMintRequest(_mintrequest, privateKey); + } + + function signMintRequest( + MySigMint20.MintRequest memory _request, + uint256 _privateKey + ) internal view returns (bytes memory) { + bytes memory encodedRequest = abi.encode( + typehashMintRequest, + _request.to, + _request.primarySaleRecipient, + _request.quantity, + _request.price, + _request.currency, + _request.validityStartTimestamp, + _request.validityEndTimestamp, + _request.uid + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, typedDataHash); + bytes memory sig = abi.encodePacked(r, s, v); + + return sig; + } + + function test_state_mintWithSignature() public { + vm.warp(1000); + ext.setCondition(true); + vm.prank(signer); + address recoveredSigner = ext.mintWithSignature(_mintrequest, _signature); + + assertEq(signer, recoveredSigner); + } + + function test_revert_mintWithSignature_NotAuthorized() public { + vm.expectRevert("not authorized"); + ext.mintWithSignature(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_InvalidReq() public { + vm.warp(1000); + ext.setCondition(true); + + vm.prank(signer); + ext.mintWithSignature(_mintrequest, _signature); + + vm.expectRevert("Invalid request"); + ext.mintWithSignature(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_RequestExpired() public { + vm.warp(3000); + ext.setCondition(true); + + vm.prank(signer); + vm.expectRevert("Request expired"); + ext.mintWithSignature(_mintrequest, _signature); + } +} diff --git a/src/test/sdk/extension/SignatureMintERC721.t.sol b/src/test/sdk/extension/SignatureMintERC721.t.sol new file mode 100644 index 000000000..85c0e7db1 --- /dev/null +++ b/src/test/sdk/extension/SignatureMintERC721.t.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { SignatureMintERC721 } from "contracts/extension/SignatureMintERC721.sol"; + +contract MySigMint721 is SignatureMintERC721 { + bool condition; + + function setCondition(bool _condition) external { + condition = _condition; + } + + function _canSignMintRequest(address) internal view override returns (bool) { + return condition; + } + + function mintWithSignature( + MintRequest calldata req, + bytes calldata signature + ) external payable returns (address signer) { + if (!_canSignMintRequest(msg.sender)) { + revert("not authorized"); + } + + signer = _processRequest(req, signature); + } +} + +contract ExtensionSignatureMintERC721 is DSTest, Test { + MySigMint721 internal ext; + + uint256 public privateKey = 1234; + address public signer; + + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + MySigMint721.MintRequest _mintrequest; + bytes _signature; + + function setUp() public { + ext = new MySigMint721(); + + signer = vm.addr(privateKey); + + typehashMintRequest = keccak256( + "MintRequest(address to,address royaltyRecipient,uint256 royaltyBps,address primarySaleRecipient,string uri,uint256 quantity,uint256 pricePerToken,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + nameHash = keccak256(bytes("SignatureMintERC721")); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256(abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(ext))); + + _mintrequest.to = address(1); + _mintrequest.royaltyRecipient = address(2); + _mintrequest.royaltyBps = 0; + _mintrequest.primarySaleRecipient = address(2); + _mintrequest.uri = "ipfs://"; + _mintrequest.quantity = 1; + _mintrequest.pricePerToken = 1; + _mintrequest.currency = address(0x111); + _mintrequest.validityStartTimestamp = 1000; + _mintrequest.validityEndTimestamp = 2000; + _mintrequest.uid = bytes32(0); + + _signature = signMintRequest(_mintrequest, privateKey); + } + + function signMintRequest( + MySigMint721.MintRequest memory _request, + uint256 _privateKey + ) internal view returns (bytes memory) { + bytes memory encodedRequest = abi.encode( + typehashMintRequest, + _request.to, + _request.royaltyRecipient, + _request.royaltyBps, + _request.primarySaleRecipient, + keccak256(bytes(_request.uri)), + _request.quantity, + _request.pricePerToken, + _request.currency, + _request.validityStartTimestamp, + _request.validityEndTimestamp, + _request.uid + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, typedDataHash); + bytes memory sig = abi.encodePacked(r, s, v); + + return sig; + } + + function test_state_mintWithSignature() public { + vm.warp(1000); + ext.setCondition(true); + vm.prank(signer); + address recoveredSigner = ext.mintWithSignature(_mintrequest, _signature); + + assertEq(signer, recoveredSigner); + } + + function test_revert_mintWithSignature_NotAuthorized() public { + vm.expectRevert("not authorized"); + ext.mintWithSignature(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_InvalidReq() public { + vm.warp(1000); + ext.setCondition(true); + + vm.prank(signer); + ext.mintWithSignature(_mintrequest, _signature); + + vm.expectRevert(abi.encodeWithSelector(SignatureMintERC721.SignatureMintInvalidSigner.selector)); + ext.mintWithSignature(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_RequestExpired() public { + vm.warp(3000); + ext.setCondition(true); + + vm.prank(signer); + vm.expectRevert( + abi.encodeWithSelector( + SignatureMintERC721.SignatureMintInvalidTime.selector, + _mintrequest.validityStartTimestamp, + _mintrequest.validityEndTimestamp, + block.timestamp + ) + ); + ext.mintWithSignature(_mintrequest, _signature); + } +} diff --git a/src/test/sdk/extension/StakingExtension.t.sol b/src/test/sdk/extension/StakingExtension.t.sol new file mode 100644 index 000000000..2805ba210 --- /dev/null +++ b/src/test/sdk/extension/StakingExtension.t.sol @@ -0,0 +1,499 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Staking721 } from "contracts/extension/Staking721.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "contracts/eip/interface/IERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; + +import { MockERC721 } from "../../mocks/MockERC721.sol"; + +contract MyStakingContract is ERC20, Staking721, IERC721Receiver { + bool condition; + + constructor( + string memory _name, + string memory _symbol, + address _nftCollection, + uint256 _timeUnit, + uint256 _rewardsPerUnitTime + ) ERC20(_name, _symbol) Staking721(_nftCollection) { + condition = true; + _setStakingCondition(_timeUnit, _rewardsPerUnitTime); + } + + /// @notice View total rewards available in the staking contract. + function getRewardTokenBalance() external view override returns (uint256) {} + + /*/////////////////////////////////////////////////////////////// + ERC 165 / 721 logic + //////////////////////////////////////////////////////////////*/ + + function onERC721Received(address, address, uint256, bytes calldata) external view override returns (bytes4) { + require(isStaking == 2, "Direct transfer"); + return this.onERC721Received.selector; + } + + function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { + return interfaceId == type(IERC721Receiver).interfaceId; + } + + function setCondition(bool _condition) external { + condition = _condition; + } + + function _canSetStakeConditions() internal view override returns (bool) { + return condition; + } + + function _mintRewards(address _staker, uint256 _rewards) internal override { + _mint(_staker, _rewards); + } +} + +contract StakingExtensionTest is DSTest, Test { + MyStakingContract internal ext; + MockERC721 public erc721; + + uint256 timeUnit; + uint256 rewardsPerUnitTime; + + address deployer; + address stakerOne; + address stakerTwo; + + function setUp() public { + erc721 = new MockERC721(); + timeUnit = 1 hours; + rewardsPerUnitTime = 100; + + deployer = address(0x123); + stakerOne = address(0x345); + stakerTwo = address(0x567); + + erc721.mint(stakerOne, 5); // mint token id 0 to 4 + erc721.mint(stakerTwo, 5); // mint token id 5 to 9 + + vm.prank(deployer); + ext = new MyStakingContract("Test Staking Contract", "TSC", address(erc721), timeUnit, rewardsPerUnitTime); + + // set approvals + vm.prank(stakerOne); + erc721.setApprovalForAll(address(ext), true); + + vm.prank(stakerTwo); + erc721.setApprovalForAll(address(ext), true); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: Stake + //////////////////////////////////////////////////////////////*/ + + function test_state_stake() public { + //================ first staker ====================== + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](3); + _tokenIdsOne[0] = 0; + _tokenIdsOne[1] = 1; + _tokenIdsOne[2] = 2; + + // stake 3 tokens + vm.prank(stakerOne); + ext.stake(_tokenIdsOne); + uint256 timeOfLastUpdate_one = block.timestamp; + + // check balances/ownership of staked tokens + for (uint256 i = 0; i < _tokenIdsOne.length; i++) { + assertEq(erc721.ownerOf(_tokenIdsOne[i]), address(ext)); + assertEq(ext.stakerAddress(_tokenIdsOne[i]), stakerOne); + } + assertEq(erc721.balanceOf(stakerOne), 2); + assertEq(erc721.balanceOf(address(ext)), _tokenIdsOne.length); + + // check available rewards right after staking + (uint256[] memory _amountStaked, uint256 _availableRewards) = ext.getStakeInfo(stakerOne); + + assertEq(_amountStaked.length, _tokenIdsOne.length); + assertEq(_availableRewards, 0); + + //=================== warp timestamp to calculate rewards + vm.roll(100); + vm.warp(1000); + + // check available rewards after warp + (, _availableRewards) = ext.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate_one) * _tokenIdsOne.length) * rewardsPerUnitTime) / timeUnit) + ); + + //================ second staker ====================== + vm.roll(200); + vm.warp(2000); + uint256[] memory _tokenIdsTwo = new uint256[](2); + _tokenIdsTwo[0] = 5; + _tokenIdsTwo[1] = 6; + + // stake 2 tokens + vm.prank(stakerTwo); + ext.stake(_tokenIdsTwo); + uint256 timeOfLastUpdate_two = block.timestamp; + + // check balances/ownership of staked tokens + for (uint256 i = 0; i < _tokenIdsTwo.length; i++) { + assertEq(erc721.ownerOf(_tokenIdsTwo[i]), address(ext)); + assertEq(ext.stakerAddress(_tokenIdsTwo[i]), stakerTwo); + } + assertEq(erc721.balanceOf(stakerTwo), 3); + assertEq(erc721.balanceOf(address(ext)), _tokenIdsTwo.length + _tokenIdsOne.length); + + // check available rewards right after staking + (_amountStaked, _availableRewards) = ext.getStakeInfo(stakerTwo); + + assertEq(_amountStaked.length, _tokenIdsTwo.length); + assertEq(_availableRewards, 0); + + //=================== warp timestamp to calculate rewards + vm.roll(300); + vm.warp(3000); + + // check available rewards for stakerOne + (, _availableRewards) = ext.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate_one) * _tokenIdsOne.length) * rewardsPerUnitTime) / timeUnit) + ); + + // check available rewards for stakerTwo + (, _availableRewards) = ext.getStakeInfo(stakerTwo); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate_two) * _tokenIdsTwo.length) * rewardsPerUnitTime) / timeUnit) + ); + } + + function test_revert_stake_stakingZeroTokens() public { + // stake 0 tokens + uint256[] memory _tokenIds; + + vm.prank(stakerOne); + vm.expectRevert("Staking 0 tokens"); + ext.stake(_tokenIds); + } + + function test_revert_stake_notStaker() public { + // stake unowned tokens + uint256[] memory _tokenIds = new uint256[](1); + _tokenIds[0] = 6; + + vm.prank(stakerOne); + vm.expectRevert("ERC721: transfer from incorrect owner"); + ext.stake(_tokenIds); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: claimRewards + //////////////////////////////////////////////////////////////*/ + + function test_state_claimRewards() public { + //================ first staker ====================== + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](3); + _tokenIdsOne[0] = 0; + _tokenIdsOne[1] = 1; + _tokenIdsOne[2] = 2; + + // stake 3 tokens + vm.prank(stakerOne); + ext.stake(_tokenIdsOne); + uint256 timeOfLastUpdate_one = block.timestamp; + + //=================== warp timestamp to claim rewards + vm.roll(100); + vm.warp(1000); + + vm.prank(stakerOne); + ext.claimRewards(); + + // check reward balances + assertEq( + ext.balanceOf(stakerOne), + ((((block.timestamp - timeOfLastUpdate_one) * _tokenIdsOne.length) * rewardsPerUnitTime) / timeUnit) + ); + + // check available rewards after claiming + (uint256[] memory _amountStaked, uint256 _availableRewards) = ext.getStakeInfo(stakerOne); + + assertEq(_amountStaked.length, _tokenIdsOne.length); + assertEq(_availableRewards, 0); + } + + function test_revert_claimRewards_noRewards() public { + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](3); + _tokenIdsOne[0] = 0; + _tokenIdsOne[1] = 1; + _tokenIdsOne[2] = 2; + + // stake 3 tokens + vm.prank(stakerOne); + ext.stake(_tokenIdsOne); + + //=================== try to claim rewards in same block + + vm.prank(stakerOne); + vm.expectRevert("No rewards"); + ext.claimRewards(); + + //======= withdraw tokens and claim rewards + vm.roll(100); + vm.warp(1000); + + vm.prank(stakerOne); + ext.withdraw(_tokenIdsOne); + vm.prank(stakerOne); + ext.claimRewards(); + + //===== try to claim rewards again + vm.roll(200); + vm.warp(2000); + vm.prank(stakerOne); + vm.expectRevert("No rewards"); + ext.claimRewards(); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: stake conditions + //////////////////////////////////////////////////////////////*/ + + function test_state_setRewardsPerUnitTime() public { + // check current value + assertEq(rewardsPerUnitTime, ext.getRewardsPerUnitTime()); + + // set new value and check + uint256 newRewardsPerUnitTime = 50; + ext.setRewardsPerUnitTime(newRewardsPerUnitTime); + assertEq(newRewardsPerUnitTime, ext.getRewardsPerUnitTime()); + + //================ stake tokens + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](3); + _tokenIdsOne[0] = 0; + _tokenIdsOne[1] = 1; + _tokenIdsOne[2] = 2; + + // stake 3 tokens + vm.prank(stakerOne); + ext.stake(_tokenIdsOne); + uint256 timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set rewardsPerUnitTime + vm.roll(100); + vm.warp(1000); + + ext.setRewardsPerUnitTime(200); + assertEq(200, ext.getRewardsPerUnitTime()); + uint256 newTimeOfLastUpdate = block.timestamp; + + // check available rewards -- should use previous value for rewardsPerUnitTime for calculation + (, uint256 _availableRewards) = ext.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * _tokenIdsOne.length) * newRewardsPerUnitTime) / timeUnit) + ); + + //====== check rewards after some time + vm.roll(300); + vm.warp(3000); + + (, uint256 _newRewards) = ext.getStakeInfo(stakerOne); + + assertEq( + _newRewards, + _availableRewards + ((((block.timestamp - newTimeOfLastUpdate) * _tokenIdsOne.length) * 200) / timeUnit) + ); + } + + function test_revert_setRewardsPerUnitTime_notAuthorized() public { + ext.setCondition(false); + + vm.expectRevert("Not authorized"); + ext.setRewardsPerUnitTime(1); + } + + function test_state_setTimeUnit() public { + // check current value + assertEq(timeUnit, ext.getTimeUnit()); + + // set new value and check + uint256 newTimeUnit = 1 minutes; + ext.setTimeUnit(newTimeUnit); + assertEq(newTimeUnit, ext.getTimeUnit()); + + //================ stake tokens + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](3); + _tokenIdsOne[0] = 0; + _tokenIdsOne[1] = 1; + _tokenIdsOne[2] = 2; + + // stake 3 tokens + vm.prank(stakerOne); + ext.stake(_tokenIdsOne); + uint256 timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set rewardsPerUnitTime + vm.roll(100); + vm.warp(1000); + + ext.setTimeUnit(1 seconds); + assertEq(1 seconds, ext.getTimeUnit()); + uint256 newTimeOfLastUpdate = block.timestamp; + + // check available rewards -- should use previous value for rewardsPerUnitTime for calculation + (, uint256 _availableRewards) = ext.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * _tokenIdsOne.length) * rewardsPerUnitTime) / newTimeUnit) + ); + + //====== check rewards after some time + vm.roll(300); + vm.warp(3000); + + (, uint256 _newRewards) = ext.getStakeInfo(stakerOne); + + assertEq( + _newRewards, + _availableRewards + + ((((block.timestamp - newTimeOfLastUpdate) * _tokenIdsOne.length) * rewardsPerUnitTime) / (1 seconds)) + ); + } + + function test_revert_setTimeUnit_notAuthorized() public { + ext.setCondition(false); + + vm.expectRevert("Not authorized"); + ext.setTimeUnit(1); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: withdraw + //////////////////////////////////////////////////////////////*/ + + function test_state_withdraw() public { + //================ first staker ====================== + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](3); + _tokenIdsOne[0] = 0; + _tokenIdsOne[1] = 1; + _tokenIdsOne[2] = 2; + + // stake 3 tokens + vm.prank(stakerOne); + ext.stake(_tokenIdsOne); + uint256 timeOfLastUpdate = block.timestamp; + + // check balances/ownership of staked tokens + for (uint256 i = 0; i < _tokenIdsOne.length; i++) { + assertEq(erc721.ownerOf(_tokenIdsOne[i]), address(ext)); + assertEq(ext.stakerAddress(_tokenIdsOne[i]), stakerOne); + } + assertEq(erc721.balanceOf(stakerOne), 2); + assertEq(erc721.balanceOf(address(ext)), _tokenIdsOne.length); + + // check available rewards right after staking + (uint256[] memory _amountStaked, uint256 _availableRewards) = ext.getStakeInfo(stakerOne); + + assertEq(_amountStaked.length, _tokenIdsOne.length); + assertEq(_availableRewards, 0); + + //========== warp timestamp before withdraw + vm.roll(100); + vm.warp(1000); + + uint256[] memory _tokensToWithdraw = new uint256[](2); + _tokensToWithdraw[0] = 2; + _tokensToWithdraw[1] = 0; + + vm.prank(stakerOne); + ext.withdraw(_tokensToWithdraw); + + // check balances/ownership after withdraw + for (uint256 i = 0; i < _tokensToWithdraw.length; i++) { + assertEq(erc721.ownerOf(_tokensToWithdraw[i]), stakerOne); + assertEq(ext.stakerAddress(_tokensToWithdraw[i]), address(0)); + } + assertEq(erc721.balanceOf(stakerOne), 4); + assertEq(erc721.balanceOf(address(ext)), 1); + + // check available rewards after withdraw + (, _availableRewards) = ext.getStakeInfo(stakerOne); + assertEq(_availableRewards, ((((block.timestamp - timeOfLastUpdate) * 3) * rewardsPerUnitTime) / timeUnit)); + + uint256 timeOfLastUpdateLatest = block.timestamp; + + // check available rewards some time after withdraw + vm.roll(200); + vm.warp(2000); + + (, _availableRewards) = ext.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + (((((timeOfLastUpdateLatest - timeOfLastUpdate) * 3)) * rewardsPerUnitTime) / timeUnit) + + (((((block.timestamp - timeOfLastUpdateLatest) * 1)) * rewardsPerUnitTime) / timeUnit) + ); + } + + function test_revert_withdraw_withdrawingZeroTokens() public { + uint256[] memory _tokensToWithdraw; + + vm.expectRevert("Withdrawing 0 tokens"); + ext.withdraw(_tokensToWithdraw); + } + + function test_revert_withdraw_notStaker() public { + // stake tokens + uint256[] memory _tokenIds = new uint256[](2); + _tokenIds[0] = 0; + _tokenIds[1] = 1; + + vm.prank(stakerOne); + ext.stake(_tokenIds); + + // trying to withdraw zero tokens + uint256[] memory _tokensToWithdraw = new uint256[](1); + _tokensToWithdraw[0] = 2; + + vm.prank(stakerOne); + vm.expectRevert("Not staker"); + ext.withdraw(_tokensToWithdraw); + } + + function test_revert_withdraw_withdrawingMoreThanStaked() public { + // stake tokens + uint256[] memory _tokenIds = new uint256[](1); + _tokenIds[0] = 0; + + vm.prank(stakerOne); + ext.stake(_tokenIds); + + // trying to withdraw tokens not staked by caller + uint256[] memory _tokensToWithdraw = new uint256[](2); + _tokensToWithdraw[0] = 0; + _tokensToWithdraw[1] = 1; + + vm.prank(stakerOne); + vm.expectRevert("Withdrawing more than staked"); + ext.withdraw(_tokensToWithdraw); + } +} diff --git a/src/test/sdk/extension/TokenBundle.t.sol b/src/test/sdk/extension/TokenBundle.t.sol new file mode 100644 index 000000000..a357f1b35 --- /dev/null +++ b/src/test/sdk/extension/TokenBundle.t.sol @@ -0,0 +1,322 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import "../../mocks/WETH9.sol"; +import "../../mocks/MockERC20.sol"; +import "../../mocks/MockERC721.sol"; +import "../../mocks/MockERC1155.sol"; + +import { TokenBundle, ITokenBundle } from "contracts/extension/TokenBundle.sol"; + +contract MyTokenBundle is TokenBundle { + function createBundle(Token[] calldata _tokensToBind, uint256 _bundleId) external { + _createBundle(_tokensToBind, _bundleId); + } + + function updateBundle(Token[] calldata _tokensToBind, uint256 _bundleId) external { + _updateBundle(_tokensToBind, _bundleId); + } + + function addTokenInBundle(Token memory _tokenToBind, uint256 _bundleId) external { + _addTokenInBundle(_tokenToBind, _bundleId); + } + + function updateTokenInBundle(Token memory _tokenToBind, uint256 _bundleId, uint256 _index) external { + _updateTokenInBundle(_tokenToBind, _bundleId, _index); + } + + function setUriOfBundle(string calldata _uri, uint256 _bundleId) external { + _setUriOfBundle(_uri, _bundleId); + } + + function deleteBundle(uint256 _bundleId) external { + _deleteBundle(_bundleId); + } +} + +contract ExtensionTokenBundle is DSTest, Test { + MyTokenBundle internal ext; + + MockERC20 public erc20; + MockERC721 public erc721; + MockERC1155 public erc1155; + WETH9 public weth; + + ITokenBundle.Token[] internal bundleContent; + + function setUp() public { + ext = new MyTokenBundle(); + + erc20 = new MockERC20(); + erc721 = new MockERC721(); + erc1155 = new MockERC1155(); + weth = new WETH9(); + + bundleContent.push( + ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 10 ether + }) + ); + bundleContent.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 0, + totalAmount: 1 + }) + ); + bundleContent.push( + ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 0, + totalAmount: 100 + }) + ); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `createBundle` + //////////////////////////////////////////////////////////////*/ + + function test_state_createBundle() public { + ext.createBundle(bundleContent, 0); + + uint256 tokenCountOfBundle = ext.getTokenCountOfBundle(0); + assertEq(bundleContent.length, tokenCountOfBundle); + + for (uint256 i = 0; i < tokenCountOfBundle; i += 1) { + ITokenBundle.Token memory tokenOfBundle = ext.getTokenOfBundle(0, i); + assertEq(bundleContent[i].assetContract, tokenOfBundle.assetContract); + assertEq(uint256(bundleContent[i].tokenType), uint256(tokenOfBundle.tokenType)); + assertEq(bundleContent[i].tokenId, tokenOfBundle.tokenId); + assertEq(bundleContent[i].totalAmount, tokenOfBundle.totalAmount); + } + } + + function test_revert_createBundle_emptyBundle() public { + ITokenBundle.Token[] memory emptyBundle; + + vm.expectRevert("!Tokens"); + ext.createBundle(emptyBundle, 0); + } + + function test_revert_createBundle_existingBundleId() public { + ext.createBundle(bundleContent, 0); + + vm.expectRevert("id exists"); + ext.createBundle(bundleContent, 0); + } + + function test_revert_createBundle_tokenTypeMismatch() public { + bundleContent.push( + ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 0, + totalAmount: 0 + }) + ); + + vm.expectRevert("!TokenType"); + ext.createBundle(bundleContent, 0); + + bundleContent.pop(); + bundleContent.push( + ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 0, + totalAmount: 0 + }) + ); + + vm.expectRevert("!TokenType"); + ext.createBundle(bundleContent, 0); + + bundleContent.pop(); + bundleContent.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 0 + }) + ); + + vm.expectRevert("!TokenType"); + ext.createBundle(bundleContent, 0); + + bundleContent.pop(); + bundleContent.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 0, + totalAmount: 0 + }) + ); + + vm.expectRevert("!TokenType"); + ext.createBundle(bundleContent, 0); + + bundleContent.pop(); + bundleContent.push( + ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 0 + }) + ); + + vm.expectRevert("!TokenType"); + ext.createBundle(bundleContent, 0); + + bundleContent.pop(); + bundleContent.push( + ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 0, + totalAmount: 0 + }) + ); + + vm.expectRevert("!TokenType"); + ext.createBundle(bundleContent, 0); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `updateBundle` + //////////////////////////////////////////////////////////////*/ + + function test_state_updateBundle() public { + ext.createBundle(bundleContent, 0); + + bundleContent.push( + ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 1, + totalAmount: 200 + }) + ); + + ext.updateBundle(bundleContent, 0); + + uint256 tokenCountOfBundle = ext.getTokenCountOfBundle(0); + assertEq(bundleContent.length, tokenCountOfBundle); + + for (uint256 i = 0; i < tokenCountOfBundle; i += 1) { + ITokenBundle.Token memory tokenOfBundle = ext.getTokenOfBundle(0, i); + assertEq(bundleContent[i].assetContract, tokenOfBundle.assetContract); + assertEq(uint256(bundleContent[i].tokenType), uint256(tokenOfBundle.tokenType)); + assertEq(bundleContent[i].tokenId, tokenOfBundle.tokenId); + assertEq(bundleContent[i].totalAmount, tokenOfBundle.totalAmount); + } + + bundleContent.pop(); + bundleContent.pop(); + ext.updateBundle(bundleContent, 0); + + tokenCountOfBundle = ext.getTokenCountOfBundle(0); + assertEq(bundleContent.length, tokenCountOfBundle); + + for (uint256 i = 0; i < tokenCountOfBundle; i += 1) { + ITokenBundle.Token memory tokenOfBundle = ext.getTokenOfBundle(0, i); + assertEq(bundleContent[i].assetContract, tokenOfBundle.assetContract); + assertEq(uint256(bundleContent[i].tokenType), uint256(tokenOfBundle.tokenType)); + assertEq(bundleContent[i].tokenId, tokenOfBundle.tokenId); + assertEq(bundleContent[i].totalAmount, tokenOfBundle.totalAmount); + } + } + + function test_revert_updateBundle_emptyBundle() public { + ext.createBundle(bundleContent, 0); + + ITokenBundle.Token[] memory emptyBundle; + vm.expectRevert("!Tokens"); + ext.updateBundle(emptyBundle, 0); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `addTokenInBundle` + //////////////////////////////////////////////////////////////*/ + + function test_state_addTokenInBundle() public { + ext.createBundle(bundleContent, 0); + + ITokenBundle.Token memory newToken = ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 1, + totalAmount: 200 + }); + + ext.addTokenInBundle(newToken, 0); + + uint256 tokenCountOfBundle = ext.getTokenCountOfBundle(0); + assertEq(bundleContent.length + 1, tokenCountOfBundle); + + for (uint256 i = 0; i < tokenCountOfBundle - 1; i += 1) { + ITokenBundle.Token memory tokenOfBundle_ = ext.getTokenOfBundle(0, i); + assertEq(bundleContent[i].assetContract, tokenOfBundle_.assetContract); + assertEq(uint256(bundleContent[i].tokenType), uint256(tokenOfBundle_.tokenType)); + assertEq(bundleContent[i].tokenId, tokenOfBundle_.tokenId); + assertEq(bundleContent[i].totalAmount, tokenOfBundle_.totalAmount); + } + + ITokenBundle.Token memory tokenOfBundle = ext.getTokenOfBundle(0, tokenCountOfBundle - 1); + assertEq(newToken.assetContract, tokenOfBundle.assetContract); + assertEq(uint256(newToken.tokenType), uint256(tokenOfBundle.tokenType)); + assertEq(newToken.tokenId, tokenOfBundle.tokenId); + assertEq(newToken.totalAmount, tokenOfBundle.totalAmount); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `updateTokenInBundle` + //////////////////////////////////////////////////////////////*/ + + function test_state_updateTokenInBundle() public { + ext.createBundle(bundleContent, 0); + + ITokenBundle.Token memory newToken = ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 1, + totalAmount: 200 + }); + + ext.updateTokenInBundle(newToken, 0, 1); + + uint256 tokenCountOfBundle = ext.getTokenCountOfBundle(0); + assertEq(bundleContent.length, tokenCountOfBundle); + + ITokenBundle.Token memory tokenOfBundle = ext.getTokenOfBundle(0, 1); + assertEq(newToken.assetContract, tokenOfBundle.assetContract); + assertEq(uint256(newToken.tokenType), uint256(tokenOfBundle.tokenType)); + assertEq(newToken.tokenId, tokenOfBundle.tokenId); + assertEq(newToken.totalAmount, tokenOfBundle.totalAmount); + } + + function test_revert_updateTokenInBundle_indexDNE() public { + ext.createBundle(bundleContent, 0); + + ITokenBundle.Token memory newToken = ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 1, + totalAmount: 200 + }); + + vm.expectRevert("index DNE"); + ext.updateTokenInBundle(newToken, 0, 3); + } +} diff --git a/src/test/sdk/extension/TokenStore.t.sol b/src/test/sdk/extension/TokenStore.t.sol new file mode 100644 index 000000000..c60531719 --- /dev/null +++ b/src/test/sdk/extension/TokenStore.t.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import "../../mocks/WETH9.sol"; +import "../../mocks/MockERC20.sol"; +import "../../mocks/MockERC721.sol"; +import "../../mocks/MockERC1155.sol"; +import "../../utils/Wallet.sol"; + +import { TokenStore, TokenBundle, ITokenBundle, CurrencyTransferLib } from "contracts/extension/TokenStore.sol"; + +contract MyTokenStore is TokenStore { + constructor(address _nativeTokenWrapper) TokenStore(_nativeTokenWrapper) {} + + receive() external payable {} + + function storeTokens( + address _tokenOwner, + Token[] calldata _tokens, + string calldata _uriForTokens, + uint256 _idForTokens + ) external { + _storeTokens(_tokenOwner, _tokens, _uriForTokens, _idForTokens); + } + + function releaseTokens(address _recipient, uint256 _idForContent) external { + _releaseTokens(_recipient, _idForContent); + } +} + +contract ExtensionTokenStore is DSTest, Test { + MyTokenStore internal ext; + + MockERC20 public erc20; + MockERC721 public erc721; + MockERC1155 public erc1155; + WETH9 public weth; + + ITokenBundle.Token[] internal bundleContent; + + Wallet internal tokenOwner; + + function setUp() public { + ext = new MyTokenStore(CurrencyTransferLib.NATIVE_TOKEN); + + erc20 = new MockERC20(); + erc721 = new MockERC721(); + erc1155 = new MockERC1155(); + weth = new WETH9(); + + tokenOwner = new Wallet(); + + bundleContent.push( + ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 10 ether + }) + ); + bundleContent.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 0, + totalAmount: 1 + }) + ); + bundleContent.push( + ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 0, + totalAmount: 100 + }) + ); + + erc20.mint(address(tokenOwner), 10 ether); + erc721.mint(address(tokenOwner), 1); + erc1155.mint(address(tokenOwner), 0, 100); + + tokenOwner.setAllowanceERC20(address(erc20), address(ext), type(uint256).max); + tokenOwner.setApprovalForAllERC721(address(erc721), address(ext), true); + tokenOwner.setApprovalForAllERC1155(address(erc1155), address(ext), true); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `storeTokens` + //////////////////////////////////////////////////////////////*/ + + function test_balances_storeTokens() public { + assertEq(erc20.balanceOf(address(tokenOwner)), 10 ether); + assertEq(erc20.balanceOf(address(ext)), 0); + + assertEq(erc721.ownerOf(0), address(tokenOwner)); + + assertEq(erc1155.balanceOf(address(tokenOwner), 0), 100); + assertEq(erc1155.balanceOf(address(ext), 0), 0); + + vm.prank(address(tokenOwner)); + ext.storeTokens(address(tokenOwner), bundleContent, "", 0); + + assertEq(erc20.balanceOf(address(tokenOwner)), 0); + assertEq(erc20.balanceOf(address(ext)), 10 ether); + + assertEq(erc721.ownerOf(0), address(ext)); + + assertEq(erc1155.balanceOf(address(tokenOwner), 0), 0); + assertEq(erc1155.balanceOf(address(ext), 0), 100); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `releaseTokens` + //////////////////////////////////////////////////////////////*/ + + function test_balances_releaseTokens() public { + vm.prank(address(tokenOwner)); + ext.storeTokens(address(tokenOwner), bundleContent, "", 0); + + assertEq(erc20.balanceOf(address(tokenOwner)), 0); + assertEq(erc20.balanceOf(address(ext)), 10 ether); + + assertEq(erc721.ownerOf(0), address(ext)); + + assertEq(erc1155.balanceOf(address(tokenOwner), 0), 0); + assertEq(erc1155.balanceOf(address(ext), 0), 100); + + ext.releaseTokens(address(0x345), 0); + + assertEq(erc20.balanceOf(address(0x345)), 10 ether); + assertEq(erc20.balanceOf(address(ext)), 0); + + assertEq(erc721.ownerOf(0), address(0x345)); + + assertEq(erc1155.balanceOf(address(0x345), 0), 100); + assertEq(erc1155.balanceOf(address(ext), 0), 0); + } +} diff --git a/src/test/sdk/extension/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.t.sol b/src/test/sdk/extension/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.t.sol new file mode 100644 index 000000000..63dd48e7f --- /dev/null +++ b/src/test/sdk/extension/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.t.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BatchMintMetadata } from "contracts/extension/BatchMintMetadata.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyBatchMintMetadata is BatchMintMetadata { + function batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) external returns (uint256 nextTokenIdToMint, uint256 batchId) { + (nextTokenIdToMint, batchId) = _batchMintMetadata(_startId, _amountToMint, _baseURIForTokens); + } + + function getBaseURI(uint256 _tokenId) public view returns (string memory) { + return _getBaseURI(_tokenId); + } +} + +contract BatchMintMetadata_BatchMintMetadata is ExtensionUtilTest { + MyBatchMintMetadata internal ext; + uint256 internal startId; + uint256 internal amountToMint; + string internal baseURI; + + function setUp() public override { + super.setUp(); + + ext = new MyBatchMintMetadata(); + startId = 0; + amountToMint = 100; + baseURI = "ipfs://baseURI"; + } + + function test_batchMintMetadata() public { + uint256 prevBaseURICount = ext.getBaseURICount(); + uint256 batchId = startId + amountToMint; + + ext.batchMintMetadata(startId, amountToMint, baseURI); + uint256 newBaseURICount = ext.getBaseURICount(); + assertEq(ext.getBaseURI(amountToMint - 1), baseURI); + assertEq(newBaseURICount, prevBaseURICount + 1); + assertEq(ext.getBatchIdAtIndex(newBaseURICount - 1), batchId); + + vm.expectRevert(abi.encodeWithSelector(BatchMintMetadata.BatchMintInvalidBatchId.selector, newBaseURICount)); + ext.getBatchIdAtIndex(newBaseURICount); + } +} diff --git a/src/test/sdk/extension/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.tree b/src/test/sdk/extension/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.tree new file mode 100644 index 000000000..572dd5203 --- /dev/null +++ b/src/test/sdk/extension/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.tree @@ -0,0 +1,7 @@ +_batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens +) +├── it should store batch id equal to the sum of `_startId` and `_amountToMint` in batchIds array ✅ +├── it should map the new batch id to `_baseURIForTokens` ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.t.sol b/src/test/sdk/extension/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.t.sol new file mode 100644 index 000000000..147820874 --- /dev/null +++ b/src/test/sdk/extension/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.t.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BatchMintMetadata } from "contracts/extension/BatchMintMetadata.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyBatchMintMetadata is BatchMintMetadata { + function batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) external returns (uint256 nextTokenIdToMint, uint256 batchId) { + (nextTokenIdToMint, batchId) = _batchMintMetadata(_startId, _amountToMint, _baseURIForTokens); + } + + function freezeBaseURI(uint256 _batchId) external { + _freezeBaseURI(_batchId); + } +} + +contract BatchMintMetadata_FreezeBaseURI is ExtensionUtilTest { + MyBatchMintMetadata internal ext; + uint256 internal startId; + uint256[] internal batchIds; + uint256 internal indexToFreeze; + + event MetadataFrozen(); + + function setUp() public override { + super.setUp(); + + ext = new MyBatchMintMetadata(); + + startId = 0; + // mint 5 batches + for (uint256 i = 0; i < 5; i++) { + uint256 amount = (i + 1) * 10; + uint256 batchId = startId + amount; + batchIds.push(batchId); + + (startId, ) = ext.batchMintMetadata(startId, amount, "ipfs://"); + assertEq(ext.batchFrozen(batchId), false); + } + + indexToFreeze = 3; + } + + function test_freezeBaseURI_invalidBatch() public { + vm.expectRevert( + abi.encodeWithSelector(BatchMintMetadata.BatchMintInvalidBatchId.selector, batchIds[indexToFreeze] * 10) + ); + ext.freezeBaseURI(batchIds[indexToFreeze] * 10); // non-existent batchId + } + + modifier whenBatchIdValid() { + _; + } + + function test_freezeBaseURI() public whenBatchIdValid { + ext.freezeBaseURI(batchIds[indexToFreeze]); + + assertEq(ext.batchFrozen(batchIds[indexToFreeze]), true); + } + + function test_freezeBaseURI_event() public whenBatchIdValid { + vm.expectEmit(false, false, false, false); + emit MetadataFrozen(); + ext.freezeBaseURI(batchIds[indexToFreeze]); + } +} diff --git a/src/test/sdk/extension/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.tree b/src/test/sdk/extension/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.tree new file mode 100644 index 000000000..4dd87edef --- /dev/null +++ b/src/test/sdk/extension/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.tree @@ -0,0 +1,6 @@ +_freezeBaseURI(uint256 _batchId) +├── when there is no baseURI for given `_batchId` + │ └── it should revert ✅ + └── when there is a baseURI present for given `_batchId` + └── it should freeze the `batchId` by setting `frozen[_batchId]` to `true` ✅ + └── it should emit MetadataFrozen event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/batch-mint-metadata/get-base-uri/_getBaseURI.t.sol b/src/test/sdk/extension/batch-mint-metadata/get-base-uri/_getBaseURI.t.sol new file mode 100644 index 000000000..afa7aa6de --- /dev/null +++ b/src/test/sdk/extension/batch-mint-metadata/get-base-uri/_getBaseURI.t.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BatchMintMetadata } from "contracts/extension/BatchMintMetadata.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyBatchMintMetadata is BatchMintMetadata { + function batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) external returns (uint256 nextTokenIdToMint, uint256 batchId) { + (nextTokenIdToMint, batchId) = _batchMintMetadata(_startId, _amountToMint, _baseURIForTokens); + } + + function getBaseURI(uint256 _tokenId) external view returns (string memory) { + return _getBaseURI(_tokenId); + } +} + +contract BatchMintMetadata_GetBaseURI is ExtensionUtilTest { + MyBatchMintMetadata internal ext; + uint256 internal startId; + uint256[] internal batchIds; + + function setUp() public override { + super.setUp(); + + ext = new MyBatchMintMetadata(); + + startId = 0; + // mint 5 batches + for (uint256 i = 0; i < 5; i++) { + uint256 amount = (i + 1) * 10; + uint256 batchId = startId + amount; + batchIds.push(batchId); + + string memory baseURI = Strings.toString(batchId); + (startId, ) = ext.batchMintMetadata(startId, amount, baseURI); + } + } + + function test_getBaseURI_invalidTokenId() public { + uint256 tokenId = batchIds[4]; // tokenId greater than the last batchId + + vm.expectRevert(abi.encodeWithSelector(BatchMintMetadata.BatchMintInvalidTokenId.selector, tokenId)); + ext.getBaseURI(tokenId); + } + + modifier whenValidTokenId() { + _; + } + + function test_getBaseURI() public whenValidTokenId { + for (uint256 i = 0; i < 5; i++) { + uint256 start = i == 0 ? 0 : batchIds[i - 1]; + for (uint256 j = start; j < batchIds[i]; j++) { + string memory _baseURI = ext.getBaseURI(j); + + assertEq(_baseURI, Strings.toString(batchIds[i])); + } + } + } +} diff --git a/src/test/sdk/extension/batch-mint-metadata/get-base-uri/_getBaseURI.tree b/src/test/sdk/extension/batch-mint-metadata/get-base-uri/_getBaseURI.tree new file mode 100644 index 000000000..c4ee674bf --- /dev/null +++ b/src/test/sdk/extension/batch-mint-metadata/get-base-uri/_getBaseURI.tree @@ -0,0 +1,6 @@ +_getBaseURI(uint256 _tokenId) +├── when `_tokenId` doesn't belong to any batch, i.e. greater than the last batchId + │ └── it should revert ✅ + └── when `_tokenId` belongs to some batch, i.e. less than that batchId + └── it should return correct baseURI for the `_tokenId` ✅ +(note: all batches are assumed to be contiguous, i.e. start id of one batch is the end id of the previous batch) \ No newline at end of file diff --git a/src/test/sdk/extension/batch-mint-metadata/get-batch-id/_getBatchId.t.sol b/src/test/sdk/extension/batch-mint-metadata/get-batch-id/_getBatchId.t.sol new file mode 100644 index 000000000..a4f16687e --- /dev/null +++ b/src/test/sdk/extension/batch-mint-metadata/get-batch-id/_getBatchId.t.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BatchMintMetadata } from "contracts/extension/BatchMintMetadata.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyBatchMintMetadata is BatchMintMetadata { + function batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) external returns (uint256 nextTokenIdToMint, uint256 batchId) { + (nextTokenIdToMint, batchId) = _batchMintMetadata(_startId, _amountToMint, _baseURIForTokens); + } + + function getBatchId(uint256 _tokenId) external view returns (uint256 batchId, uint256 index) { + return _getBatchId(_tokenId); + } +} + +contract BatchMintMetadata_GetBatchId is ExtensionUtilTest { + MyBatchMintMetadata internal ext; + uint256 internal startId; + uint256[] internal batchIds; + + function setUp() public override { + super.setUp(); + + ext = new MyBatchMintMetadata(); + + startId = 0; + // mint 5 batches + for (uint256 i = 0; i < 5; i++) { + uint256 amount = (i + 1) * 10; + batchIds.push(startId + amount); + (startId, ) = ext.batchMintMetadata(startId, amount, "ipfs://"); + } + } + + function test_getBatchId_invalidTokenId() public { + uint256 tokenId = batchIds[4]; // tokenId greater than the last batchId + + vm.expectRevert(abi.encodeWithSelector(BatchMintMetadata.BatchMintInvalidTokenId.selector, tokenId)); + ext.getBatchId(tokenId); + } + + modifier whenValidTokenId() { + _; + } + + function test_getBatchId() public whenValidTokenId { + for (uint256 i = 0; i < 5; i++) { + uint256 start = i == 0 ? 0 : batchIds[i - 1]; + for (uint256 j = start; j < batchIds[i]; j++) { + (uint256 batchId, uint256 index) = ext.getBatchId(j); + + assertEq(batchId, batchIds[i]); + assertEq(index, i); + } + } + } +} diff --git a/src/test/sdk/extension/batch-mint-metadata/get-batch-id/_getBatchId.tree b/src/test/sdk/extension/batch-mint-metadata/get-batch-id/_getBatchId.tree new file mode 100644 index 000000000..2e6dd366e --- /dev/null +++ b/src/test/sdk/extension/batch-mint-metadata/get-batch-id/_getBatchId.tree @@ -0,0 +1,6 @@ +_getBatchId(uint256 _tokenId) +├── when `_tokenId` doesn't belong to any batch, i.e. greater than the last batchId + │ └── it should revert ✅ + └── when `_tokenId` belongs to some batch, i.e. less than that batchId + └── it should return correct batchId and batch index for the `_tokenId` ✅ +(note: all batches are assumed to be contiguous, i.e. start id of one batch is the end id of the previous batch) \ No newline at end of file diff --git a/src/test/sdk/extension/batch-mint-metadata/get-batch-start-id/_getBatchStartId.t.sol b/src/test/sdk/extension/batch-mint-metadata/get-batch-start-id/_getBatchStartId.t.sol new file mode 100644 index 000000000..18d1fc954 --- /dev/null +++ b/src/test/sdk/extension/batch-mint-metadata/get-batch-start-id/_getBatchStartId.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BatchMintMetadata } from "contracts/extension/BatchMintMetadata.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyBatchMintMetadata is BatchMintMetadata { + function batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) external returns (uint256 nextTokenIdToMint, uint256 batchId) { + (nextTokenIdToMint, batchId) = _batchMintMetadata(_startId, _amountToMint, _baseURIForTokens); + } + + function getBatchStartId(uint256 _batchId) external view returns (uint256) { + return _getBatchStartId(_batchId); + } +} + +contract BatchMintMetadata_GetBatchStartId is ExtensionUtilTest { + MyBatchMintMetadata internal ext; + uint256 internal startId; + uint256[] internal batchIds; + + function setUp() public override { + super.setUp(); + + ext = new MyBatchMintMetadata(); + + startId = 0; + // mint 5 batches + for (uint256 i = 0; i < 5; i++) { + uint256 amount = (i + 1) * 10; + batchIds.push(startId + amount); + (startId, ) = ext.batchMintMetadata(startId, amount, "ipfs://"); + } + } + + function test_getBatchStartId_invalidBatchId() public { + uint256 batchId = batchIds[4] + 1; // non-existent batchId + + vm.expectRevert(abi.encodeWithSelector(BatchMintMetadata.BatchMintInvalidBatchId.selector, batchId)); + ext.getBatchStartId(batchId); + } + + modifier whenValidBatchId() { + _; + } + + function test_getBatchStartId() public whenValidBatchId { + for (uint256 i = 0; i < 5; i++) { + uint256 start = i == 0 ? 0 : batchIds[i - 1]; + uint256 _batchStartId = ext.getBatchStartId(batchIds[i]); + + assertEq(start, _batchStartId); + } + } +} diff --git a/src/test/sdk/extension/batch-mint-metadata/get-batch-start-id/_getBatchStartId.tree b/src/test/sdk/extension/batch-mint-metadata/get-batch-start-id/_getBatchStartId.tree new file mode 100644 index 000000000..7e303ab46 --- /dev/null +++ b/src/test/sdk/extension/batch-mint-metadata/get-batch-start-id/_getBatchStartId.tree @@ -0,0 +1,6 @@ +_getBatchStartId(uint256 _batchID) +├── when `_batchID` doesn't exist + │ └── it should revert ✅ + └── when `_batchID` exists + └── it should return the starting tokenId for that batch ✅ +(note: all batches are assumed to be contiguous, i.e. start id of one batch is the end id of the previous batch) \ No newline at end of file diff --git a/src/test/sdk/extension/batch-mint-metadata/set-base-uri/_setBaseURI.t.sol b/src/test/sdk/extension/batch-mint-metadata/set-base-uri/_setBaseURI.t.sol new file mode 100644 index 000000000..1ec55e8a8 --- /dev/null +++ b/src/test/sdk/extension/batch-mint-metadata/set-base-uri/_setBaseURI.t.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BatchMintMetadata } from "contracts/extension/BatchMintMetadata.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyBatchMintMetadata is BatchMintMetadata { + function batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) external returns (uint256 nextTokenIdToMint, uint256 batchId) { + (nextTokenIdToMint, batchId) = _batchMintMetadata(_startId, _amountToMint, _baseURIForTokens); + } + + function setBaseURI(uint256 _batchId, string memory _baseURI) external { + _setBaseURI(_batchId, _baseURI); + } + + function freezeBaseURI(uint256 _batchId, bool _freeze) public { + batchFrozen[_batchId] = _freeze; + } + + function getBaseURI(uint256 _tokenId) external view returns (string memory) { + return _getBaseURI(_tokenId); + } +} + +contract BatchMintMetadata_SetBaseURI is ExtensionUtilTest { + MyBatchMintMetadata internal ext; + string internal newBaseURI; + uint256 internal startId; + uint256[] internal batchIds; + uint256 internal indexToUpdate; + + event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); + + function setUp() public override { + super.setUp(); + + ext = new MyBatchMintMetadata(); + + startId = 0; + // mint 5 batches + for (uint256 i = 0; i < 5; i++) { + uint256 amount = (i + 1) * 10; + uint256 batchId = startId + amount; + batchIds.push(batchId); + + (startId, ) = ext.batchMintMetadata(startId, amount, "ipfs://"); + ext.freezeBaseURI(batchId, true); + } + + indexToUpdate = 3; + newBaseURI = "ipfs://baseURI"; + } + + function test_setBaseURI_frozenBatchId() public { + vm.expectRevert( + abi.encodeWithSelector(BatchMintMetadata.BatchMintMetadataFrozen.selector, batchIds[indexToUpdate]) + ); + ext.setBaseURI(batchIds[indexToUpdate], newBaseURI); + } + + modifier whenBatchIdNotFrozen() { + ext.freezeBaseURI(batchIds[indexToUpdate], false); + _; + } + + function test_setBaseURI() public whenBatchIdNotFrozen { + ext.setBaseURI(batchIds[indexToUpdate], newBaseURI); + + string memory _baseURI = ext.getBaseURI(batchIds[indexToUpdate] - 1); + + assertEq(_baseURI, newBaseURI); + } + + function test_setBaseURI_event() public whenBatchIdNotFrozen { + vm.expectEmit(false, false, false, true); + emit BatchMetadataUpdate(batchIds[indexToUpdate - 1], batchIds[indexToUpdate]); + ext.setBaseURI(batchIds[indexToUpdate], newBaseURI); + } +} diff --git a/src/test/sdk/extension/batch-mint-metadata/set-base-uri/_setBaseURI.tree b/src/test/sdk/extension/batch-mint-metadata/set-base-uri/_setBaseURI.tree new file mode 100644 index 000000000..3df76f653 --- /dev/null +++ b/src/test/sdk/extension/batch-mint-metadata/set-base-uri/_setBaseURI.tree @@ -0,0 +1,6 @@ +_setBaseURI(uint256 _batchId, string memory _baseURI) +├── when the `_batchId` is frozen + │ └── it should revert ✅ + └── when the `_batchId` is not frozen + └── it should map the `_batchId` to `_baseURI` param ✅ + └── it should emit BatchMetadataUpdate event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.t.sol b/src/test/sdk/extension/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.t.sol new file mode 100644 index 000000000..e01939b3b --- /dev/null +++ b/src/test/sdk/extension/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.t.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BurnToClaim, IBurnToClaim } from "contracts/extension/BurnToClaim.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyBurnToClaim is BurnToClaim { + function burnTokensOnOrigin(address _tokenOwner, uint256 _tokenId, uint256 _quantity) public { + _burnTokensOnOrigin(_tokenOwner, _tokenId, _quantity); + } + + function _canSetBurnToClaim() internal view override returns (bool) { + return true; + } +} + +contract BurnToClaim_BurnTokensOnOrigin is ExtensionUtilTest { + MyBurnToClaim internal ext; + Wallet internal tokenOwner; + uint256 internal tokenId; + uint256 internal quantity; + + function setUp() public override { + super.setUp(); + + ext = new MyBurnToClaim(); + + tokenOwner = getWallet(); + erc721.mint(address(tokenOwner), 10); + erc1155.mint(address(tokenOwner), 1, 10); + + erc721NonBurnable.mint(address(tokenOwner), 10); + erc1155NonBurnable.mint(address(tokenOwner), 1, 10); + + tokenOwner.setApprovalForAllERC721(address(erc721), address(ext), true); + tokenOwner.setApprovalForAllERC1155(address(erc1155), address(ext), true); + } + + // ================== + // ======= Test branch: token type is ERC721 + // ================== + + modifier whenNotBurnableERC721() { + ext.setBurnToClaimInfo( + IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc721NonBurnable), + tokenType: IBurnToClaim.TokenType.ERC721, + tokenId: 0, + mintPriceForNewToken: 0, + currency: address(erc20) + }) + ); + _; + } + + function test_burnTokensOnOrigin_ERC721_nonBurnable() public whenNotBurnableERC721 { + vm.expectRevert(); + ext.burnTokensOnOrigin(address(tokenOwner), tokenId, quantity); + } + + modifier whenBurnableERC721() { + ext.setBurnToClaimInfo( + IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc721), + tokenType: IBurnToClaim.TokenType.ERC721, + tokenId: 0, + mintPriceForNewToken: 0, + currency: address(erc20) + }) + ); + _; + } + + function test_burnTokensOnOrigin_ERC721() public whenBurnableERC721 { + ext.burnTokensOnOrigin(address(tokenOwner), tokenId, quantity); + + assertEq(erc721.balanceOf(address(tokenOwner)), 9); + + vm.expectRevert(); + erc721.ownerOf(tokenId); // token doesn't exist after burning + } + + // ================== + // ======= Test branch: token type is ERC71155 + // ================== + + modifier whenNotBurnableERC1155() { + ext.setBurnToClaimInfo( + IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc1155NonBurnable), + tokenType: IBurnToClaim.TokenType.ERC1155, + tokenId: 1, + mintPriceForNewToken: 0, + currency: address(erc20) + }) + ); + _; + } + + function test_burnTokensOnOrigin_ERC1155_nonBurnable() public whenNotBurnableERC1155 { + vm.expectRevert(); + ext.burnTokensOnOrigin(address(tokenOwner), tokenId, quantity); + } + + modifier whenBurnableERC1155() { + ext.setBurnToClaimInfo( + IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc1155), + tokenType: IBurnToClaim.TokenType.ERC1155, + tokenId: 1, + mintPriceForNewToken: 0, + currency: address(erc20) + }) + ); + _; + } + + function test_burnTokensOnOrigin_ERC1155() public whenBurnableERC1155 { + ext.burnTokensOnOrigin(address(tokenOwner), tokenId, quantity); + + assertEq(erc1155.balanceOf(address(tokenOwner), tokenId), 0); + } +} diff --git a/src/test/sdk/extension/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.tree b/src/test/sdk/extension/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.tree new file mode 100644 index 000000000..a2a3911ac --- /dev/null +++ b/src/test/sdk/extension/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.tree @@ -0,0 +1,15 @@ +_burnTokensOnOrigin( + address _tokenOwner, + uint256 _tokenId, + uint256 _quantity +) +├── when burn-to-claim info has token type ERC721 + ├── when the origin ERC721 contract is not burnable + │ └── it should revert ✅ + └── when the origin ERC721 contract is burnable + └── it should successfully burn the token with given tokenId for the token owner ✅ +├── when burn-to-claim info has token type ERC1155 + ├── when the origin ERC1155 contract is not burnable + │ └── it should revert ✅ + └── when the origin ERC1155 contract is burnable + └── it should successfully burn tokens with given tokenId and quantity for the token owner ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.t.sol b/src/test/sdk/extension/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.t.sol new file mode 100644 index 000000000..b4e721145 --- /dev/null +++ b/src/test/sdk/extension/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.t.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BurnToClaim, IBurnToClaim } from "contracts/extension/BurnToClaim.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyBurnToClaim is BurnToClaim { + bool condition; + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function _canSetBurnToClaim() internal view override returns (bool) { + return msg.sender == admin; + } +} + +contract BurnToClaim_SetBurnToClaimInfo is ExtensionUtilTest { + MyBurnToClaim internal ext; + address internal admin; + address internal caller; + IBurnToClaim.BurnToClaimInfo internal info; + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + caller = getActor(1); + + ext = new MyBurnToClaim(address(admin)); + info = IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(0), + tokenType: IBurnToClaim.TokenType.ERC721, + tokenId: 0, + mintPriceForNewToken: 0, + currency: address(0) + }); + } + + function test_setBurnToClaimInfo_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert("Not authorized."); + ext.setBurnToClaimInfo(info); + } + + modifier whenCallerAuthorized() { + caller = admin; + _; + } + + function test_setBurnToClaimInfo_invalidOriginContract_addressZero() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectRevert("Origin contract not set."); + ext.setBurnToClaimInfo(info); + } + + modifier whenValidOriginContract() { + info.originContractAddress = address(erc721); + _; + } + + function test_setBurnToClaimInfo_invalidCurrency_addressZero() public whenCallerAuthorized whenValidOriginContract { + vm.prank(address(caller)); + vm.expectRevert("Currency not set."); + ext.setBurnToClaimInfo(info); + } + + modifier whenValidCurrency() { + info.currency = address(erc20); + _; + } + + function test_setBurnToClaimInfo() public whenCallerAuthorized whenValidOriginContract whenValidCurrency { + vm.prank(address(caller)); + ext.setBurnToClaimInfo(info); + + IBurnToClaim.BurnToClaimInfo memory _info = ext.getBurnToClaimInfo(); + + assertEq(_info.originContractAddress, info.originContractAddress); + assertEq(_info.currency, info.currency); + } +} diff --git a/src/test/sdk/extension/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.tree b/src/test/sdk/extension/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.tree new file mode 100644 index 000000000..d6e347f5e --- /dev/null +++ b/src/test/sdk/extension/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.tree @@ -0,0 +1,11 @@ +setBurnToClaimInfo(BurnToClaimInfo calldata _burnToClaimInfo) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + ├── when input originContractAddress is address(0) + │ └── it should revert ✅ + └── when input originContractAddress is not address(0) + ├── when input currency is address(0) + │ └── it should revert ✅ + └── when input currency is not address(0) + └── it should save incoming struct values into burnToClaimInfo state ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.t.sol b/src/test/sdk/extension/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.t.sol new file mode 100644 index 000000000..b985d473d --- /dev/null +++ b/src/test/sdk/extension/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.t.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BurnToClaim, IBurnToClaim } from "contracts/extension/BurnToClaim.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyBurnToClaim is BurnToClaim { + bool condition; + + function setCondition(bool _condition) external { + condition = _condition; + } + + function _canSetBurnToClaim() internal view override returns (bool) { + return condition; + } +} + +contract BurnToClaim_VerifyBurnToClaim is ExtensionUtilTest { + MyBurnToClaim internal ext; + address internal tokenOwner; + uint256 internal tokenId; + uint256 internal quantity; + + function setUp() public override { + super.setUp(); + + ext = new MyBurnToClaim(); + ext.setCondition(true); + + tokenOwner = getActor(1); + erc721.mint(address(tokenOwner), 10); + erc1155.mint(address(tokenOwner), 1, 10); + } + + function test_verifyBurnToClaim_infoNotSet() public { + vm.expectRevert(); + ext.verifyBurnToClaim(tokenOwner, tokenId, 1); + } + + // ================== + // ======= Test branch: token type is ERC721 + // ================== + + modifier whenBurnToClaimInfoSetERC721() { + ext.setBurnToClaimInfo( + IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc721), + tokenType: IBurnToClaim.TokenType.ERC721, + tokenId: 0, + mintPriceForNewToken: 0, + currency: address(erc20) + }) + ); + IBurnToClaim.BurnToClaimInfo memory info = ext.getBurnToClaimInfo(); + _; + } + + function test_verifyBurnToClaim_ERC721_quantity_not_1() public whenBurnToClaimInfoSetERC721 { + quantity = 10; + vm.expectRevert("Invalid amount"); + ext.verifyBurnToClaim(tokenOwner, tokenId, quantity); + } + + modifier whenQuantityParamisOne() { + quantity = 1; + _; + } + + function test_verifyBurnToClaim_ERC721_notOwnerOfToken() + public + whenBurnToClaimInfoSetERC721 + whenQuantityParamisOne + { + vm.expectRevert("!Owner"); + ext.verifyBurnToClaim(address(0x123), tokenId, quantity); // random address as owner + } + + modifier whenCorrectOwner() { + _; + } + + function test_verifyBurnToClaim_ERC721() + public + whenBurnToClaimInfoSetERC721 + whenQuantityParamisOne + whenCorrectOwner + { + ext.verifyBurnToClaim(tokenOwner, tokenId, quantity); + } + + // ================== + // ======= Test branch: token type is ERC1155 + // ================== + + modifier whenBurnToClaimInfoSetERC1155() { + ext.setBurnToClaimInfo( + IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc1155), + tokenType: IBurnToClaim.TokenType.ERC1155, + tokenId: 1, + mintPriceForNewToken: 0, + currency: address(erc20) + }) + ); + IBurnToClaim.BurnToClaimInfo memory info = ext.getBurnToClaimInfo(); + _; + } + + function test_verifyBurnToClaim_ERC1155_invalidTokenId() public whenBurnToClaimInfoSetERC1155 { + vm.expectRevert("Invalid token Id"); + ext.verifyBurnToClaim(tokenOwner, tokenId, quantity); // the tokenId here is 0, but eligible one is set as 1 above + } + + modifier whenCorrectTokenId() { + tokenId = 1; + _; + } + + function test_verifyBurnToClaim_ERC1155_balanceLessThanQuantity() + public + whenBurnToClaimInfoSetERC1155 + whenCorrectTokenId + { + quantity = 100; + vm.expectRevert("!Balance"); + ext.verifyBurnToClaim(tokenOwner, tokenId, quantity); // available balance is 10 + } + + modifier whenSufficientBalance() { + quantity = 10; + _; + } + + function test_verifyBurnToClaim_ERC1155() + public + whenBurnToClaimInfoSetERC1155 + whenCorrectTokenId + whenSufficientBalance + { + ext.verifyBurnToClaim(tokenOwner, tokenId, quantity); + } +} diff --git a/src/test/sdk/extension/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.tree b/src/test/sdk/extension/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.tree new file mode 100644 index 000000000..ffc4dba9d --- /dev/null +++ b/src/test/sdk/extension/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.tree @@ -0,0 +1,23 @@ +verifyBurnToClaim( + address tokenOwner, + uint256 tokenId, + uint256 quantity +) +├── when burn-to-claim info is not set + │ └── it should revert ✅ + └── when burn-to-claim info is set, with token type ERC721 + │ ├── when quantity param is not 1 + │ │ └── it should revert ✅ + │ └── when quantity param is 1 + │ ├── when token owner param is not the actual token owner + │ │ └── it should revert ✅ + │ └── when token owner param is the correct token owner + │ │ └── execution completes -- exit function ✅ + └── when burn-to-claim info is set, with token type ERC1155 + ├── when tokenId param doesn't match eligible tokenId + │ └── it should revert ✅ + └── when tokenId param matches eligible tokenId + ├── when token owner has balance less than quantity param + │ └── it should revert ✅ + └── when token owner has balance greater than or equal to quantity param + └── execution completes -- exit function ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/contract-metadata/set-contract-uri/setContractURI.t.sol b/src/test/sdk/extension/contract-metadata/set-contract-uri/setContractURI.t.sol new file mode 100644 index 000000000..3395c0850 --- /dev/null +++ b/src/test/sdk/extension/contract-metadata/set-contract-uri/setContractURI.t.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { ContractMetadata, IContractMetadata } from "contracts/extension/ContractMetadata.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyContractMetadata is ContractMetadata { + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function _canSetContractURI() internal view override returns (bool) { + return msg.sender == admin; + } +} + +contract ContractMetadata_SetContractURI is ExtensionUtilTest { + MyContractMetadata internal ext; + address internal admin; + address internal caller; + string internal uri; + + event ContractURIUpdated(string prevURI, string newURI); + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + caller = getActor(1); + uri = "ipfs://newUri"; + + ext = new MyContractMetadata(address(admin)); + } + + function test_setContractURI_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert(abi.encodeWithSelector(ContractMetadata.ContractMetadataUnauthorized.selector)); + ext.setContractURI(uri); + } + + modifier whenCallerAuthorized() { + caller = admin; + _; + } + + function test_setContractURI() public whenCallerAuthorized { + vm.prank(address(caller)); + ext.setContractURI(uri); + + string memory _updatedUri = ext.contractURI(); + assertEq(_updatedUri, uri); + } + + function test_setContractURI_event() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectEmit(false, false, false, true); + emit ContractURIUpdated("", uri); + ext.setContractURI(uri); + } +} diff --git a/src/test/sdk/extension/contract-metadata/set-contract-uri/setContractURI.tree b/src/test/sdk/extension/contract-metadata/set-contract-uri/setContractURI.tree new file mode 100644 index 000000000..e626d76e4 --- /dev/null +++ b/src/test/sdk/extension/contract-metadata/set-contract-uri/setContractURI.tree @@ -0,0 +1,6 @@ +setContractURI(string memory uri) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── it should update contract URI to the new URI value ✅ + └── it should emit ContractURIUpdated event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/delayed-reveal/get-reveal-uri/getRevealURI.t.sol b/src/test/sdk/extension/delayed-reveal/get-reveal-uri/getRevealURI.t.sol new file mode 100644 index 000000000..0e97a5eb4 --- /dev/null +++ b/src/test/sdk/extension/delayed-reveal/get-reveal-uri/getRevealURI.t.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { DelayedReveal, IDelayedReveal } from "contracts/extension/DelayedReveal.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyDelayedReveal is DelayedReveal { + function setEncryptedData(uint256 _batchId, bytes memory _encryptedData) external { + _setEncryptedData(_batchId, _encryptedData); + } + + function reveal(uint256 identifier, bytes calldata key) external returns (string memory revealedURI) {} +} + +contract DelayedReveal_GetRevealURI is ExtensionUtilTest { + MyDelayedReveal internal ext; + string internal originalURI; + bytes internal encryptionKey; + bytes internal encryptedURI; + bytes internal encryptedData; + uint256 internal batchId; + bytes32 internal provenanceHash; + + function setUp() public override { + super.setUp(); + + ext = new MyDelayedReveal(); + originalURI = "ipfs://original"; + encryptionKey = "key123"; + batchId = 1; + + provenanceHash = keccak256(abi.encodePacked(originalURI, encryptionKey, block.chainid)); + encryptedURI = ext.encryptDecrypt(bytes(originalURI), encryptionKey); + encryptedData = abi.encode(encryptedURI, provenanceHash); + } + + function test_getRevealURI_encryptedDataNotSet() public { + vm.expectRevert(abi.encodeWithSelector(DelayedReveal.DelayedRevealNothingToReveal.selector)); + ext.getRevealURI(batchId, encryptionKey); + } + + modifier whenEncryptedDataIsSet() { + ext.setEncryptedData(batchId, encryptedData); + _; + } + + function test_getRevealURI_incorrectKey() public whenEncryptedDataIsSet { + bytes memory incorrectKey = "incorrect key"; + string memory incorrectURI = string(ext.encryptDecrypt(encryptedURI, incorrectKey)); + + vm.expectRevert( + abi.encodeWithSelector( + DelayedReveal.DelayedRevealIncorrectResultHash.selector, + provenanceHash, + keccak256(abi.encodePacked(incorrectURI, incorrectKey, block.chainid)) + ) + ); + ext.getRevealURI(batchId, incorrectKey); + } + + modifier whenCorrectKey() { + _; + } + + function test_getRevealURI() public whenEncryptedDataIsSet whenCorrectKey { + string memory revealedURI = ext.getRevealURI(batchId, encryptionKey); + + assertEq(originalURI, revealedURI); + } +} diff --git a/src/test/sdk/extension/delayed-reveal/get-reveal-uri/getRevealURI.tree b/src/test/sdk/extension/delayed-reveal/get-reveal-uri/getRevealURI.tree new file mode 100644 index 000000000..acb580468 --- /dev/null +++ b/src/test/sdk/extension/delayed-reveal/get-reveal-uri/getRevealURI.tree @@ -0,0 +1,8 @@ +getRevealURI(uint256 _batchId, bytes calldata _key) +├── when there is no encrypted data set for the given batch id + │ └── it should revert ✅ + └── when there is an associated encrypted data present for the given batch id + ├── when the encryption key provided is incorrect + │ └── it should revert ✅ + └── when the encryption key provided is correct + └── it should correctly decrypt and return the original URI ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/delayed-reveal/set-encrypted-data/_setEncryptedData.t.sol b/src/test/sdk/extension/delayed-reveal/set-encrypted-data/_setEncryptedData.t.sol new file mode 100644 index 000000000..096e33568 --- /dev/null +++ b/src/test/sdk/extension/delayed-reveal/set-encrypted-data/_setEncryptedData.t.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { DelayedReveal, IDelayedReveal } from "contracts/extension/DelayedReveal.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyDelayedReveal is DelayedReveal { + function setEncryptedData(uint256 _batchId, bytes memory _encryptedData) external { + _setEncryptedData(_batchId, _encryptedData); + } + + function reveal(uint256 identifier, bytes calldata key) external returns (string memory revealedURI) {} +} + +contract DelayedReveal_SetEncryptedData is ExtensionUtilTest { + MyDelayedReveal internal ext; + uint256 internal batchId; + bytes internal data; + + function setUp() public override { + super.setUp(); + + ext = new MyDelayedReveal(); + batchId = 1; + data = "test"; + } + + function test_setEncryptedData() public { + ext.setEncryptedData(batchId, data); + + assertEq(true, ext.isEncryptedBatch(batchId)); + assertEq(ext.encryptedData(batchId), data); + } +} diff --git a/src/test/sdk/extension/delayed-reveal/set-encrypted-data/_setEncryptedData.tree b/src/test/sdk/extension/delayed-reveal/set-encrypted-data/_setEncryptedData.tree new file mode 100644 index 000000000..68f99a2c8 --- /dev/null +++ b/src/test/sdk/extension/delayed-reveal/set-encrypted-data/_setEncryptedData.tree @@ -0,0 +1,3 @@ +_setEncryptedData(uint256 _batchId, bytes memory _encryptedData) +├── it should store input bytes data for the given batch id param ✅ +├── isEncryptedBatch should return true for this batch id ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/drop/claim/claim.t.sol b/src/test/sdk/extension/drop/claim/claim.t.sol new file mode 100644 index 000000000..f1dbab680 --- /dev/null +++ b/src/test/sdk/extension/drop/claim/claim.t.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Drop, IDrop, IClaimConditionMultiPhase, IClaimCondition } from "contracts/extension/Drop.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyDrop is Drop { + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override {} + + function _transferTokensOnClaim( + address _to, + uint256 _quantityBeingClaimed + ) internal override returns (uint256 startTokenId) {} + + function _canSetClaimConditions() internal view override returns (bool) { + return true; + } + + function verifyClaim( + uint256 _conditionId, + address _claimer, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof + ) public view override returns (bool isOverride) {} +} + +contract Drop_Claim is ExtensionUtilTest { + MyDrop internal ext; + + address internal _claimer; + uint256 internal _quantity; + address internal _currency; + uint256 internal _pricePerToken; + IDrop.AllowlistProof internal _allowlistProof; + + IClaimCondition.ClaimCondition[] internal claimConditions; + + function setUp() public override { + super.setUp(); + + ext = new MyDrop(); + _claimer = getActor(1); + _quantity = 10; + } + + function _setConditionsState() public { + // values here are not important (except timestamp), since we won't be verifying claim params + + claimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 0, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + ext.setClaimConditions(claimConditions, false); + } + + function test_claim_noConditionsSet() public { + vm.expectRevert(abi.encodeWithSelector(Drop.DropNoActiveCondition.selector)); + ext.claim(_claimer, _quantity, _currency, _pricePerToken, _allowlistProof, ""); + } + + modifier whenConditionsAreSet() { + _setConditionsState(); + _; + } + + function test_claim() public whenConditionsAreSet { + // claim + vm.prank(_claimer); + ext.claim(_claimer, _quantity, _currency, _pricePerToken, _allowlistProof, ""); + + uint256 supplyClaimedByWallet_1 = ext.getSupplyClaimedByWallet(0, _claimer); + uint256 supplyClaimed_1 = (ext.getClaimConditionById(0)).supplyClaimed; + + // claim again + vm.prank(_claimer); + ext.claim(_claimer, _quantity, _currency, _pricePerToken, _allowlistProof, ""); + + uint256 supplyClaimedByWallet_2 = ext.getSupplyClaimedByWallet(0, _claimer); + uint256 supplyClaimed_2 = (ext.getClaimConditionById(0)).supplyClaimed; + + // check state + assertEq(supplyClaimedByWallet_1, _quantity); + assertEq(supplyClaimedByWallet_2, supplyClaimedByWallet_1 + _quantity); + + assertEq(supplyClaimed_1, _quantity); + assertEq(supplyClaimed_2, supplyClaimed_1 + _quantity); + } +} diff --git a/src/test/sdk/extension/drop/claim/claim.tree b/src/test/sdk/extension/drop/claim/claim.tree new file mode 100644 index 000000000..4ca1d3187 --- /dev/null +++ b/src/test/sdk/extension/drop/claim/claim.tree @@ -0,0 +1,15 @@ +claim( + address _receiver, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof, + bytes memory _data +) +├── when no active condition + │ └── it should revert ✅ + └── when there's an active condition + └── it should increase the supplyClaimed for that condition by quantity param input ✅ + └── it should increase the supplyClaimedByWallet for that condition and msg.sender by quantity param input ✅ + +(Note: verifyClaim function has been tested separately, and hence not being tested here) \ No newline at end of file diff --git a/src/test/sdk/extension/drop/get-active-claim-condition-id/getActiveClaimConditionId.t.sol b/src/test/sdk/extension/drop/get-active-claim-condition-id/getActiveClaimConditionId.t.sol new file mode 100644 index 000000000..833e4f4fd --- /dev/null +++ b/src/test/sdk/extension/drop/get-active-claim-condition-id/getActiveClaimConditionId.t.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Drop, IDrop, IClaimConditionMultiPhase, IClaimCondition } from "contracts/extension/upgradeable/Drop.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyDrop is Drop { + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override {} + + function _transferTokensOnClaim( + address _to, + uint256 _quantityBeingClaimed + ) internal override returns (uint256 startTokenId) {} + + function _canSetClaimConditions() internal view override returns (bool) { + return true; + } +} + +contract Drop_GetActiveClaimConditionId is ExtensionUtilTest { + MyDrop internal ext; + + IClaimCondition.ClaimCondition[] internal claimConditions; + + function setUp() public override { + super.setUp(); + + ext = new MyDrop(); + } + + function _setConditionsState() public { + claimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 100, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + claimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 200, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + claimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 300, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + ext.setClaimConditions(claimConditions, false); + } + + function test_getActiveClaimConditionId_noConditionsSet() public { + vm.expectRevert("!CONDITION."); + ext.getActiveClaimConditionId(); + } + + modifier whenConditionsAreSet() { + _setConditionsState(); + _; + } + + function test_getActiveClaimConditionId_noActiveCondition() public whenConditionsAreSet { + vm.expectRevert("!CONDITION."); + ext.getActiveClaimConditionId(); + } + + modifier whenActiveConditions() { + _; + } + + function test_getActiveClaimConditionId_activeConditions() public whenConditionsAreSet whenActiveConditions { + vm.warp(claimConditions[0].startTimestamp); + + uint256 id = ext.getActiveClaimConditionId(); + assertEq(id, 0); + + vm.warp(claimConditions[1].startTimestamp); + + id = ext.getActiveClaimConditionId(); + assertEq(id, 1); + + vm.warp(claimConditions[2].startTimestamp); + + id = ext.getActiveClaimConditionId(); + assertEq(id, 2); + } +} diff --git a/src/test/sdk/extension/drop/get-active-claim-condition-id/getActiveClaimConditionId.tree b/src/test/sdk/extension/drop/get-active-claim-condition-id/getActiveClaimConditionId.tree new file mode 100644 index 000000000..8b8a94d99 --- /dev/null +++ b/src/test/sdk/extension/drop/get-active-claim-condition-id/getActiveClaimConditionId.tree @@ -0,0 +1,8 @@ +getActiveClaimConditionId() +├── when no conditions are set + │ └── it should revert ✅ + └── when condition(s) are set + ├── when no active condition, i.e. start timestamps of all conditions greater than block timestamp + │ └── it should revert ✅ + └── when conditions active, i.e. start timestamps at least one condition is less than or equal to the block timestamp + └── it should return the latest active claim condition id (i.e. with highest start timestamp among those active) ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/drop/set-claim-conditions/setClaimConditions.t.sol b/src/test/sdk/extension/drop/set-claim-conditions/setClaimConditions.t.sol new file mode 100644 index 000000000..476155352 --- /dev/null +++ b/src/test/sdk/extension/drop/set-claim-conditions/setClaimConditions.t.sol @@ -0,0 +1,379 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Drop, IDrop, IClaimConditionMultiPhase, IClaimCondition } from "contracts/extension/Drop.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyDrop is Drop { + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override {} + + function _transferTokensOnClaim( + address _to, + uint256 _quantityBeingClaimed + ) internal override returns (uint256 startTokenId) {} + + function _canSetClaimConditions() internal view override returns (bool) { + return msg.sender == admin; + } + + /** + * note: the functions below are dummy functions for test purposes, + * to directly access and set/reset state without going through the actual functions and checks + */ + + function setCondition(ClaimCondition calldata condition, uint256 _conditionId) public { + claimCondition.conditions[_conditionId] = condition; + } + + function setSupplyClaimedForCondition(uint256 _conditionId, uint256 _supplyClaimed) public { + claimCondition.conditions[_conditionId].supplyClaimed = _supplyClaimed; + } +} + +contract Drop_SetClaimConditions is ExtensionUtilTest { + MyDrop internal ext; + address internal admin; + + IClaimCondition.ClaimCondition[] internal newClaimConditions; + IClaimCondition.ClaimCondition[] internal oldClaimConditions; + + event ClaimConditionsUpdated(IClaimCondition.ClaimCondition[] claimConditions, bool resetEligibility); + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + ext = new MyDrop(admin); + + _setOldConditionsState(); + + newClaimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 100, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + newClaimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 200, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + } + + function _setOldConditionsState() public { + oldClaimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 10, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + oldClaimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 20, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + oldClaimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 30, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + vm.prank(admin); + ext.setClaimConditions(oldClaimConditions, false); + (, uint256 count) = ext.claimCondition(); + assertEq(count, oldClaimConditions.length); + + ext.setSupplyClaimedForCondition(0, 5); + ext.setSupplyClaimedForCondition(0, 20); + ext.setSupplyClaimedForCondition(0, 100); + } + + function test_setClaimConditions_notAuthorized() public { + vm.expectRevert(abi.encodeWithSelector(Drop.DropUnauthorized.selector)); + ext.setClaimConditions(newClaimConditions, false); + + vm.expectRevert(abi.encodeWithSelector(Drop.DropUnauthorized.selector)); + ext.setClaimConditions(newClaimConditions, true); + } + + modifier whenCallerAuthorized() { + vm.startPrank(admin); + _; + vm.stopPrank(); + } + + function test_setClaimConditions_incorrectStartTimestamps() public whenCallerAuthorized { + // reverse the order of timestamps + newClaimConditions[0].startTimestamp = newClaimConditions[1].startTimestamp + 100; + + vm.expectRevert(bytes("ST")); + ext.setClaimConditions(newClaimConditions, false); + + vm.expectRevert(bytes("ST")); + ext.setClaimConditions(newClaimConditions, true); + } + + modifier whenCorrectTimestamps() { + _; + } + + // ================== + // ======= Test branch: claim eligibility reset + // ================== + + function test_setClaimConditions_resetEligibility_startIndex() public whenCallerAuthorized whenCorrectTimestamps { + (, uint256 oldCount) = ext.claimCondition(); + + ext.setClaimConditions(newClaimConditions, true); + + (uint256 newStartIndex, ) = ext.claimCondition(); + assertEq(newStartIndex, oldCount); + } + + function test_setClaimConditions_resetEligibility_conditionCount() + public + whenCallerAuthorized + whenCorrectTimestamps + { + (, uint256 oldCount) = ext.claimCondition(); + assertEq(oldCount, oldClaimConditions.length); + + ext.setClaimConditions(newClaimConditions, true); + + (uint256 newStartIndex, uint256 newCount) = ext.claimCondition(); + assertEq(newCount, newClaimConditions.length); + } + + function test_setClaimConditions_resetEligibility_conditionState() + public + whenCallerAuthorized + whenCorrectTimestamps + { + ext.setClaimConditions(newClaimConditions, true); + + (uint256 newStartIndex, uint256 newCount) = ext.claimCondition(); + + for (uint256 i = 0; i < newCount; i++) { + IClaimCondition.ClaimCondition memory _claimCondition = ext.getClaimConditionById(i + newStartIndex); + + assertEq(_claimCondition.startTimestamp, newClaimConditions[i].startTimestamp); + assertEq(_claimCondition.maxClaimableSupply, newClaimConditions[i].maxClaimableSupply); + assertEq(_claimCondition.supplyClaimed, 0); + } + } + + function test_setClaimConditions_resetEligibility_oldConditionsDeleted() + public + whenCallerAuthorized + whenCorrectTimestamps + { + (uint256 oldStartIndex, uint256 oldCount) = ext.claimCondition(); + assertEq(oldCount, oldClaimConditions.length); + + ext.setClaimConditions(newClaimConditions, true); + + for (uint256 i = 0; i < oldCount; i++) { + IClaimCondition.ClaimCondition memory _claimCondition = ext.getClaimConditionById(i + oldStartIndex); + + assertEq(_claimCondition.startTimestamp, 0); + assertEq(_claimCondition.maxClaimableSupply, 0); + assertEq(_claimCondition.supplyClaimed, 0); + assertEq(_claimCondition.quantityLimitPerWallet, 0); + assertEq(_claimCondition.merkleRoot, bytes32(0)); + assertEq(_claimCondition.pricePerToken, 0); + assertEq(_claimCondition.currency, address(0)); + assertEq(_claimCondition.metadata, ""); + } + } + + function test_setClaimConditions_resetEligibility_event() public whenCallerAuthorized whenCorrectTimestamps { + // TODO: fix/review event data check by setting last param true + vm.expectEmit(false, false, false, false); + emit ClaimConditionsUpdated(newClaimConditions, true); + ext.setClaimConditions(newClaimConditions, true); + } + + // ================== + // ======= Test branch: claim eligibility not reset + // ================== + + function test_setClaimConditions_noReset_maxClaimableLessThanClaimed() + public + whenCallerAuthorized + whenCorrectTimestamps + { + IClaimCondition.ClaimCondition memory _oldCondition = ext.getClaimConditionById(0); + + // set new maxClaimableSupply less than supplyClaimed of the old condition + newClaimConditions[0].maxClaimableSupply = _oldCondition.supplyClaimed - 1; + + vm.expectRevert(abi.encodeWithSelector(Drop.DropExceedMaxSupply.selector)); + ext.setClaimConditions(newClaimConditions, false); + } + + modifier whenMaxClaimableNotLessThanClaimed() { + _; + } + + function test_setClaimConditions_noReset_startIndex() + public + whenCallerAuthorized + whenCorrectTimestamps + whenMaxClaimableNotLessThanClaimed + { + (uint256 oldStartIndex, ) = ext.claimCondition(); + + ext.setClaimConditions(newClaimConditions, false); + + (uint256 newStartIndex, ) = ext.claimCondition(); + assertEq(newStartIndex, oldStartIndex); + } + + function test_setClaimConditions_noReset_conditionCount() + public + whenCallerAuthorized + whenCorrectTimestamps + whenMaxClaimableNotLessThanClaimed + { + (, uint256 oldCount) = ext.claimCondition(); + assertEq(oldCount, oldClaimConditions.length); + + ext.setClaimConditions(newClaimConditions, false); + + (uint256 newStartIndex, uint256 newCount) = ext.claimCondition(); + assertEq(newCount, newClaimConditions.length); + } + + function test_setClaimConditions_noReset_conditionState() + public + whenCallerAuthorized + whenCorrectTimestamps + whenMaxClaimableNotLessThanClaimed + { + (, uint256 oldCount) = ext.claimCondition(); + + // setting array size as this way to avoid out-of-bound error in the second loop + uint256 length = newClaimConditions.length > oldCount ? newClaimConditions.length : oldCount; + IClaimCondition.ClaimCondition[] memory _oldConditions = new IClaimCondition.ClaimCondition[](length); + + for (uint256 i = 0; i < oldCount; i++) { + _oldConditions[i] = ext.getClaimConditionById(i); + } + + ext.setClaimConditions(newClaimConditions, false); + + (uint256 newStartIndex, uint256 newCount) = ext.claimCondition(); + + for (uint256 i = 0; i < newCount; i++) { + IClaimCondition.ClaimCondition memory _claimCondition = ext.getClaimConditionById(i + newStartIndex); + + assertEq(_claimCondition.startTimestamp, newClaimConditions[i].startTimestamp); + assertEq(_claimCondition.maxClaimableSupply, newClaimConditions[i].maxClaimableSupply); + assertEq(_claimCondition.supplyClaimed, _oldConditions[i].supplyClaimed); + } + } + + function test_setClaimConditions_resetEligibility_oldConditionsDeletedOrReplaced() + public + whenCallerAuthorized + whenCorrectTimestamps + whenMaxClaimableNotLessThanClaimed + { + (uint256 oldStartIndex, uint256 oldCount) = ext.claimCondition(); + assertEq(oldCount, oldClaimConditions.length); + + ext.setClaimConditions(newClaimConditions, false); + (, uint256 newCount) = ext.claimCondition(); + + for (uint256 i = 0; i < oldCount; i++) { + IClaimCondition.ClaimCondition memory _claimCondition = ext.getClaimConditionById(i + oldStartIndex); + + if (i >= newCount) { + // case where deleted + + assertEq(_claimCondition.startTimestamp, 0); + assertEq(_claimCondition.maxClaimableSupply, 0); + assertEq(_claimCondition.supplyClaimed, 0); + assertEq(_claimCondition.quantityLimitPerWallet, 0); + assertEq(_claimCondition.merkleRoot, bytes32(0)); + assertEq(_claimCondition.pricePerToken, 0); + assertEq(_claimCondition.currency, address(0)); + assertEq(_claimCondition.metadata, ""); + } else { + // case where replaced + + // supply claimed should be same as old condition, hence not checked below + + assertEq(_claimCondition.startTimestamp, newClaimConditions[i].startTimestamp); + assertEq(_claimCondition.maxClaimableSupply, newClaimConditions[i].maxClaimableSupply); + assertEq(_claimCondition.quantityLimitPerWallet, newClaimConditions[i].quantityLimitPerWallet); + assertEq(_claimCondition.merkleRoot, newClaimConditions[i].merkleRoot); + assertEq(_claimCondition.pricePerToken, newClaimConditions[i].pricePerToken); + assertEq(_claimCondition.currency, newClaimConditions[i].currency); + assertEq(_claimCondition.metadata, newClaimConditions[i].metadata); + } + } + } + + function test_setClaimConditions_noReset_event() + public + whenCallerAuthorized + whenCorrectTimestamps + whenMaxClaimableNotLessThanClaimed + { + // TODO: fix/review event data check by setting last param true + vm.expectEmit(false, false, false, false); + emit ClaimConditionsUpdated(newClaimConditions, false); + ext.setClaimConditions(newClaimConditions, false); + } +} diff --git a/src/test/sdk/extension/drop/set-claim-conditions/setClaimConditions.tree b/src/test/sdk/extension/drop/set-claim-conditions/setClaimConditions.tree new file mode 100644 index 000000000..dbf6297d3 --- /dev/null +++ b/src/test/sdk/extension/drop/set-claim-conditions/setClaimConditions.tree @@ -0,0 +1,24 @@ +setClaimConditions(ClaimCondition[] calldata _conditions, bool _resetClaimEligibility) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + ├── when start timestamps of new conditions aren't in ascending order + │ └── it should revert ✅ + └── when start timestamps of new conditions are in ascending order + ├── when claim eligibility is reset + │ └── it should set new conditions start index as the count of old conditions ✅ + │ └── it should set claim condition count equal to the count of new conditions ✅ + │ └── it should correctly save all new conditions at right index ✅ + │ └── it should set supply claimed for each condition equal to 0 ✅ + │ └── it should delete all old conditions (i.e. all conditions with index less than new start index) ✅ + │ └── it should emit ClaimConditionsUpdated event ✅ + └── when claim eligibility is not reset + ├── when maxClaimableSupply of a new condition is less than supplyClaimed of the old condition (at that index) + │ └── it should revert ✅ + └── when maxClaimableSupply of a new condition is greater than or equal to supplyClaimed of the old condition (at that index) + └── it should set new conditions start index same as old start index ✅ + └── it should set claim condition count equal to the count of new conditions ✅ + └── it should correctly save all new conditions at right index ✅ + └── it should set supply claimed for each condition equal to what it was in old condition (at that index) ✅ + └── it should delete all old conditions with index exceeding new count, in case new count is less than previous count ✅ + └── it should emit ClaimConditionsUpdated event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/drop/verify-claim/verifyClaim.t.sol b/src/test/sdk/extension/drop/verify-claim/verifyClaim.t.sol new file mode 100644 index 000000000..0057f6220 --- /dev/null +++ b/src/test/sdk/extension/drop/verify-claim/verifyClaim.t.sol @@ -0,0 +1,481 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Drop, IDrop, IClaimConditionMultiPhase, IClaimCondition } from "contracts/extension/Drop.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyDrop is Drop { + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override {} + + function _transferTokensOnClaim( + address _to, + uint256 _quantityBeingClaimed + ) internal override returns (uint256 startTokenId) {} + + function _canSetClaimConditions() internal view override returns (bool) { + return true; + } + + /** + * note: the functions below are dummy functions for test purposes, + * to directly access and set/reset state without going through the actual functions and checks + */ + + function setCondition(ClaimCondition calldata condition, uint256 _conditionId) public { + claimCondition.conditions[_conditionId] = condition; + } + + function setSupplyClaimedByWallet(uint256 _conditionId, address _wallet, uint256 _supplyClaimed) public { + claimCondition.supplyClaimedByWallet[_conditionId][_wallet] = _supplyClaimed; + } +} + +contract Drop_VerifyClaim is ExtensionUtilTest { + MyDrop internal ext; + + uint256 internal _conditionId; + address internal _claimer; + address internal _allowlistClaimer; + uint256 internal _quantity; + address internal _currency; + uint256 internal _pricePerToken; + IDrop.AllowlistProof internal _allowlistProof; + IDrop.AllowlistProof internal _allowlistProofEmpty; // will leave uninitialized + + IClaimCondition.ClaimCondition internal claimCondition; + IClaimCondition.ClaimCondition internal claimConditionWithAllowlist; + + function setUp() public override { + super.setUp(); + + ext = new MyDrop(); + + _claimer = getActor(1); + _allowlistClaimer = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + + // claim condition without allowlist + claimCondition = IClaimCondition.ClaimCondition({ + startTimestamp: 1000, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }); + + // claim condition with allowlist -- set defaults for now + claimConditionWithAllowlist = claimCondition; + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs( + 0, // default + type(uint256).max, // default + address(0) // default + ); + } + + function _setAllowlistAndProofs( + uint256 _quantity, + uint256 _price, + address _currency + ) internal returns (IDrop.AllowlistProof memory, bytes32) { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = Strings.toString(_quantity); + inputs[3] = Strings.toString(_price); + inputs[4] = Strings.toHexString(uint160(_currency)); + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + IDrop.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = _quantity; + alp.pricePerToken = _price; + alp.currency = address(_currency); + + return (alp, root); + } + + // ================== + // ======= Test branch: when no allowlist + // ================== + + function test_verifyClaim_noAllowlist_invalidCurrency() public { + ext.setCondition(claimCondition, _conditionId); + + vm.expectRevert( + abi.encodeWithSelector( + Drop.DropClaimInvalidTokenPrice.selector, + _currency, + _pricePerToken, + claimCondition.currency, + claimCondition.pricePerToken + ) + ); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + modifier whenValidCurrency_open() { + _currency = claimCondition.currency; + _; + } + + function test_verifyClaim_noAllowlist_invalidPrice() public whenValidCurrency_open { + ext.setCondition(claimCondition, _conditionId); + + vm.expectRevert( + abi.encodeWithSelector( + Drop.DropClaimInvalidTokenPrice.selector, + _currency, + _pricePerToken, + claimCondition.currency, + claimCondition.pricePerToken + ) + ); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + modifier whenValidPrice_open() { + _pricePerToken = claimCondition.pricePerToken; + _; + } + + function test_verifyClaim_noAllowlist_zeroQuantity() public whenValidCurrency_open whenValidPrice_open { + ext.setCondition(claimCondition, _conditionId); + + _quantity = 0; + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, claimCondition.quantityLimitPerWallet, 0) + ); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + modifier whenNonZeroQuantity() { + _quantity = claimCondition.quantityLimitPerWallet + 1234; + _; + } + + function test_verifyClaim_noAllowlist_nonZeroInvalidQuantity() + public + whenValidCurrency_open + whenValidPrice_open + whenNonZeroQuantity + { + ext.setCondition(claimCondition, _conditionId); + ext.setSupplyClaimedByWallet(_conditionId, _claimer, claimCondition.quantityLimitPerWallet); + + vm.expectRevert( + abi.encodeWithSelector( + Drop.DropClaimExceedLimit.selector, + claimCondition.quantityLimitPerWallet, + _quantity + claimCondition.quantityLimitPerWallet + ) + ); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + modifier whenValidQuantity_open() { + _quantity = 1; + _; + } + + function test_verifyClaim_noAllowlist_quantityMoreThanMaxClaimableSupply() + public + whenValidCurrency_open + whenValidPrice_open + whenNonZeroQuantity + whenValidQuantity_open + { + claimCondition.supplyClaimed = claimCondition.maxClaimableSupply; + ext.setCondition(claimCondition, _conditionId); + + vm.expectRevert( + abi.encodeWithSelector( + Drop.DropClaimExceedMaxSupply.selector, + claimCondition.maxClaimableSupply, + _quantity + claimCondition.maxClaimableSupply + ) + ); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + modifier whenQuantityWithinMaxLimit() { + _; + } + + function test_verifyClaim_noAllowlist_beforeStartTimestamp() + public + whenValidCurrency_open + whenValidPrice_open + whenNonZeroQuantity + whenValidQuantity_open + whenQuantityWithinMaxLimit + { + ext.setCondition(claimCondition, _conditionId); + + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimNotStarted.selector, claimCondition.startTimestamp, block.timestamp) + ); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + modifier whenValidTimestamp() { + vm.warp(claimCondition.startTimestamp); + _; + } + + function test_verifyClaim_noAllowlist() + public + whenValidCurrency_open + whenValidPrice_open + whenNonZeroQuantity + whenValidQuantity_open + whenQuantityWithinMaxLimit + whenValidTimestamp + { + ext.setCondition(claimCondition, _conditionId); + + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + // ================== + // ======= Test branch: allowlist but incorrect proof -- open limits should apply + // ================== + + function test_verifyClaim_incorrectProof_invalidCurrency() public { + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + vm.expectRevert( + abi.encodeWithSelector( + Drop.DropClaimInvalidTokenPrice.selector, + _currency, + _pricePerToken, + claimCondition.currency, + claimCondition.pricePerToken + ) + ); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + function test_verifyClaim_incorrectProof_invalidPrice() public whenValidCurrency_open { + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + vm.expectRevert( + abi.encodeWithSelector( + Drop.DropClaimInvalidTokenPrice.selector, + _currency, + _pricePerToken, + claimCondition.currency, + claimCondition.pricePerToken + ) + ); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + function test_verifyClaim_incorrectProof_zeroQuantity() public whenValidCurrency_open whenValidPrice_open { + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + _quantity = 0; + vm.expectRevert( + abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, claimCondition.quantityLimitPerWallet, _quantity) + ); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + function test_verifyClaim_incorrectProof_nonZeroInvalidQuantity() + public + whenValidCurrency_open + whenValidPrice_open + whenNonZeroQuantity + { + ext.setCondition(claimConditionWithAllowlist, _conditionId); + ext.setSupplyClaimedByWallet(_conditionId, _claimer, claimCondition.quantityLimitPerWallet); + + vm.expectRevert( + abi.encodeWithSelector( + Drop.DropClaimExceedLimit.selector, + claimCondition.quantityLimitPerWallet, + claimCondition.quantityLimitPerWallet + _quantity + ) + ); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + function test_verifyClaim_incorrectProof() + public + whenValidCurrency_open + whenValidPrice_open + whenNonZeroQuantity + whenValidQuantity_open + whenQuantityWithinMaxLimit + whenValidTimestamp + { + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + // ================== + // ======= Test branch: allowlist with correct proof + // ================== + + function test_verifyClaim_allowlist_defaultPriceAndCurrency_invalidCurrencyParam() public { + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + vm.expectRevert( + abi.encodeWithSelector( + Drop.DropClaimInvalidTokenPrice.selector, + _currency, + _pricePerToken, + claimCondition.currency, + claimCondition.pricePerToken + ) + ); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist_defaultPriceNonDefaultCurrenct_invalidCurrencyParam() public { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs( + 0, // default + type(uint256).max, // default + address(weth) + ); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + vm.expectRevert( + abi.encodeWithSelector( + Drop.DropClaimInvalidTokenPrice.selector, + _currency, + _pricePerToken, + claimCondition.currency, + claimCondition.pricePerToken + ) + ); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist_nonDefaultPriceAndCurrency_invalidCurrencyParam() public { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs( + 0, // default + 2, + address(weth) + ); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + vm.expectRevert( + abi.encodeWithSelector( + Drop.DropClaimInvalidTokenPrice.selector, + _currency, + _pricePerToken, + address(weth), + 2 + ) + ); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist_defaultQuantity_invalidQuantityParam() public { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs( + 0, // default + 2, + address(weth) + ); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + ext.setSupplyClaimedByWallet( + _conditionId, + _allowlistClaimer, + claimConditionWithAllowlist.quantityLimitPerWallet + ); + + _currency = address(weth); + _pricePerToken = 2; + _quantity = 1; + vm.expectRevert( + abi.encodeWithSelector( + Drop.DropClaimExceedLimit.selector, + claimCondition.quantityLimitPerWallet, + claimCondition.quantityLimitPerWallet + _quantity + ) + ); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist_nonDefaultQuantity_invalidQuantityParam() public { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs(5, 2, address(weth)); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + ext.setSupplyClaimedByWallet(_conditionId, _allowlistClaimer, 5); + + _currency = address(weth); + _pricePerToken = 2; + _quantity = 1; + vm.expectRevert(abi.encodeWithSelector(Drop.DropClaimExceedLimit.selector, 5, 5 + _quantity)); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist_defaultPrice_invalidPriceParam() public { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs( + 5, + type(uint256).max, // default + address(weth) + ); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + _currency = address(weth); + _quantity = 1; + vm.expectRevert( + abi.encodeWithSelector( + Drop.DropClaimInvalidTokenPrice.selector, + _currency, + _pricePerToken, + claimCondition.currency, + claimCondition.pricePerToken + ) + ); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist_nonDefaultPrice_invalidPriceParam() public { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs(5, 1, address(weth)); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + _currency = address(weth); + _quantity = 1; + _pricePerToken = 2; + vm.expectRevert( + abi.encodeWithSelector( + Drop.DropClaimInvalidTokenPrice.selector, + _currency, + _pricePerToken, + address(weth), + 1 + ) + ); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist() public whenQuantityWithinMaxLimit whenValidTimestamp { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs(5, 1, address(weth)); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + _currency = address(weth); + _quantity = 1; + _pricePerToken = 1; + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } +} diff --git a/src/test/sdk/extension/drop/verify-claim/verifyClaim.tree b/src/test/sdk/extension/drop/verify-claim/verifyClaim.tree new file mode 100644 index 000000000..64553ab90 --- /dev/null +++ b/src/test/sdk/extension/drop/verify-claim/verifyClaim.tree @@ -0,0 +1,67 @@ +verifyClaim( + uint256 conditionId, + address claimer, + uint256 quantity, + address currency, + uint256 pricePerToken, + AllowlistProof calldata allowlistProof +) +├── when no allowlist + └── when currency param not equal to open claim currency + │ └── it should revert ✅ + └── when currency param is equal to open claim currency + └── when pricePerToken param not equal to open claim price + │ └── it should revert ✅ + └── when pricePerToken param is equal to open claim price + └── when quantity param is 0 + │ └── it should revert ✅ + └── when quantity param is not 0 + └── when quantity param plus supply claimed is more than open claim limit + │ └── it should revert ✅ + └── when quantity param plus supply claimed is within open claim limit + └── when quantity param plus claimed supply is more than max claimable supply + │ └── it should revert ✅ + └── when quantity param plus claimed supply is within max claimable supply limit + └── when block timestamp is less than start timestamp of claim phase + │ └── it should revert ✅ + └── when block timestamp is greater than or equal to start timestamp of claim phase + └── execution completes -- exit function ✅ + +├── when allowlist but incorrect merkle proof + └── when currency param not equal to open claim currency + │ └── it should revert ✅ + └── when currency param is equal to open claim currency + └── when pricePerToken param not equal to open claim price + │ └── it should revert ✅ + └── when pricePerToken param is equal to open claim price + └── when quantity param is 0 + │ └── it should revert ✅ + └── when quantity param is not 0 + └── when quantity param plus supply claimed is more than open claim limit + │ └── it should revert ✅ + +├── when allowlist and correct merkle proof + └── when allowlist price is default max uint256 and allowlist currency is default address(0) + │ └── when currency param not equal to open claim currency + │ └── it should revert ✅ + └── when allowlist price is default max uint256 and allowlist currency is not default + │ └── when currency param not equal to open claim currency + │ └── it should revert ✅ + └── when allowlist price is not default and allowlist currency is default address(0) + │ └── when currency param not equal to open claim currency + │ └── it should revert ✅ + └── when allowlist price is not default and allowlist currency is not default + │ └── when currency param not equal to allowlist claim currency + │ └── it should revert ✅ + └── when allowlist quantity is default 0 + │ └── when nonzero quantity param plus supply claimed is more than open claim limit + │ └── it should revert ✅ + └── when allowlist quantity is not default + │ └── when nonzero quantity param plus supply claimed is more than allowlist claim limit + │ └── it should revert ✅ + └── when allowlist price is default max uint256 + │ └── when pricePerToken param not equal to open claim price + │ └── it should revert ✅ + └── when allowlist price is not default + │ └── when pricePerToken param not equal to allowlist claim price + │ └── it should revert ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/lazy-mint/lazy-mint/lazyMint.t.sol b/src/test/sdk/extension/lazy-mint/lazy-mint/lazyMint.t.sol new file mode 100644 index 000000000..ae6774d9e --- /dev/null +++ b/src/test/sdk/extension/lazy-mint/lazy-mint/lazyMint.t.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { LazyMint, BatchMintMetadata } from "contracts/extension/LazyMint.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyLazyMint is LazyMint { + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function _canLazyMint() internal view override returns (bool) { + return msg.sender == admin; + } + + function getBaseURI(uint256 _tokenId) external view returns (string memory) { + return _getBaseURI(_tokenId); + } + + function getBatchStartId(uint256 _batchID) public view returns (uint256) { + return _getBatchStartId(_batchID); + } + + function nextTokenIdToMint() public view returns (uint256) { + return nextTokenIdToLazyMint; + } +} + +contract LazyMint_LazyMint is ExtensionUtilTest { + MyLazyMint internal ext; + uint256 internal startId; + uint256 internal amount; + uint256[] internal batchIds; + address internal admin; + address internal caller; + + event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI); + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + caller = getActor(1); + + ext = new MyLazyMint(address(admin)); + + startId = 0; + // mint 5 batches + vm.startPrank(admin); + for (uint256 i = 0; i < 5; i++) { + uint256 _amount = (i + 1) * 10; + uint256 batchId = startId + _amount; + batchIds.push(batchId); + + string memory baseURI = Strings.toString(batchId); + startId = ext.lazyMint(_amount, baseURI, ""); + } + vm.stopPrank(); + } + + function test_lazyMint_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert(abi.encodeWithSelector(LazyMint.LazyMintUnauthorized.selector)); + ext.lazyMint(amount, "", ""); + } + + modifier whenCallerAuthorized() { + caller = admin; + _; + } + + function test_lazyMint_zeroAmount() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectRevert(abi.encodeWithSelector(LazyMint.LazyMintInvalidAmount.selector)); + ext.lazyMint(amount, "", ""); + } + + modifier whenAmountNotZero() { + amount = 50; + _; + } + + function test_lazyMint() public whenCallerAuthorized whenAmountNotZero { + // check previous state + uint256 _nextTokenIdToLazyMintOld = ext.nextTokenIdToMint(); + assertEq(_nextTokenIdToLazyMintOld, batchIds[4]); + + string memory baseURI = "ipfs://baseURI"; + + // lazy mint next batch + vm.prank(address(caller)); + uint256 _batchId = ext.lazyMint(amount, baseURI, ""); + + // check new state + uint256 _batchStartId = ext.getBatchStartId(_batchId); + assertEq(_nextTokenIdToLazyMintOld, _batchStartId); + assertEq(_batchId, _nextTokenIdToLazyMintOld + amount); + for (uint256 i = _batchStartId; i < _batchId; i++) { + assertEq(ext.getBaseURI(i), baseURI); + } + assertEq(ext.nextTokenIdToMint(), _nextTokenIdToLazyMintOld + amount); + } + + function test_lazyMint_event() public whenCallerAuthorized whenAmountNotZero { + string memory baseURI = "ipfs://baseURI"; + uint256 _nextTokenIdToLazyMintOld = ext.nextTokenIdToMint(); + + // lazy mint next batch + vm.prank(address(caller)); + vm.expectEmit(); + emit TokensLazyMinted(_nextTokenIdToLazyMintOld, _nextTokenIdToLazyMintOld + amount - 1, baseURI, ""); + ext.lazyMint(amount, baseURI, ""); + } +} diff --git a/src/test/sdk/extension/lazy-mint/lazy-mint/lazyMint.tree b/src/test/sdk/extension/lazy-mint/lazy-mint/lazyMint.tree new file mode 100644 index 000000000..72ac4ddb3 --- /dev/null +++ b/src/test/sdk/extension/lazy-mint/lazy-mint/lazyMint.tree @@ -0,0 +1,17 @@ +lazyMint( + uint256 _amount, + string calldata _baseURIForTokens, + bytes calldata _data +) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── when amount to lazy mint is 0 + │ └── it should revert ✅ + └── when amount to lazy mint is not 0 + └── it should save the batch of tokens starting at `nextTokenIdToLazyMint` ✅ + └── it should store batch id equal to the sum of `nextTokenIdToLazyMint` and `_amount` ✅ + └── it should map the new batch id to `_baseURIForTokens` ✅ + └── it should increase `nextTokenIdToLazyMint` by `_amount` ✅ + └── it should return the new `batchId` ✅ + └── it should emit TokensLazyMinted event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/ownable/set-owner/setOwner.t.sol b/src/test/sdk/extension/ownable/set-owner/setOwner.t.sol new file mode 100644 index 000000000..7f6d136d6 --- /dev/null +++ b/src/test/sdk/extension/ownable/set-owner/setOwner.t.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Ownable, IOwnable } from "contracts/extension/Ownable.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyOwnable is Ownable { + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function _canSetOwner() internal view override returns (bool) { + return msg.sender == admin; + } +} + +contract Ownable_SetOwner is ExtensionUtilTest { + MyOwnable internal ext; + address internal admin; + address internal caller; + address internal oldOwner; + address internal newOwner; + + event OwnerUpdated(address indexed prevOwner, address indexed newOwner); + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + caller = getActor(1); + + oldOwner = getActor(2); + newOwner = getActor(3); + + ext = new MyOwnable(address(admin)); + + vm.prank(address(admin)); + ext.setOwner(oldOwner); + + assertEq(oldOwner, ext.owner()); + } + + function test_setOwner_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorized.selector)); + ext.setOwner(newOwner); + } + + modifier whenCallerAuthorized() { + caller = admin; + _; + } + + function test_setOwner() public whenCallerAuthorized { + vm.prank(address(caller)); + ext.setOwner(newOwner); + + assertEq(newOwner, ext.owner()); + } + + function test_setOwner_event() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectEmit(true, true, false, false); + emit OwnerUpdated(oldOwner, newOwner); + ext.setOwner(newOwner); + } +} diff --git a/src/test/sdk/extension/ownable/set-owner/setOwner.tree b/src/test/sdk/extension/ownable/set-owner/setOwner.tree new file mode 100644 index 000000000..9db2c0a70 --- /dev/null +++ b/src/test/sdk/extension/ownable/set-owner/setOwner.tree @@ -0,0 +1,6 @@ +setContractURI(string memory uri) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── it should update owner by replacing old owner with the new owner input ✅ + └── it should emit OwnerUpdated event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.t.sol b/src/test/sdk/extension/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.t.sol new file mode 100644 index 000000000..792f1c52e --- /dev/null +++ b/src/test/sdk/extension/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.t.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Royalty, IRoyalty } from "contracts/extension/Royalty.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyRoyalty is Royalty { + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {} + + function _canSetRoyaltyInfo() internal view override returns (bool) { + return msg.sender == admin; + } +} + +contract Royalty_SetDefaultRoyaltyInfo is ExtensionUtilTest { + MyRoyalty internal ext; + address internal admin; + address internal caller; + address internal defaultRoyaltyRecipient; + uint256 internal defaultRoyaltyBps; + + event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps); + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + caller = getActor(1); + defaultRoyaltyRecipient = getActor(2); + + ext = new MyRoyalty(address(admin)); + } + + function test_setDefaultRoyaltyInfo_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert(abi.encodeWithSelector(Royalty.RoyaltyUnauthorized.selector)); + ext.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } + + modifier whenCallerAuthorized() { + caller = admin; + _; + } + + function test_setDefaultRoyaltyInfo_exceedMaxBps() public whenCallerAuthorized { + defaultRoyaltyBps = 10_001; + vm.prank(address(caller)); + vm.expectRevert(abi.encodeWithSelector(Royalty.RoyaltyExceededMaxFeeBps.selector, 10_000, defaultRoyaltyBps)); + ext.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } + + modifier whenNotExceedMaxBps() { + defaultRoyaltyBps = 500; + _; + } + + function test_setDefaultRoyaltyInfo() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + ext.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + + // get default royalty info + (address _recipient, uint16 _royaltyBps) = ext.getDefaultRoyaltyInfo(); + assertEq(_recipient, defaultRoyaltyRecipient); + assertEq(_royaltyBps, uint16(defaultRoyaltyBps)); + + // get royalty info for token + uint256 tokenId = 0; + (_recipient, _royaltyBps) = ext.getRoyaltyInfoForToken(tokenId); + assertEq(_recipient, defaultRoyaltyRecipient); + assertEq(_royaltyBps, uint16(defaultRoyaltyBps)); + + // royaltyInfo - ERC2981 + uint256 salePrice = 1000; + (address _royaltyRecipient, uint256 _royaltyAmount) = ext.royaltyInfo(tokenId, salePrice); + assertEq(_royaltyRecipient, defaultRoyaltyRecipient); + assertEq(_royaltyAmount, (salePrice * defaultRoyaltyBps) / 10_000); + } + + function test_setDefaultRoyaltyInfo_event() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + vm.expectEmit(true, false, false, true); + emit DefaultRoyalty(defaultRoyaltyRecipient, defaultRoyaltyBps); + ext.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } +} diff --git a/src/test/sdk/extension/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.tree b/src/test/sdk/extension/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.tree new file mode 100644 index 000000000..78a4312de --- /dev/null +++ b/src/test/sdk/extension/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.tree @@ -0,0 +1,11 @@ +setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + ├── when royalty bps input is greater than MAX_BPS + │ └── it should revert ✅ + └── when royalty bps input is less than or equal to MAX_BPS + └── it should update default royalty recipient ✅ + └── it should update default royalty bps ✅ + └── it should calculate royalty amount for a token-id based on default royalty info ✅ + └── it should emit DefaultRoyalty event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.t.sol b/src/test/sdk/extension/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.t.sol new file mode 100644 index 000000000..997697a5e --- /dev/null +++ b/src/test/sdk/extension/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Royalty, IRoyalty } from "contracts/extension/Royalty.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyRoyalty is Royalty { + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {} + + function _canSetRoyaltyInfo() internal view override returns (bool) { + return msg.sender == admin; + } +} + +contract Royalty_SetRoyaltyInfoForToken is ExtensionUtilTest { + MyRoyalty internal ext; + address internal admin; + address internal caller; + address internal defaultRoyaltyRecipient; + uint256 internal defaultRoyaltyBps; + + address internal royaltyRecipientForToken; + uint256 internal royaltyBpsForToken; + uint256 internal tokenId; + + event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, uint256 royaltyBps); + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + caller = getActor(1); + defaultRoyaltyRecipient = getActor(2); + royaltyRecipientForToken = getActor(3); + defaultRoyaltyBps = 500; + tokenId = 1; + + ext = new MyRoyalty(address(admin)); + + vm.prank(address(admin)); + ext.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } + + function test_setRoyaltyInfoForToken_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert(abi.encodeWithSelector(Royalty.RoyaltyUnauthorized.selector)); + ext.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + } + + modifier whenCallerAuthorized() { + caller = admin; + _; + } + + function test_setRoyaltyInfoForToken_exceedMaxBps() public whenCallerAuthorized { + royaltyBpsForToken = 10_001; + vm.prank(address(caller)); + vm.expectRevert(abi.encodeWithSelector(Royalty.RoyaltyExceededMaxFeeBps.selector, 10_000, royaltyBpsForToken)); + ext.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + } + + modifier whenNotExceedMaxBps() { + royaltyBpsForToken = 1000; + _; + } + + function test_setRoyaltyInfoForToken() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + ext.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + + // get default royalty info + (address _defaultRecipient, uint16 _defaultRoyaltyBps) = ext.getDefaultRoyaltyInfo(); + assertEq(_defaultRecipient, defaultRoyaltyRecipient); + assertEq(_defaultRoyaltyBps, uint16(defaultRoyaltyBps)); + + // get royalty info for token + (address _royaltyRecipientForToken, uint16 _royaltyBpsForToken) = ext.getRoyaltyInfoForToken(tokenId); + assertEq(_royaltyRecipientForToken, royaltyRecipientForToken); + assertEq(_royaltyBpsForToken, uint16(royaltyBpsForToken)); + + // royaltyInfo - ERC2981: calculate for default + uint256 salePrice = 1000; + (address _royaltyRecipient, uint256 _royaltyAmount) = ext.royaltyInfo(0, salePrice); + assertEq(_royaltyRecipient, defaultRoyaltyRecipient); + assertEq(_royaltyAmount, (salePrice * defaultRoyaltyBps) / 10_000); + + // royaltyInfo - ERC2981: calculate for specific tokenId we set the royalty info for + (_royaltyRecipient, _royaltyAmount) = ext.royaltyInfo(tokenId, salePrice); + assertEq(_royaltyRecipient, royaltyRecipientForToken); + assertEq(_royaltyAmount, (salePrice * royaltyBpsForToken) / 10_000); + } + + function test_setRoyaltyInfoForToken_event() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + vm.expectEmit(true, true, false, true); + emit RoyaltyForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + ext.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + } +} diff --git a/src/test/sdk/extension/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.tree b/src/test/sdk/extension/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.tree new file mode 100644 index 000000000..e28295634 --- /dev/null +++ b/src/test/sdk/extension/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.tree @@ -0,0 +1,15 @@ +function setRoyaltyInfoForToken( + uint256 _tokenId, + address _recipient, + uint256 _bps +) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + ├── when royalty bps input is greater than MAX_BPS + │ └── it should revert ✅ + └── when royalty bps input is less than or equal to MAX_BPS + └── it should update default royalty recipient ✅ + └── it should update default royalty bps ✅ + └── it should calculate royalty amount for a token-id based on default royalty info ✅ + └── it should emit DefaultRoyalty event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.t.sol b/src/test/sdk/extension/upgradeable/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.t.sol new file mode 100644 index 000000000..6ca105a6f --- /dev/null +++ b/src/test/sdk/extension/upgradeable/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.t.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BatchMintMetadata } from "contracts/extension/upgradeable/BatchMintMetadata.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyBatchMintMetadataUpg is BatchMintMetadata { + function batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) external returns (uint256 nextTokenIdToMint, uint256 batchId) { + (nextTokenIdToMint, batchId) = _batchMintMetadata(_startId, _amountToMint, _baseURIForTokens); + } + + function getBaseURI(uint256 _batchId) external view returns (string memory) { + return _batchMintMetadataStorage().baseURI[_batchId]; + } +} + +contract UpgradeableBatchMintMetadata_BatchMintMetadata is ExtensionUtilTest { + MyBatchMintMetadataUpg internal ext; + uint256 internal startId; + uint256 internal amountToMint; + string internal baseURI; + + function setUp() public override { + super.setUp(); + + ext = new MyBatchMintMetadataUpg(); + startId = 20; + amountToMint = 100; + baseURI = "ipfs://baseURI"; + } + + function test_batchMintMetadata() public { + uint256 prevBaseURICount = ext.getBaseURICount(); + uint256 batchId = startId + amountToMint; + + ext.batchMintMetadata(startId, amountToMint, baseURI); + uint256 newBaseURICount = ext.getBaseURICount(); + assertEq(ext.getBaseURI(batchId), baseURI); + assertEq(newBaseURICount, prevBaseURICount + 1); + assertEq(ext.getBatchIdAtIndex(newBaseURICount - 1), batchId); + + vm.expectRevert("Invalid index"); + ext.getBatchIdAtIndex(newBaseURICount); + } +} diff --git a/src/test/sdk/extension/upgradeable/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.tree b/src/test/sdk/extension/upgradeable/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.tree new file mode 100644 index 000000000..572dd5203 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.tree @@ -0,0 +1,7 @@ +_batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens +) +├── it should store batch id equal to the sum of `_startId` and `_amountToMint` in batchIds array ✅ +├── it should map the new batch id to `_baseURIForTokens` ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.t.sol b/src/test/sdk/extension/upgradeable/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.t.sol new file mode 100644 index 000000000..9bc777bf4 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.t.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BatchMintMetadata } from "contracts/extension/upgradeable/BatchMintMetadata.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyBatchMintMetadataUpg is BatchMintMetadata { + function batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) external returns (uint256 nextTokenIdToMint, uint256 batchId) { + (nextTokenIdToMint, batchId) = _batchMintMetadata(_startId, _amountToMint, _baseURIForTokens); + } + + function freezeBaseURI(uint256 _batchId) external { + _freezeBaseURI(_batchId); + } + + function batchFrozen(uint256 _batchId) external view returns (bool) { + return _batchMintMetadataStorage().batchFrozen[_batchId]; + } +} + +contract UpgradeableBatchMintMetadata_FreezeBaseURI is ExtensionUtilTest { + MyBatchMintMetadataUpg internal ext; + uint256 internal startId; + uint256[] internal batchIds; + uint256 internal indexToFreeze; + + event MetadataFrozen(); + + function setUp() public override { + super.setUp(); + + ext = new MyBatchMintMetadataUpg(); + + startId = 0; + // mint 5 batches + for (uint256 i = 0; i < 5; i++) { + uint256 amount = (i + 1) * 10; + uint256 batchId = startId + amount; + batchIds.push(batchId); + + (startId, ) = ext.batchMintMetadata(startId, amount, "ipfs://"); + assertEq(ext.batchFrozen(batchId), false); + } + + indexToFreeze = 3; + } + + function test_freezeBaseURI_invalidBatch() public { + vm.expectRevert("Invalid batch"); + ext.freezeBaseURI(batchIds[indexToFreeze] * 10); // non-existent batchId + } + + modifier whenBatchIdValid() { + _; + } + + function test_freezeBaseURI() public whenBatchIdValid { + ext.freezeBaseURI(batchIds[indexToFreeze]); + + assertEq(ext.batchFrozen(batchIds[indexToFreeze]), true); + } + + function test_freezeBaseURI_event() public whenBatchIdValid { + vm.expectEmit(false, false, false, false); + emit MetadataFrozen(); + ext.freezeBaseURI(batchIds[indexToFreeze]); + } +} diff --git a/src/test/sdk/extension/upgradeable/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.tree b/src/test/sdk/extension/upgradeable/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.tree new file mode 100644 index 000000000..4dd87edef --- /dev/null +++ b/src/test/sdk/extension/upgradeable/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.tree @@ -0,0 +1,6 @@ +_freezeBaseURI(uint256 _batchId) +├── when there is no baseURI for given `_batchId` + │ └── it should revert ✅ + └── when there is a baseURI present for given `_batchId` + └── it should freeze the `batchId` by setting `frozen[_batchId]` to `true` ✅ + └── it should emit MetadataFrozen event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-base-uri/_getBaseURI.t.sol b/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-base-uri/_getBaseURI.t.sol new file mode 100644 index 000000000..153915408 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-base-uri/_getBaseURI.t.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BatchMintMetadata } from "contracts/extension/upgradeable/BatchMintMetadata.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyBatchMintMetadataUpg is BatchMintMetadata { + function batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) external returns (uint256 nextTokenIdToMint, uint256 batchId) { + (nextTokenIdToMint, batchId) = _batchMintMetadata(_startId, _amountToMint, _baseURIForTokens); + } + + function getBaseURI(uint256 _tokenId) external view returns (string memory) { + return _getBaseURI(_tokenId); + } +} + +contract UpgradeableBatchMintMetadata_GetBaseURI is ExtensionUtilTest { + MyBatchMintMetadataUpg internal ext; + uint256 internal startId; + uint256[] internal batchIds; + + function setUp() public override { + super.setUp(); + + ext = new MyBatchMintMetadataUpg(); + + startId = 0; + // mint 5 batches + for (uint256 i = 0; i < 5; i++) { + uint256 amount = (i + 1) * 10; + uint256 batchId = startId + amount; + batchIds.push(batchId); + + string memory baseURI = Strings.toString(batchId); + (startId, ) = ext.batchMintMetadata(startId, amount, baseURI); + } + } + + function test_getBaseURI_invalidTokenId() public { + uint256 tokenId = batchIds[4]; // tokenId greater than the last batchId + + vm.expectRevert("Invalid tokenId"); + ext.getBaseURI(tokenId); + } + + modifier whenValidTokenId() { + _; + } + + function test_getBaseURI() public whenValidTokenId { + for (uint256 i = 0; i < 5; i++) { + uint256 start = i == 0 ? 0 : batchIds[i - 1]; + for (uint256 j = start; j < batchIds[i]; j++) { + string memory _baseURI = ext.getBaseURI(j); + + assertEq(_baseURI, Strings.toString(batchIds[i])); + } + } + } +} diff --git a/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-base-uri/_getBaseURI.tree b/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-base-uri/_getBaseURI.tree new file mode 100644 index 000000000..c4ee674bf --- /dev/null +++ b/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-base-uri/_getBaseURI.tree @@ -0,0 +1,6 @@ +_getBaseURI(uint256 _tokenId) +├── when `_tokenId` doesn't belong to any batch, i.e. greater than the last batchId + │ └── it should revert ✅ + └── when `_tokenId` belongs to some batch, i.e. less than that batchId + └── it should return correct baseURI for the `_tokenId` ✅ +(note: all batches are assumed to be contiguous, i.e. start id of one batch is the end id of the previous batch) \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-id/_getBatchId.t.sol b/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-id/_getBatchId.t.sol new file mode 100644 index 000000000..9c8ec1136 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-id/_getBatchId.t.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BatchMintMetadata } from "contracts/extension/upgradeable/BatchMintMetadata.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyBatchMintMetadataUpg is BatchMintMetadata { + function batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) external returns (uint256 nextTokenIdToMint, uint256 batchId) { + (nextTokenIdToMint, batchId) = _batchMintMetadata(_startId, _amountToMint, _baseURIForTokens); + } + + function getBatchId(uint256 _tokenId) external view returns (uint256 batchId, uint256 index) { + return _getBatchId(_tokenId); + } +} + +contract UpgradeableBatchMintMetadata_GetBatchId is ExtensionUtilTest { + MyBatchMintMetadataUpg internal ext; + uint256 internal startId; + uint256[] internal batchIds; + + function setUp() public override { + super.setUp(); + + ext = new MyBatchMintMetadataUpg(); + + startId = 0; + // mint 5 batches + for (uint256 i = 0; i < 5; i++) { + uint256 amount = (i + 1) * 10; + batchIds.push(startId + amount); + (startId, ) = ext.batchMintMetadata(startId, amount, "ipfs://"); + } + } + + function test_getBatchId_invalidTokenId() public { + uint256 tokenId = batchIds[4]; // tokenId greater than the last batchId + + vm.expectRevert("Invalid tokenId"); + ext.getBatchId(tokenId); + } + + modifier whenValidTokenId() { + _; + } + + function test_getBatchId() public whenValidTokenId { + for (uint256 i = 0; i < 5; i++) { + uint256 start = i == 0 ? 0 : batchIds[i - 1]; + for (uint256 j = start; j < batchIds[i]; j++) { + (uint256 batchId, uint256 index) = ext.getBatchId(j); + + assertEq(batchId, batchIds[i]); + assertEq(index, i); + } + } + } +} diff --git a/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-id/_getBatchId.tree b/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-id/_getBatchId.tree new file mode 100644 index 000000000..2e6dd366e --- /dev/null +++ b/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-id/_getBatchId.tree @@ -0,0 +1,6 @@ +_getBatchId(uint256 _tokenId) +├── when `_tokenId` doesn't belong to any batch, i.e. greater than the last batchId + │ └── it should revert ✅ + └── when `_tokenId` belongs to some batch, i.e. less than that batchId + └── it should return correct batchId and batch index for the `_tokenId` ✅ +(note: all batches are assumed to be contiguous, i.e. start id of one batch is the end id of the previous batch) \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-start-id/_getBatchStartId.t.sol b/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-start-id/_getBatchStartId.t.sol new file mode 100644 index 000000000..6a6182a3b --- /dev/null +++ b/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-start-id/_getBatchStartId.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BatchMintMetadata } from "contracts/extension/upgradeable/BatchMintMetadata.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyBatchMintMetadataUpg is BatchMintMetadata { + function batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) external returns (uint256 nextTokenIdToMint, uint256 batchId) { + (nextTokenIdToMint, batchId) = _batchMintMetadata(_startId, _amountToMint, _baseURIForTokens); + } + + function getBatchStartId(uint256 _batchId) external view returns (uint256) { + return _getBatchStartId(_batchId); + } +} + +contract UpgradeableBatchMintMetadata_GetBatchStartId is ExtensionUtilTest { + MyBatchMintMetadataUpg internal ext; + uint256 internal startId; + uint256[] internal batchIds; + + function setUp() public override { + super.setUp(); + + ext = new MyBatchMintMetadataUpg(); + + startId = 0; + // mint 5 batches + for (uint256 i = 0; i < 5; i++) { + uint256 amount = (i + 1) * 10; + batchIds.push(startId + amount); + (startId, ) = ext.batchMintMetadata(startId, amount, "ipfs://"); + } + } + + function test_getBatchStartId_invalidBatchId() public { + uint256 batchId = batchIds[4] + 1; // non-existent batchId + + vm.expectRevert("Invalid batchId"); + ext.getBatchStartId(batchId); + } + + modifier whenValidBatchId() { + _; + } + + function test_getBatchStartId() public whenValidBatchId { + for (uint256 i = 0; i < 5; i++) { + uint256 start = i == 0 ? 0 : batchIds[i - 1]; + uint256 _batchStartId = ext.getBatchStartId(batchIds[i]); + + assertEq(start, _batchStartId); + } + } +} diff --git a/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-start-id/_getBatchStartId.tree b/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-start-id/_getBatchStartId.tree new file mode 100644 index 000000000..7e303ab46 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-start-id/_getBatchStartId.tree @@ -0,0 +1,6 @@ +_getBatchStartId(uint256 _batchID) +├── when `_batchID` doesn't exist + │ └── it should revert ✅ + └── when `_batchID` exists + └── it should return the starting tokenId for that batch ✅ +(note: all batches are assumed to be contiguous, i.e. start id of one batch is the end id of the previous batch) \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/batch-mint-metadata/set-base-uri/_setBaseURI.t.sol b/src/test/sdk/extension/upgradeable/batch-mint-metadata/set-base-uri/_setBaseURI.t.sol new file mode 100644 index 000000000..28a351d76 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/batch-mint-metadata/set-base-uri/_setBaseURI.t.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BatchMintMetadata } from "contracts/extension/upgradeable/BatchMintMetadata.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyBatchMintMetadataUpg is BatchMintMetadata { + function batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) external returns (uint256 nextTokenIdToMint, uint256 batchId) { + (nextTokenIdToMint, batchId) = _batchMintMetadata(_startId, _amountToMint, _baseURIForTokens); + } + + function setBaseURI(uint256 _batchId, string memory _baseURI) external { + _setBaseURI(_batchId, _baseURI); + } + + function freezeBaseURI(uint256 _batchId, bool _freeze) external { + _batchMintMetadataStorage().batchFrozen[_batchId] = _freeze; + } + + function getBaseURI(uint256 _batchId) external view returns (string memory) { + return _batchMintMetadataStorage().baseURI[_batchId]; + } +} + +contract UpgradeableBatchMintMetadata_SetBaseURI is ExtensionUtilTest { + MyBatchMintMetadataUpg internal ext; + string internal newBaseURI; + uint256 internal startId; + uint256[] internal batchIds; + uint256 internal indexToUpdate; + + event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); + + function setUp() public override { + super.setUp(); + + ext = new MyBatchMintMetadataUpg(); + + startId = 0; + // mint 5 batches + for (uint256 i = 0; i < 5; i++) { + uint256 amount = (i + 1) * 10; + uint256 batchId = startId + amount; + batchIds.push(batchId); + + (startId, ) = ext.batchMintMetadata(startId, amount, "ipfs://"); + ext.freezeBaseURI(batchId, true); + } + + indexToUpdate = 3; + newBaseURI = "ipfs://baseURI"; + } + + function test_setBaseURI_frozenBatchId() public { + vm.expectRevert("Batch frozen"); + ext.setBaseURI(batchIds[indexToUpdate], newBaseURI); + } + + modifier whenBatchIdNotFrozen() { + ext.freezeBaseURI(batchIds[indexToUpdate], false); + _; + } + + function test_setBaseURI() public whenBatchIdNotFrozen { + ext.setBaseURI(batchIds[indexToUpdate], newBaseURI); + + string memory _baseURI = ext.getBaseURI(batchIds[indexToUpdate]); + + assertEq(_baseURI, newBaseURI); + } + + function test_setBaseURI_event() public whenBatchIdNotFrozen { + vm.expectEmit(false, false, false, true); + emit BatchMetadataUpdate(batchIds[indexToUpdate - 1], batchIds[indexToUpdate]); + ext.setBaseURI(batchIds[indexToUpdate], newBaseURI); + } +} diff --git a/src/test/sdk/extension/upgradeable/batch-mint-metadata/set-base-uri/_setBaseURI.tree b/src/test/sdk/extension/upgradeable/batch-mint-metadata/set-base-uri/_setBaseURI.tree new file mode 100644 index 000000000..3df76f653 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/batch-mint-metadata/set-base-uri/_setBaseURI.tree @@ -0,0 +1,6 @@ +_setBaseURI(uint256 _batchId, string memory _baseURI) +├── when the `_batchId` is frozen + │ └── it should revert ✅ + └── when the `_batchId` is not frozen + └── it should map the `_batchId` to `_baseURI` param ✅ + └── it should emit BatchMetadataUpdate event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.t.sol b/src/test/sdk/extension/upgradeable/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.t.sol new file mode 100644 index 000000000..bc530cd78 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.t.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BurnToClaim, IBurnToClaim } from "contracts/extension/upgradeable/BurnToClaim.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyBurnToClaimUpg is BurnToClaim { + function burnTokensOnOrigin(address _tokenOwner, uint256 _tokenId, uint256 _quantity) public { + _burnTokensOnOrigin(_tokenOwner, _tokenId, _quantity); + } + + function _canSetBurnToClaim() internal view override returns (bool) { + return true; + } +} + +contract UpgradeableBurnToClaim_BurnTokensOnOrigin is ExtensionUtilTest { + MyBurnToClaimUpg internal ext; + Wallet internal tokenOwner; + uint256 internal tokenId; + uint256 internal quantity; + + function setUp() public override { + super.setUp(); + + ext = new MyBurnToClaimUpg(); + + tokenOwner = getWallet(); + erc721.mint(address(tokenOwner), 10); + erc1155.mint(address(tokenOwner), 1, 10); + + erc721NonBurnable.mint(address(tokenOwner), 10); + erc1155NonBurnable.mint(address(tokenOwner), 1, 10); + + tokenOwner.setApprovalForAllERC721(address(erc721), address(ext), true); + tokenOwner.setApprovalForAllERC1155(address(erc1155), address(ext), true); + } + + // ================== + // ======= Test branch: token type is ERC721 + // ================== + + modifier whenNotBurnableERC721() { + ext.setBurnToClaimInfo( + IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc721NonBurnable), + tokenType: IBurnToClaim.TokenType.ERC721, + tokenId: 0, + mintPriceForNewToken: 0, + currency: address(erc20) + }) + ); + _; + } + + function test_burnTokensOnOrigin_ERC721_nonBurnable() public whenNotBurnableERC721 { + vm.expectRevert(); + ext.burnTokensOnOrigin(address(tokenOwner), tokenId, quantity); + } + + modifier whenBurnableERC721() { + ext.setBurnToClaimInfo( + IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc721), + tokenType: IBurnToClaim.TokenType.ERC721, + tokenId: 0, + mintPriceForNewToken: 0, + currency: address(erc20) + }) + ); + _; + } + + function test_burnTokensOnOrigin_ERC721() public whenBurnableERC721 { + ext.burnTokensOnOrigin(address(tokenOwner), tokenId, quantity); + + assertEq(erc721.balanceOf(address(tokenOwner)), 9); + + vm.expectRevert(); + erc721.ownerOf(tokenId); // token doesn't exist after burning + } + + // ================== + // ======= Test branch: token type is ERC71155 + // ================== + + modifier whenNotBurnableERC1155() { + ext.setBurnToClaimInfo( + IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc1155NonBurnable), + tokenType: IBurnToClaim.TokenType.ERC1155, + tokenId: 1, + mintPriceForNewToken: 0, + currency: address(erc20) + }) + ); + _; + } + + function test_burnTokensOnOrigin_ERC1155_nonBurnable() public whenNotBurnableERC1155 { + vm.expectRevert(); + ext.burnTokensOnOrigin(address(tokenOwner), tokenId, quantity); + } + + modifier whenBurnableERC1155() { + ext.setBurnToClaimInfo( + IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc1155), + tokenType: IBurnToClaim.TokenType.ERC1155, + tokenId: 1, + mintPriceForNewToken: 0, + currency: address(erc20) + }) + ); + _; + } + + function test_burnTokensOnOrigin_ERC1155() public whenBurnableERC1155 { + ext.burnTokensOnOrigin(address(tokenOwner), tokenId, quantity); + + assertEq(erc1155.balanceOf(address(tokenOwner), tokenId), 0); + } +} diff --git a/src/test/sdk/extension/upgradeable/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.tree b/src/test/sdk/extension/upgradeable/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.tree new file mode 100644 index 000000000..a2a3911ac --- /dev/null +++ b/src/test/sdk/extension/upgradeable/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.tree @@ -0,0 +1,15 @@ +_burnTokensOnOrigin( + address _tokenOwner, + uint256 _tokenId, + uint256 _quantity +) +├── when burn-to-claim info has token type ERC721 + ├── when the origin ERC721 contract is not burnable + │ └── it should revert ✅ + └── when the origin ERC721 contract is burnable + └── it should successfully burn the token with given tokenId for the token owner ✅ +├── when burn-to-claim info has token type ERC1155 + ├── when the origin ERC1155 contract is not burnable + │ └── it should revert ✅ + └── when the origin ERC1155 contract is burnable + └── it should successfully burn tokens with given tokenId and quantity for the token owner ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.t.sol b/src/test/sdk/extension/upgradeable/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.t.sol new file mode 100644 index 000000000..cb3cf0133 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.t.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BurnToClaim, IBurnToClaim } from "contracts/extension/upgradeable/BurnToClaim.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyBurnToClaimUpg is BurnToClaim { + bool condition; + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function _canSetBurnToClaim() internal view override returns (bool) { + return msg.sender == admin; + } +} + +contract UpgradeableBurnToClaim_SetBurnToClaimInfo is ExtensionUtilTest { + MyBurnToClaimUpg internal ext; + address internal admin; + address internal caller; + IBurnToClaim.BurnToClaimInfo internal info; + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + caller = getActor(1); + + ext = new MyBurnToClaimUpg(address(admin)); + info = IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(0), + tokenType: IBurnToClaim.TokenType.ERC721, + tokenId: 0, + mintPriceForNewToken: 0, + currency: address(0) + }); + } + + function test_setBurnToClaimInfo_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert("Not authorized."); + ext.setBurnToClaimInfo(info); + } + + modifier whenCallerAuthorized() { + caller = admin; + _; + } + + function test_setBurnToClaimInfo_invalidOriginContract_addressZero() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectRevert("Origin contract not set."); + ext.setBurnToClaimInfo(info); + } + + modifier whenValidOriginContract() { + info.originContractAddress = address(erc721); + _; + } + + function test_setBurnToClaimInfo_invalidCurrency_addressZero() public whenCallerAuthorized whenValidOriginContract { + vm.prank(address(caller)); + vm.expectRevert("Currency not set."); + ext.setBurnToClaimInfo(info); + } + + modifier whenValidCurrency() { + info.currency = address(erc20); + _; + } + + function test_setBurnToClaimInfo() public whenCallerAuthorized whenValidOriginContract whenValidCurrency { + vm.prank(address(caller)); + ext.setBurnToClaimInfo(info); + + IBurnToClaim.BurnToClaimInfo memory _info = ext.getBurnToClaimInfo(); + + assertEq(_info.originContractAddress, info.originContractAddress); + assertEq(_info.currency, info.currency); + } +} diff --git a/src/test/sdk/extension/upgradeable/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.tree b/src/test/sdk/extension/upgradeable/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.tree new file mode 100644 index 000000000..d6e347f5e --- /dev/null +++ b/src/test/sdk/extension/upgradeable/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.tree @@ -0,0 +1,11 @@ +setBurnToClaimInfo(BurnToClaimInfo calldata _burnToClaimInfo) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + ├── when input originContractAddress is address(0) + │ └── it should revert ✅ + └── when input originContractAddress is not address(0) + ├── when input currency is address(0) + │ └── it should revert ✅ + └── when input currency is not address(0) + └── it should save incoming struct values into burnToClaimInfo state ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.t.sol b/src/test/sdk/extension/upgradeable/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.t.sol new file mode 100644 index 000000000..030ea0bb2 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.t.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BurnToClaim, IBurnToClaim } from "contracts/extension/upgradeable/BurnToClaim.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyBurnToClaimUpg is BurnToClaim { + bool condition; + + function setCondition(bool _condition) external { + condition = _condition; + } + + function _canSetBurnToClaim() internal view override returns (bool) { + return condition; + } +} + +contract UpgradeableBurnToClaim_VerifyBurnToClaim is ExtensionUtilTest { + MyBurnToClaimUpg internal ext; + address internal tokenOwner; + uint256 internal tokenId; + uint256 internal quantity; + + function setUp() public override { + super.setUp(); + + ext = new MyBurnToClaimUpg(); + ext.setCondition(true); + + tokenOwner = getActor(1); + erc721.mint(address(tokenOwner), 10); + erc1155.mint(address(tokenOwner), 1, 10); + } + + function test_verifyBurnToClaim_infoNotSet() public { + vm.expectRevert(); + ext.verifyBurnToClaim(tokenOwner, tokenId, 1); + } + + // ================== + // ======= Test branch: token type is ERC721 + // ================== + + modifier whenBurnToClaimInfoSetERC721() { + ext.setBurnToClaimInfo( + IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc721), + tokenType: IBurnToClaim.TokenType.ERC721, + tokenId: 0, + mintPriceForNewToken: 0, + currency: address(erc20) + }) + ); + IBurnToClaim.BurnToClaimInfo memory info = ext.getBurnToClaimInfo(); + _; + } + + function test_verifyBurnToClaim_ERC721_quantity_not_1() public whenBurnToClaimInfoSetERC721 { + quantity = 10; + vm.expectRevert("Invalid amount"); + ext.verifyBurnToClaim(tokenOwner, tokenId, quantity); + } + + modifier whenQuantityParamisOne() { + quantity = 1; + _; + } + + function test_verifyBurnToClaim_ERC721_notOwnerOfToken() + public + whenBurnToClaimInfoSetERC721 + whenQuantityParamisOne + { + vm.expectRevert("!Owner"); + ext.verifyBurnToClaim(address(0x123), tokenId, quantity); // random address as owner + } + + modifier whenCorrectOwner() { + _; + } + + function test_verifyBurnToClaim_ERC721() + public + whenBurnToClaimInfoSetERC721 + whenQuantityParamisOne + whenCorrectOwner + { + ext.verifyBurnToClaim(tokenOwner, tokenId, quantity); + } + + // ================== + // ======= Test branch: token type is ERC1155 + // ================== + + modifier whenBurnToClaimInfoSetERC1155() { + ext.setBurnToClaimInfo( + IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc1155), + tokenType: IBurnToClaim.TokenType.ERC1155, + tokenId: 1, + mintPriceForNewToken: 0, + currency: address(erc20) + }) + ); + IBurnToClaim.BurnToClaimInfo memory info = ext.getBurnToClaimInfo(); + _; + } + + function test_verifyBurnToClaim_ERC1155_invalidTokenId() public whenBurnToClaimInfoSetERC1155 { + vm.expectRevert("Invalid token Id"); + ext.verifyBurnToClaim(tokenOwner, tokenId, quantity); // the tokenId here is 0, but eligible one is set as 1 above + } + + modifier whenCorrectTokenId() { + tokenId = 1; + _; + } + + function test_verifyBurnToClaim_ERC1155_balanceLessThanQuantity() + public + whenBurnToClaimInfoSetERC1155 + whenCorrectTokenId + { + quantity = 100; + vm.expectRevert("!Balance"); + ext.verifyBurnToClaim(tokenOwner, tokenId, quantity); // available balance is 10 + } + + modifier whenSufficientBalance() { + quantity = 10; + _; + } + + function test_verifyBurnToClaim_ERC1155() + public + whenBurnToClaimInfoSetERC1155 + whenCorrectTokenId + whenSufficientBalance + { + ext.verifyBurnToClaim(tokenOwner, tokenId, quantity); + } +} diff --git a/src/test/sdk/extension/upgradeable/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.tree b/src/test/sdk/extension/upgradeable/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.tree new file mode 100644 index 000000000..ffc4dba9d --- /dev/null +++ b/src/test/sdk/extension/upgradeable/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.tree @@ -0,0 +1,23 @@ +verifyBurnToClaim( + address tokenOwner, + uint256 tokenId, + uint256 quantity +) +├── when burn-to-claim info is not set + │ └── it should revert ✅ + └── when burn-to-claim info is set, with token type ERC721 + │ ├── when quantity param is not 1 + │ │ └── it should revert ✅ + │ └── when quantity param is 1 + │ ├── when token owner param is not the actual token owner + │ │ └── it should revert ✅ + │ └── when token owner param is the correct token owner + │ │ └── execution completes -- exit function ✅ + └── when burn-to-claim info is set, with token type ERC1155 + ├── when tokenId param doesn't match eligible tokenId + │ └── it should revert ✅ + └── when tokenId param matches eligible tokenId + ├── when token owner has balance less than quantity param + │ └── it should revert ✅ + └── when token owner has balance greater than or equal to quantity param + └── execution completes -- exit function ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/contract-metadata/set-contract-uri/setContractURI.t.sol b/src/test/sdk/extension/upgradeable/contract-metadata/set-contract-uri/setContractURI.t.sol new file mode 100644 index 000000000..0be6c9030 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/contract-metadata/set-contract-uri/setContractURI.t.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { ContractMetadata, IContractMetadata } from "contracts/extension/upgradeable/ContractMetadata.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyContractMetadataUpg is ContractMetadata { + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function _canSetContractURI() internal view override returns (bool) { + return msg.sender == admin; + } +} + +contract UpgradeableContractMetadata_SetContractURI is ExtensionUtilTest { + MyContractMetadataUpg internal ext; + address internal admin; + address internal caller; + string internal uri; + + event ContractURIUpdated(string prevURI, string newURI); + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + caller = getActor(1); + uri = "ipfs://newUri"; + + ext = new MyContractMetadataUpg(address(admin)); + } + + function test_setContractURI_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert("Not authorized"); + ext.setContractURI(uri); + } + + modifier whenCallerAuthorized() { + caller = admin; + _; + } + + function test_setContractURI() public whenCallerAuthorized { + vm.prank(address(caller)); + ext.setContractURI(uri); + + string memory _updatedUri = ext.contractURI(); + assertEq(_updatedUri, uri); + } + + function test_setContractURI_event() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectEmit(false, false, false, true); + emit ContractURIUpdated("", uri); + ext.setContractURI(uri); + } +} diff --git a/src/test/sdk/extension/upgradeable/contract-metadata/set-contract-uri/setContractURI.tree b/src/test/sdk/extension/upgradeable/contract-metadata/set-contract-uri/setContractURI.tree new file mode 100644 index 000000000..e626d76e4 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/contract-metadata/set-contract-uri/setContractURI.tree @@ -0,0 +1,6 @@ +setContractURI(string memory uri) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── it should update contract URI to the new URI value ✅ + └── it should emit ContractURIUpdated event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/delayed-reveal/get-reveal-uri/getRevealURI.t.sol b/src/test/sdk/extension/upgradeable/delayed-reveal/get-reveal-uri/getRevealURI.t.sol new file mode 100644 index 000000000..62a10b844 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/delayed-reveal/get-reveal-uri/getRevealURI.t.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { DelayedReveal, IDelayedReveal } from "contracts/extension/upgradeable/DelayedReveal.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyDelayedRevealUpg is DelayedReveal { + function setEncryptedData(uint256 _batchId, bytes memory _encryptedData) external { + _setEncryptedData(_batchId, _encryptedData); + } + + function reveal(uint256 identifier, bytes calldata key) external returns (string memory revealedURI) {} +} + +contract UpgradeableDelayedReveal_GetRevealURI is ExtensionUtilTest { + MyDelayedRevealUpg internal ext; + string internal originalURI; + bytes internal encryptionKey; + bytes internal encryptedURI; + bytes internal encryptedData; + uint256 internal batchId; + bytes32 internal provenanceHash; + + function setUp() public override { + super.setUp(); + + ext = new MyDelayedRevealUpg(); + originalURI = "ipfs://original"; + encryptionKey = "key123"; + batchId = 1; + + provenanceHash = keccak256(abi.encodePacked(originalURI, encryptionKey, block.chainid)); + encryptedURI = ext.encryptDecrypt(bytes(originalURI), encryptionKey); + encryptedData = abi.encode(encryptedURI, provenanceHash); + } + + function test_getRevealURI_encryptedDataNotSet() public { + vm.expectRevert("Nothing to reveal"); + ext.getRevealURI(batchId, encryptionKey); + } + + modifier whenEncryptedDataIsSet() { + ext.setEncryptedData(batchId, encryptedData); + _; + } + + function test_getRevealURI_incorrectKey() public whenEncryptedDataIsSet { + bytes memory incorrectKey = "incorrect key"; + + vm.expectRevert("Incorrect key"); + ext.getRevealURI(batchId, incorrectKey); + } + + modifier whenCorrectKey() { + _; + } + + function test_getRevealURI() public whenEncryptedDataIsSet whenCorrectKey { + string memory revealedURI = ext.getRevealURI(batchId, encryptionKey); + + assertEq(originalURI, revealedURI); + } +} diff --git a/src/test/sdk/extension/upgradeable/delayed-reveal/get-reveal-uri/getRevealURI.tree b/src/test/sdk/extension/upgradeable/delayed-reveal/get-reveal-uri/getRevealURI.tree new file mode 100644 index 000000000..acb580468 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/delayed-reveal/get-reveal-uri/getRevealURI.tree @@ -0,0 +1,8 @@ +getRevealURI(uint256 _batchId, bytes calldata _key) +├── when there is no encrypted data set for the given batch id + │ └── it should revert ✅ + └── when there is an associated encrypted data present for the given batch id + ├── when the encryption key provided is incorrect + │ └── it should revert ✅ + └── when the encryption key provided is correct + └── it should correctly decrypt and return the original URI ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/delayed-reveal/set-encrypted-data/_setEncryptedData.t.sol b/src/test/sdk/extension/upgradeable/delayed-reveal/set-encrypted-data/_setEncryptedData.t.sol new file mode 100644 index 000000000..a499bad5e --- /dev/null +++ b/src/test/sdk/extension/upgradeable/delayed-reveal/set-encrypted-data/_setEncryptedData.t.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { DelayedReveal, IDelayedReveal } from "contracts/extension/upgradeable/DelayedReveal.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyDelayedRevealUpg is DelayedReveal { + function setEncryptedData(uint256 _batchId, bytes memory _encryptedData) external { + _setEncryptedData(_batchId, _encryptedData); + } + + function reveal(uint256 identifier, bytes calldata key) external returns (string memory revealedURI) {} +} + +contract UpgradeableDelayedReveal_SetEncryptedData is ExtensionUtilTest { + MyDelayedRevealUpg internal ext; + uint256 internal batchId; + bytes internal data; + + function setUp() public override { + super.setUp(); + + ext = new MyDelayedRevealUpg(); + batchId = 1; + data = "test"; + } + + function test_setEncryptedData() public { + ext.setEncryptedData(batchId, data); + + assertEq(true, ext.isEncryptedBatch(batchId)); + assertEq(ext.encryptedData(batchId), data); + } +} diff --git a/src/test/sdk/extension/upgradeable/delayed-reveal/set-encrypted-data/_setEncryptedData.tree b/src/test/sdk/extension/upgradeable/delayed-reveal/set-encrypted-data/_setEncryptedData.tree new file mode 100644 index 000000000..68f99a2c8 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/delayed-reveal/set-encrypted-data/_setEncryptedData.tree @@ -0,0 +1,3 @@ +_setEncryptedData(uint256 _batchId, bytes memory _encryptedData) +├── it should store input bytes data for the given batch id param ✅ +├── isEncryptedBatch should return true for this batch id ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/drop/claim/claim.t.sol b/src/test/sdk/extension/upgradeable/drop/claim/claim.t.sol new file mode 100644 index 000000000..ca65c8830 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/drop/claim/claim.t.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Drop, IDrop, IClaimConditionMultiPhase, IClaimCondition } from "contracts/extension/upgradeable/Drop.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyDropUpg is Drop { + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override {} + + function _transferTokensOnClaim( + address _to, + uint256 _quantityBeingClaimed + ) internal override returns (uint256 startTokenId) {} + + function _canSetClaimConditions() internal view override returns (bool) { + return true; + } + + function verifyClaim( + uint256 _conditionId, + address _claimer, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof + ) public view override returns (bool isOverride) {} +} + +contract UpgradeableDrop_Claim is ExtensionUtilTest { + MyDropUpg internal ext; + + address internal _claimer; + uint256 internal _quantity; + address internal _currency; + uint256 internal _pricePerToken; + IDrop.AllowlistProof internal _allowlistProof; + + IClaimCondition.ClaimCondition[] internal claimConditions; + + function setUp() public override { + super.setUp(); + + ext = new MyDropUpg(); + _claimer = getActor(1); + _quantity = 10; + } + + function _setConditionsState() public { + // values here are not important (except timestamp), since we won't be verifying claim params + + claimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 0, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + ext.setClaimConditions(claimConditions, false); + } + + function test_claim_noConditionsSet() public { + vm.expectRevert("!CONDITION."); + ext.claim(_claimer, _quantity, _currency, _pricePerToken, _allowlistProof, ""); + } + + modifier whenConditionsAreSet() { + _setConditionsState(); + _; + } + + function test_claim() public whenConditionsAreSet { + // claim + vm.prank(_claimer); + ext.claim(_claimer, _quantity, _currency, _pricePerToken, _allowlistProof, ""); + + uint256 supplyClaimedByWallet_1 = ext.getSupplyClaimedByWallet(0, _claimer); + uint256 supplyClaimed_1 = (ext.getClaimConditionById(0)).supplyClaimed; + + // claim again + vm.prank(_claimer); + ext.claim(_claimer, _quantity, _currency, _pricePerToken, _allowlistProof, ""); + + uint256 supplyClaimedByWallet_2 = ext.getSupplyClaimedByWallet(0, _claimer); + uint256 supplyClaimed_2 = (ext.getClaimConditionById(0)).supplyClaimed; + + // check state + assertEq(supplyClaimedByWallet_1, _quantity); + assertEq(supplyClaimedByWallet_2, supplyClaimedByWallet_1 + _quantity); + + assertEq(supplyClaimed_1, _quantity); + assertEq(supplyClaimed_2, supplyClaimed_1 + _quantity); + } +} diff --git a/src/test/sdk/extension/upgradeable/drop/claim/claim.tree b/src/test/sdk/extension/upgradeable/drop/claim/claim.tree new file mode 100644 index 000000000..4ca1d3187 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/drop/claim/claim.tree @@ -0,0 +1,15 @@ +claim( + address _receiver, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof, + bytes memory _data +) +├── when no active condition + │ └── it should revert ✅ + └── when there's an active condition + └── it should increase the supplyClaimed for that condition by quantity param input ✅ + └── it should increase the supplyClaimedByWallet for that condition and msg.sender by quantity param input ✅ + +(Note: verifyClaim function has been tested separately, and hence not being tested here) \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/drop/get-active-claim-condition-id/getActiveClaimConditionId.t.sol b/src/test/sdk/extension/upgradeable/drop/get-active-claim-condition-id/getActiveClaimConditionId.t.sol new file mode 100644 index 000000000..3fdd2dce2 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/drop/get-active-claim-condition-id/getActiveClaimConditionId.t.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Drop, IDrop, IClaimConditionMultiPhase, IClaimCondition } from "contracts/extension/upgradeable/Drop.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyDropUpg is Drop { + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override {} + + function _transferTokensOnClaim( + address _to, + uint256 _quantityBeingClaimed + ) internal override returns (uint256 startTokenId) {} + + function _canSetClaimConditions() internal view override returns (bool) { + return true; + } +} + +contract UpgradeableDrop_GetActiveClaimConditionId is ExtensionUtilTest { + MyDropUpg internal ext; + + IClaimCondition.ClaimCondition[] internal claimConditions; + + function setUp() public override { + super.setUp(); + + ext = new MyDropUpg(); + } + + function _setConditionsState() public { + claimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 100, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + claimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 200, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + claimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 300, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + ext.setClaimConditions(claimConditions, false); + } + + function test_getActiveClaimConditionId_noConditionsSet() public { + vm.expectRevert("!CONDITION."); + ext.getActiveClaimConditionId(); + } + + modifier whenConditionsAreSet() { + _setConditionsState(); + _; + } + + function test_getActiveClaimConditionId_noActiveCondition() public whenConditionsAreSet { + vm.expectRevert("!CONDITION."); + ext.getActiveClaimConditionId(); + } + + modifier whenActiveConditions() { + _; + } + + function test_getActiveClaimConditionId_activeConditions() public whenConditionsAreSet whenActiveConditions { + vm.warp(claimConditions[0].startTimestamp); + + uint256 id = ext.getActiveClaimConditionId(); + assertEq(id, 0); + + vm.warp(claimConditions[1].startTimestamp); + + id = ext.getActiveClaimConditionId(); + assertEq(id, 1); + + vm.warp(claimConditions[2].startTimestamp); + + id = ext.getActiveClaimConditionId(); + assertEq(id, 2); + } +} diff --git a/src/test/sdk/extension/upgradeable/drop/get-active-claim-condition-id/getActiveClaimConditionId.tree b/src/test/sdk/extension/upgradeable/drop/get-active-claim-condition-id/getActiveClaimConditionId.tree new file mode 100644 index 000000000..8b8a94d99 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/drop/get-active-claim-condition-id/getActiveClaimConditionId.tree @@ -0,0 +1,8 @@ +getActiveClaimConditionId() +├── when no conditions are set + │ └── it should revert ✅ + └── when condition(s) are set + ├── when no active condition, i.e. start timestamps of all conditions greater than block timestamp + │ └── it should revert ✅ + └── when conditions active, i.e. start timestamps at least one condition is less than or equal to the block timestamp + └── it should return the latest active claim condition id (i.e. with highest start timestamp among those active) ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/drop/set-claim-conditions/setClaimConditions.t.sol b/src/test/sdk/extension/upgradeable/drop/set-claim-conditions/setClaimConditions.t.sol new file mode 100644 index 000000000..cd96e3970 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/drop/set-claim-conditions/setClaimConditions.t.sol @@ -0,0 +1,379 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Drop, IDrop, IClaimConditionMultiPhase, IClaimCondition } from "contracts/extension/upgradeable/Drop.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyDropUpg is Drop { + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override {} + + function _transferTokensOnClaim( + address _to, + uint256 _quantityBeingClaimed + ) internal override returns (uint256 startTokenId) {} + + function _canSetClaimConditions() internal view override returns (bool) { + return msg.sender == admin; + } + + /** + * note: the functions below are dummy functions for test purposes, + * to directly access and set/reset state without going through the actual functions and checks + */ + + function setCondition(ClaimCondition calldata condition, uint256 _conditionId) public { + _dropStorage().claimCondition.conditions[_conditionId] = condition; + } + + function setSupplyClaimedForCondition(uint256 _conditionId, uint256 _supplyClaimed) public { + _dropStorage().claimCondition.conditions[_conditionId].supplyClaimed = _supplyClaimed; + } +} + +contract UpgradeableDrop_SetClaimConditions is ExtensionUtilTest { + MyDropUpg internal ext; + address internal admin; + + IClaimCondition.ClaimCondition[] internal newClaimConditions; + IClaimCondition.ClaimCondition[] internal oldClaimConditions; + + event ClaimConditionsUpdated(IClaimCondition.ClaimCondition[] claimConditions, bool resetEligibility); + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + ext = new MyDropUpg(admin); + + _setOldConditionsState(); + + newClaimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 100, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + newClaimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 200, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + } + + function _setOldConditionsState() public { + oldClaimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 10, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + oldClaimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 20, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + oldClaimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 30, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + vm.prank(admin); + ext.setClaimConditions(oldClaimConditions, false); + (, uint256 count) = ext.claimCondition(); + assertEq(count, oldClaimConditions.length); + + ext.setSupplyClaimedForCondition(0, 5); + ext.setSupplyClaimedForCondition(0, 20); + ext.setSupplyClaimedForCondition(0, 100); + } + + function test_setClaimConditions_notAuthorized() public { + vm.expectRevert("Not authorized"); + ext.setClaimConditions(newClaimConditions, false); + + vm.expectRevert("Not authorized"); + ext.setClaimConditions(newClaimConditions, true); + } + + modifier whenCallerAuthorized() { + vm.startPrank(admin); + _; + vm.stopPrank(); + } + + function test_setClaimConditions_incorrectStartTimestamps() public whenCallerAuthorized { + // reverse the order of timestamps + newClaimConditions[0].startTimestamp = newClaimConditions[1].startTimestamp + 100; + + vm.expectRevert(bytes("ST")); + ext.setClaimConditions(newClaimConditions, false); + + vm.expectRevert(bytes("ST")); + ext.setClaimConditions(newClaimConditions, true); + } + + modifier whenCorrectTimestamps() { + _; + } + + // ================== + // ======= Test branch: claim eligibility reset + // ================== + + function test_setClaimConditions_resetEligibility_startIndex() public whenCallerAuthorized whenCorrectTimestamps { + (, uint256 oldCount) = ext.claimCondition(); + + ext.setClaimConditions(newClaimConditions, true); + + (uint256 newStartIndex, ) = ext.claimCondition(); + assertEq(newStartIndex, oldCount); + } + + function test_setClaimConditions_resetEligibility_conditionCount() + public + whenCallerAuthorized + whenCorrectTimestamps + { + (, uint256 oldCount) = ext.claimCondition(); + assertEq(oldCount, oldClaimConditions.length); + + ext.setClaimConditions(newClaimConditions, true); + + (uint256 newStartIndex, uint256 newCount) = ext.claimCondition(); + assertEq(newCount, newClaimConditions.length); + } + + function test_setClaimConditions_resetEligibility_conditionState() + public + whenCallerAuthorized + whenCorrectTimestamps + { + ext.setClaimConditions(newClaimConditions, true); + + (uint256 newStartIndex, uint256 newCount) = ext.claimCondition(); + + for (uint256 i = 0; i < newCount; i++) { + IClaimCondition.ClaimCondition memory _claimCondition = ext.getClaimConditionById(i + newStartIndex); + + assertEq(_claimCondition.startTimestamp, newClaimConditions[i].startTimestamp); + assertEq(_claimCondition.maxClaimableSupply, newClaimConditions[i].maxClaimableSupply); + assertEq(_claimCondition.supplyClaimed, 0); + } + } + + function test_setClaimConditions_resetEligibility_oldConditionsDeleted() + public + whenCallerAuthorized + whenCorrectTimestamps + { + (uint256 oldStartIndex, uint256 oldCount) = ext.claimCondition(); + assertEq(oldCount, oldClaimConditions.length); + + ext.setClaimConditions(newClaimConditions, true); + + for (uint256 i = 0; i < oldCount; i++) { + IClaimCondition.ClaimCondition memory _claimCondition = ext.getClaimConditionById(i + oldStartIndex); + + assertEq(_claimCondition.startTimestamp, 0); + assertEq(_claimCondition.maxClaimableSupply, 0); + assertEq(_claimCondition.supplyClaimed, 0); + assertEq(_claimCondition.quantityLimitPerWallet, 0); + assertEq(_claimCondition.merkleRoot, bytes32(0)); + assertEq(_claimCondition.pricePerToken, 0); + assertEq(_claimCondition.currency, address(0)); + assertEq(_claimCondition.metadata, ""); + } + } + + function test_setClaimConditions_resetEligibility_event() public whenCallerAuthorized whenCorrectTimestamps { + // TODO: fix/review event data check by setting last param true + vm.expectEmit(false, false, false, false); + emit ClaimConditionsUpdated(newClaimConditions, true); + ext.setClaimConditions(newClaimConditions, true); + } + + // ================== + // ======= Test branch: claim eligibility not reset + // ================== + + function test_setClaimConditions_noReset_maxClaimableLessThanClaimed() + public + whenCallerAuthorized + whenCorrectTimestamps + { + IClaimCondition.ClaimCondition memory _oldCondition = ext.getClaimConditionById(0); + + // set new maxClaimableSupply less than supplyClaimed of the old condition + newClaimConditions[0].maxClaimableSupply = _oldCondition.supplyClaimed - 1; + + vm.expectRevert("max supply claimed"); + ext.setClaimConditions(newClaimConditions, false); + } + + modifier whenMaxClaimableNotLessThanClaimed() { + _; + } + + function test_setClaimConditions_noReset_startIndex() + public + whenCallerAuthorized + whenCorrectTimestamps + whenMaxClaimableNotLessThanClaimed + { + (uint256 oldStartIndex, ) = ext.claimCondition(); + + ext.setClaimConditions(newClaimConditions, false); + + (uint256 newStartIndex, ) = ext.claimCondition(); + assertEq(newStartIndex, oldStartIndex); + } + + function test_setClaimConditions_noReset_conditionCount() + public + whenCallerAuthorized + whenCorrectTimestamps + whenMaxClaimableNotLessThanClaimed + { + (, uint256 oldCount) = ext.claimCondition(); + assertEq(oldCount, oldClaimConditions.length); + + ext.setClaimConditions(newClaimConditions, false); + + (uint256 newStartIndex, uint256 newCount) = ext.claimCondition(); + assertEq(newCount, newClaimConditions.length); + } + + function test_setClaimConditions_noReset_conditionState() + public + whenCallerAuthorized + whenCorrectTimestamps + whenMaxClaimableNotLessThanClaimed + { + (, uint256 oldCount) = ext.claimCondition(); + + // setting array size as this way to avoid out-of-bound error in the second loop + uint256 length = newClaimConditions.length > oldCount ? newClaimConditions.length : oldCount; + IClaimCondition.ClaimCondition[] memory _oldConditions = new IClaimCondition.ClaimCondition[](length); + + for (uint256 i = 0; i < oldCount; i++) { + _oldConditions[i] = ext.getClaimConditionById(i); + } + + ext.setClaimConditions(newClaimConditions, false); + + (uint256 newStartIndex, uint256 newCount) = ext.claimCondition(); + + for (uint256 i = 0; i < newCount; i++) { + IClaimCondition.ClaimCondition memory _claimCondition = ext.getClaimConditionById(i + newStartIndex); + + assertEq(_claimCondition.startTimestamp, newClaimConditions[i].startTimestamp); + assertEq(_claimCondition.maxClaimableSupply, newClaimConditions[i].maxClaimableSupply); + assertEq(_claimCondition.supplyClaimed, _oldConditions[i].supplyClaimed); + } + } + + function test_setClaimConditions_resetEligibility_oldConditionsDeletedOrReplaced() + public + whenCallerAuthorized + whenCorrectTimestamps + whenMaxClaimableNotLessThanClaimed + { + (uint256 oldStartIndex, uint256 oldCount) = ext.claimCondition(); + assertEq(oldCount, oldClaimConditions.length); + + ext.setClaimConditions(newClaimConditions, false); + (, uint256 newCount) = ext.claimCondition(); + + for (uint256 i = 0; i < oldCount; i++) { + IClaimCondition.ClaimCondition memory _claimCondition = ext.getClaimConditionById(i + oldStartIndex); + + if (i >= newCount) { + // case where deleted + + assertEq(_claimCondition.startTimestamp, 0); + assertEq(_claimCondition.maxClaimableSupply, 0); + assertEq(_claimCondition.supplyClaimed, 0); + assertEq(_claimCondition.quantityLimitPerWallet, 0); + assertEq(_claimCondition.merkleRoot, bytes32(0)); + assertEq(_claimCondition.pricePerToken, 0); + assertEq(_claimCondition.currency, address(0)); + assertEq(_claimCondition.metadata, ""); + } else { + // case where replaced + + // supply claimed should be same as old condition, hence not checked below + + assertEq(_claimCondition.startTimestamp, newClaimConditions[i].startTimestamp); + assertEq(_claimCondition.maxClaimableSupply, newClaimConditions[i].maxClaimableSupply); + assertEq(_claimCondition.quantityLimitPerWallet, newClaimConditions[i].quantityLimitPerWallet); + assertEq(_claimCondition.merkleRoot, newClaimConditions[i].merkleRoot); + assertEq(_claimCondition.pricePerToken, newClaimConditions[i].pricePerToken); + assertEq(_claimCondition.currency, newClaimConditions[i].currency); + assertEq(_claimCondition.metadata, newClaimConditions[i].metadata); + } + } + } + + function test_setClaimConditions_noReset_event() + public + whenCallerAuthorized + whenCorrectTimestamps + whenMaxClaimableNotLessThanClaimed + { + // TODO: fix/review event data check by setting last param true + vm.expectEmit(false, false, false, false); + emit ClaimConditionsUpdated(newClaimConditions, false); + ext.setClaimConditions(newClaimConditions, false); + } +} diff --git a/src/test/sdk/extension/upgradeable/drop/set-claim-conditions/setClaimConditions.tree b/src/test/sdk/extension/upgradeable/drop/set-claim-conditions/setClaimConditions.tree new file mode 100644 index 000000000..aecd6b06f --- /dev/null +++ b/src/test/sdk/extension/upgradeable/drop/set-claim-conditions/setClaimConditions.tree @@ -0,0 +1,24 @@ +setClaimConditions(ClaimCondition[] calldata _conditions, bool _resetClaimEligibility) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + ├── when start timestamps of new conditions aren't in ascending order + │ └── it should revert ✅ + └── when start timestamps of new conditions are in ascending order + ├── when claim eligibility is reset + │ └── it should set new conditions start index as the count of old conditions ✅ + │ └── it should set claim condition count equal to the count of new conditions ✅ + │ └── it should correctly save all new conditions at right index ✅ + │ └── it should set supply claimed for each condition equal to 0 ✅ + │ └── it should delete all old conditions (i.e. all conditions with index less than new start index) ✅ + │ └── it should emit ClaimConditionsUpdated event ✅ + └── when claim eligibility is not reset + ├── when maxClaimableSupply of a new condition is less than supplyClaimed of the old condition (at that index) + │ └── it should revert ✅ + └── when maxClaimableSupply of a new condition is greater than or equal to supplyClaimed of the old condition (at that index) + └── it should set new conditions start index same as old start index ✅ + └── it should set claim condition count equal to the count of new conditions ✅ + └── it should correctly save all new conditions at right index ✅ + └── it should set supply claimed for each condition equal to what it was in old condition (at that index) ✅ + └── it should delete all old conditions with index exceeding new count, in case new count is less than previous count ✅ + └── it should emit ClaimConditionsUpdated event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/drop/verify-claim/verifyClaim.t.sol b/src/test/sdk/extension/upgradeable/drop/verify-claim/verifyClaim.t.sol new file mode 100644 index 000000000..3cd3372a1 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/drop/verify-claim/verifyClaim.t.sol @@ -0,0 +1,379 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Drop, IDrop, IClaimConditionMultiPhase, IClaimCondition } from "contracts/extension/upgradeable/Drop.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyDropUpg is Drop { + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override {} + + function _transferTokensOnClaim( + address _to, + uint256 _quantityBeingClaimed + ) internal override returns (uint256 startTokenId) {} + + function _canSetClaimConditions() internal view override returns (bool) { + return true; + } + + /** + * note: the functions below are dummy functions for test purposes, + * to directly access and set/reset state without going through the actual functions and checks + */ + + function setCondition(ClaimCondition calldata condition, uint256 _conditionId) public { + _dropStorage().claimCondition.conditions[_conditionId] = condition; + } + + function setSupplyClaimedByWallet(uint256 _conditionId, address _wallet, uint256 _supplyClaimed) public { + _dropStorage().claimCondition.supplyClaimedByWallet[_conditionId][_wallet] = _supplyClaimed; + } +} + +contract UpgradeableDrop_VerifyClaim is ExtensionUtilTest { + MyDropUpg internal ext; + + uint256 internal _conditionId; + address internal _claimer; + address internal _allowlistClaimer; + uint256 internal _quantity; + address internal _currency; + uint256 internal _pricePerToken; + IDrop.AllowlistProof internal _allowlistProof; + IDrop.AllowlistProof internal _allowlistProofEmpty; // will leave uninitialized + + IClaimCondition.ClaimCondition internal claimCondition; + IClaimCondition.ClaimCondition internal claimConditionWithAllowlist; + + function setUp() public override { + super.setUp(); + + ext = new MyDropUpg(); + + _claimer = getActor(1); + _allowlistClaimer = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + + // claim condition without allowlist + claimCondition = IClaimCondition.ClaimCondition({ + startTimestamp: 1000, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }); + + // claim condition with allowlist -- set defaults for now + claimConditionWithAllowlist = claimCondition; + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs( + 0, // default + type(uint256).max, // default + address(0) // default + ); + } + + function _setAllowlistAndProofs( + uint256 _quantity, + uint256 _price, + address _currency + ) internal returns (IDrop.AllowlistProof memory, bytes32) { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = Strings.toString(_quantity); + inputs[3] = Strings.toString(_price); + inputs[4] = Strings.toHexString(uint160(_currency)); + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + IDrop.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = _quantity; + alp.pricePerToken = _price; + alp.currency = address(_currency); + + return (alp, root); + } + + // ================== + // ======= Test branch: when no allowlist + // ================== + + function test_verifyClaim_noAllowlist_invalidCurrency() public { + ext.setCondition(claimCondition, _conditionId); + + vm.expectRevert("!PriceOrCurrency"); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + modifier whenValidCurrency_open() { + _currency = claimCondition.currency; + _; + } + + function test_verifyClaim_noAllowlist_invalidPrice() public whenValidCurrency_open { + ext.setCondition(claimCondition, _conditionId); + + vm.expectRevert("!PriceOrCurrency"); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + modifier whenValidPrice_open() { + _pricePerToken = claimCondition.pricePerToken; + _; + } + + function test_verifyClaim_noAllowlist_zeroQuantity() public whenValidCurrency_open whenValidPrice_open { + ext.setCondition(claimCondition, _conditionId); + + _quantity = 0; + vm.expectRevert(bytes("!Qty")); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + modifier whenNonZeroQuantity() { + _quantity = claimCondition.quantityLimitPerWallet + 1234; + _; + } + + function test_verifyClaim_noAllowlist_nonZeroInvalidQuantity() + public + whenValidCurrency_open + whenValidPrice_open + whenNonZeroQuantity + { + ext.setCondition(claimCondition, _conditionId); + ext.setSupplyClaimedByWallet(_conditionId, _claimer, claimCondition.quantityLimitPerWallet); + + vm.expectRevert(bytes("!Qty")); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + modifier whenValidQuantity_open() { + _quantity = 1; + _; + } + + function test_verifyClaim_noAllowlist_quantityMoreThanMaxClaimableSupply() + public + whenValidCurrency_open + whenValidPrice_open + whenNonZeroQuantity + whenValidQuantity_open + { + claimCondition.supplyClaimed = claimCondition.maxClaimableSupply; + ext.setCondition(claimCondition, _conditionId); + + vm.expectRevert("!MaxSupply"); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + modifier whenQuantityWithinMaxLimit() { + _; + } + + function test_verifyClaim_noAllowlist_beforeStartTimestamp() + public + whenValidCurrency_open + whenValidPrice_open + whenNonZeroQuantity + whenValidQuantity_open + whenQuantityWithinMaxLimit + { + ext.setCondition(claimCondition, _conditionId); + + vm.expectRevert("cant claim yet"); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + modifier whenValidTimestamp() { + vm.warp(claimCondition.startTimestamp); + _; + } + + function test_verifyClaim_noAllowlist() + public + whenValidCurrency_open + whenValidPrice_open + whenNonZeroQuantity + whenValidQuantity_open + whenQuantityWithinMaxLimit + whenValidTimestamp + { + ext.setCondition(claimCondition, _conditionId); + + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + // ================== + // ======= Test branch: allowlist but incorrect proof -- open limits should apply + // ================== + + function test_verifyClaim_incorrectProof_invalidCurrency() public { + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + vm.expectRevert("!PriceOrCurrency"); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + function test_verifyClaim_incorrectProof_invalidPrice() public whenValidCurrency_open { + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + vm.expectRevert("!PriceOrCurrency"); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + function test_verifyClaim_incorrectProof_zeroQuantity() public whenValidCurrency_open whenValidPrice_open { + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + _quantity = 0; + vm.expectRevert(bytes("!Qty")); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + function test_verifyClaim_incorrectProof_nonZeroInvalidQuantity() + public + whenValidCurrency_open + whenValidPrice_open + whenNonZeroQuantity + { + ext.setCondition(claimConditionWithAllowlist, _conditionId); + ext.setSupplyClaimedByWallet(_conditionId, _claimer, claimCondition.quantityLimitPerWallet); + + vm.expectRevert(bytes("!Qty")); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + function test_verifyClaim_incorrectProof() + public + whenValidCurrency_open + whenValidPrice_open + whenNonZeroQuantity + whenValidQuantity_open + whenQuantityWithinMaxLimit + whenValidTimestamp + { + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + // ================== + // ======= Test branch: allowlist with correct proof + // ================== + + function test_verifyClaim_allowlist_defaultPriceAndCurrency_invalidCurrencyParam() public { + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + vm.expectRevert("!PriceOrCurrency"); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist_defaultPriceNonDefaultCurrenct_invalidCurrencyParam() public { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs( + 0, // default + type(uint256).max, // default + address(weth) + ); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + vm.expectRevert("!PriceOrCurrency"); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist_nonDefaultPriceAndCurrency_invalidCurrencyParam() public { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs( + 0, // default + 2, + address(weth) + ); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + vm.expectRevert("!PriceOrCurrency"); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist_defaultQuantity_invalidQuantityParam() public { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs( + 0, // default + 2, + address(weth) + ); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + ext.setSupplyClaimedByWallet( + _conditionId, + _allowlistClaimer, + claimConditionWithAllowlist.quantityLimitPerWallet + ); + + _currency = address(weth); + _pricePerToken = 2; + _quantity = 1; + vm.expectRevert(bytes("!Qty")); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist_nonDefaultQuantity_invalidQuantityParam() public { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs(5, 2, address(weth)); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + ext.setSupplyClaimedByWallet(_conditionId, _allowlistClaimer, 5); + + _currency = address(weth); + _pricePerToken = 2; + _quantity = 1; + vm.expectRevert(bytes("!Qty")); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist_defaultPrice_invalidPriceParam() public { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs( + 5, + type(uint256).max, // default + address(weth) + ); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + _currency = address(weth); + _quantity = 1; + vm.expectRevert(bytes("!PriceOrCurrency")); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist_nonDefaultPrice_invalidPriceParam() public { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs(5, 1, address(weth)); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + _currency = address(weth); + _quantity = 1; + _pricePerToken = 2; + vm.expectRevert(bytes("!PriceOrCurrency")); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist() public whenQuantityWithinMaxLimit whenValidTimestamp { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs(5, 1, address(weth)); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + _currency = address(weth); + _quantity = 1; + _pricePerToken = 1; + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } +} diff --git a/src/test/sdk/extension/upgradeable/drop/verify-claim/verifyClaim.tree b/src/test/sdk/extension/upgradeable/drop/verify-claim/verifyClaim.tree new file mode 100644 index 000000000..ef84f4ef2 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/drop/verify-claim/verifyClaim.tree @@ -0,0 +1,67 @@ +verifyClaim( + uint256 conditionId, + address claimer, + uint256 quantity, + address currency, + uint256 pricePerToken, + AllowlistProof calldata allowlistProof +) +├── when no allowlist + └── when currency param not equal to open claim currency + │ └── it should revert ✅ + └── when currency param is equal to open claim currency + └── when pricePerToken param not equal to open claim price + │ └── it should revert ✅ + └── when pricePerToken param is equal to open claim price + └── when quantity param is 0 + │ └── it should revert ✅ + └── when quantity param is not 0 + └── when quantity param plus supply claimed is more than open claim limit + │ └── it should revert ✅ + └── when quantity param plus supply claimed is within open claim limit + └── when quantity param plus claimed supply is more than max claimable supply + │ └── it should revert ✅ + └── when quantity param plus claimed supply is within max claimable supply limit + └── when block timestamp is less than start timestamp of claim phase + │ └── it should revert ✅ + └── when block timestamp is greater than or equal to start timestamp of claim phase + └── execution completes -- exit function ✅ + +├── when allowlist but incorrect merkle proof + └── when currency param not equal to open claim currency + │ └── it should revert ✅ + └── when currency param is equal to open claim currency + └── when pricePerToken param not equal to open claim price + │ └── it should revert ✅ + └── when pricePerToken param is equal to open claim price + └── when quantity param is 0 + │ └── it should revert ✅ + └── when quantity param is not 0 + └── when quantity param plus supply claimed is more than open claim limit + │ └── it should revert ✅ + +├── when allowlist and correct merkle proof + └── when allowlist price is default max uint256 and allowlist currency is default address(0) + │ └── when currency param not equal to open claim currency + │ └── it should revert ✅ + └── when allowlist price is default max uint256 and allowlist currency is not default + │ └── when currency param not equal to open claim currency + │ └── it should revert ✅ + └── when allowlist price is not default and allowlist currency is default address(0) + │ └── when currency param not equal to open claim currency + │ └── it should revert ✅ + └── when allowlist price is not default and allowlist currency is not default + │ └── when currency param not equal to allowlist claim currency + │ └── it should revert ✅ + └── when allowlist quantity is default 0 + │ └── when nonzero quantity param plus supply claimed is more than open claim limit + │ └── it should revert ✅ + └── when allowlist quantity is not default + │ └── when nonzero quantity param plus supply claimed is more than allowlist claim limit + │ └── it should revert ✅ + └── when allowlist price is default max uint256 + │ └── when pricePerToken param not equal to open claim price + │ └── it should revert ✅ + └── when allowlist price is not default + │ └── when pricePerToken param not equal to allowlist claim price + │ └── it should revert ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/lazy-mint/lazy-mint/lazyMint.t.sol b/src/test/sdk/extension/upgradeable/lazy-mint/lazy-mint/lazyMint.t.sol new file mode 100644 index 000000000..97fec025d --- /dev/null +++ b/src/test/sdk/extension/upgradeable/lazy-mint/lazy-mint/lazyMint.t.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { LazyMint, BatchMintMetadata } from "contracts/extension/upgradeable/LazyMint.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyLazyMintUpg is LazyMint { + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function _canLazyMint() internal view override returns (bool) { + return msg.sender == admin; + } + + function getBaseURI(uint256 _tokenId) external view returns (string memory) { + return _getBaseURI(_tokenId); + } + + function getBatchStartId(uint256 _batchID) public view returns (uint256) { + return _getBatchStartId(_batchID); + } + + function nextTokenIdToMint() public view returns (uint256) { + return nextTokenIdToLazyMint(); + } +} + +contract UpgradeableLazyMint_LazyMint is ExtensionUtilTest { + MyLazyMintUpg internal ext; + uint256 internal startId; + uint256 internal amount; + uint256[] internal batchIds; + address internal admin; + address internal caller; + + event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI); + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + caller = getActor(1); + + ext = new MyLazyMintUpg(address(admin)); + + startId = 0; + // mint 5 batches + vm.startPrank(admin); + for (uint256 i = 0; i < 5; i++) { + uint256 _amount = (i + 1) * 10; + uint256 batchId = startId + _amount; + batchIds.push(batchId); + + string memory baseURI = Strings.toString(batchId); + startId = ext.lazyMint(_amount, baseURI, ""); + } + vm.stopPrank(); + } + + function test_lazyMint_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert("Not authorized"); + ext.lazyMint(amount, "", ""); + } + + modifier whenCallerAuthorized() { + caller = admin; + _; + } + + function test_lazyMint_zeroAmount() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectRevert("0 amt"); + ext.lazyMint(amount, "", ""); + } + + modifier whenAmountNotZero() { + amount = 50; + _; + } + + function test_lazyMint() public whenCallerAuthorized whenAmountNotZero { + // check previous state + uint256 _nextTokenIdToLazyMintOld = ext.nextTokenIdToMint(); + assertEq(_nextTokenIdToLazyMintOld, batchIds[4]); + + string memory baseURI = "ipfs://baseURI"; + + // lazy mint next batch + vm.prank(address(caller)); + uint256 _batchId = ext.lazyMint(amount, baseURI, ""); + + // check new state + uint256 _batchStartId = ext.getBatchStartId(_batchId); + assertEq(_nextTokenIdToLazyMintOld, _batchStartId); + assertEq(_batchId, _nextTokenIdToLazyMintOld + amount); + for (uint256 i = _batchStartId; i < _batchId; i++) { + assertEq(ext.getBaseURI(i), baseURI); + } + assertEq(ext.nextTokenIdToMint(), _nextTokenIdToLazyMintOld + amount); + } + + function test_lazyMint_event() public whenCallerAuthorized whenAmountNotZero { + string memory baseURI = "ipfs://baseURI"; + uint256 _nextTokenIdToLazyMintOld = ext.nextTokenIdToMint(); + + // lazy mint next batch + vm.prank(address(caller)); + vm.expectEmit(); + emit TokensLazyMinted(_nextTokenIdToLazyMintOld, _nextTokenIdToLazyMintOld + amount - 1, baseURI, ""); + ext.lazyMint(amount, baseURI, ""); + } +} diff --git a/src/test/sdk/extension/upgradeable/lazy-mint/lazy-mint/lazyMint.tree b/src/test/sdk/extension/upgradeable/lazy-mint/lazy-mint/lazyMint.tree new file mode 100644 index 000000000..daf177146 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/lazy-mint/lazy-mint/lazyMint.tree @@ -0,0 +1,17 @@ +lazyMint( + uint256 _amount, + string calldata _baseURIForTokens, + bytes calldata _data +) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── when amount to lazy mint is 0 + │ └── it should revert ✅ + └── when amount to lazy mint is not 0 + └── it should save the batch of tokens starting at `nextTokenIdToLazyMint` ✅ + └── it should store batch id equal to the sum of `nextTokenIdToLazyMint` and `_amount` ✅ + └── it should map the new batch id to `_baseURIForTokens` ✅ + └── it should increase `nextTokenIdToLazyMint` by `_amount` ✅ + └── it should return the new `batchId` ✅ + └── it should emit TokensLazyMinted event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/ownable/set-owner/setOwner.t.sol b/src/test/sdk/extension/upgradeable/ownable/set-owner/setOwner.t.sol new file mode 100644 index 000000000..6c938158f --- /dev/null +++ b/src/test/sdk/extension/upgradeable/ownable/set-owner/setOwner.t.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Ownable, IOwnable } from "contracts/extension/upgradeable/Ownable.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyOwnableUpg is Ownable { + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function _canSetOwner() internal view override returns (bool) { + return msg.sender == admin; + } +} + +contract UpgradeableOwnable_SetOwner is ExtensionUtilTest { + MyOwnableUpg internal ext; + address internal admin; + address internal caller; + address internal oldOwner; + address internal newOwner; + + event OwnerUpdated(address indexed prevOwner, address indexed newOwner); + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + caller = getActor(1); + + oldOwner = getActor(2); + newOwner = getActor(3); + + ext = new MyOwnableUpg(address(admin)); + + vm.prank(address(admin)); + ext.setOwner(oldOwner); + + assertEq(oldOwner, ext.owner()); + } + + function test_setOwner_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert("Not authorized"); + ext.setOwner(newOwner); + } + + modifier whenCallerAuthorized() { + caller = admin; + _; + } + + function test_setOwner() public whenCallerAuthorized { + vm.prank(address(caller)); + ext.setOwner(newOwner); + + assertEq(newOwner, ext.owner()); + } + + function test_setOwner_event() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectEmit(true, true, false, false); + emit OwnerUpdated(oldOwner, newOwner); + ext.setOwner(newOwner); + } +} diff --git a/src/test/sdk/extension/upgradeable/ownable/set-owner/setOwner.tree b/src/test/sdk/extension/upgradeable/ownable/set-owner/setOwner.tree new file mode 100644 index 000000000..9db2c0a70 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/ownable/set-owner/setOwner.tree @@ -0,0 +1,6 @@ +setContractURI(string memory uri) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── it should update owner by replacing old owner with the new owner input ✅ + └── it should emit OwnerUpdated event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.t.sol b/src/test/sdk/extension/upgradeable/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.t.sol new file mode 100644 index 000000000..e541be5ad --- /dev/null +++ b/src/test/sdk/extension/upgradeable/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.t.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Royalty, IRoyalty } from "contracts/extension/upgradeable/Royalty.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyRoyaltyUpg is Royalty { + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {} + + function _canSetRoyaltyInfo() internal view override returns (bool) { + return msg.sender == admin; + } +} + +contract UpgradeableRoyalty_SetDefaultRoyaltyInfo is ExtensionUtilTest { + MyRoyaltyUpg internal ext; + address internal admin; + address internal caller; + address internal defaultRoyaltyRecipient; + uint256 internal defaultRoyaltyBps; + + event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps); + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + caller = getActor(1); + defaultRoyaltyRecipient = getActor(2); + + ext = new MyRoyaltyUpg(address(admin)); + } + + function test_setDefaultRoyaltyInfo_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert("Not authorized"); + ext.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } + + modifier whenCallerAuthorized() { + caller = admin; + _; + } + + function test_setDefaultRoyaltyInfo_exceedMaxBps() public whenCallerAuthorized { + defaultRoyaltyBps = 10_001; + vm.prank(address(caller)); + vm.expectRevert("Exceeds max bps"); + ext.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } + + modifier whenNotExceedMaxBps() { + defaultRoyaltyBps = 500; + _; + } + + function test_setDefaultRoyaltyInfo() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + ext.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + + // get default royalty info + (address _recipient, uint16 _royaltyBps) = ext.getDefaultRoyaltyInfo(); + assertEq(_recipient, defaultRoyaltyRecipient); + assertEq(_royaltyBps, uint16(defaultRoyaltyBps)); + + // get royalty info for token + uint256 tokenId = 0; + (_recipient, _royaltyBps) = ext.getRoyaltyInfoForToken(tokenId); + assertEq(_recipient, defaultRoyaltyRecipient); + assertEq(_royaltyBps, uint16(defaultRoyaltyBps)); + + // royaltyInfo - ERC2981 + uint256 salePrice = 1000; + (address _royaltyRecipient, uint256 _royaltyAmount) = ext.royaltyInfo(tokenId, salePrice); + assertEq(_royaltyRecipient, defaultRoyaltyRecipient); + assertEq(_royaltyAmount, (salePrice * defaultRoyaltyBps) / 10_000); + } + + function test_setDefaultRoyaltyInfo_event() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + vm.expectEmit(true, false, false, true); + emit DefaultRoyalty(defaultRoyaltyRecipient, defaultRoyaltyBps); + ext.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } +} diff --git a/src/test/sdk/extension/upgradeable/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.tree b/src/test/sdk/extension/upgradeable/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.tree new file mode 100644 index 000000000..78a4312de --- /dev/null +++ b/src/test/sdk/extension/upgradeable/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.tree @@ -0,0 +1,11 @@ +setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + ├── when royalty bps input is greater than MAX_BPS + │ └── it should revert ✅ + └── when royalty bps input is less than or equal to MAX_BPS + └── it should update default royalty recipient ✅ + └── it should update default royalty bps ✅ + └── it should calculate royalty amount for a token-id based on default royalty info ✅ + └── it should emit DefaultRoyalty event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.t.sol b/src/test/sdk/extension/upgradeable/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.t.sol new file mode 100644 index 000000000..d28a142ee --- /dev/null +++ b/src/test/sdk/extension/upgradeable/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Royalty, IRoyalty } from "contracts/extension/upgradeable/Royalty.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyRoyaltyUpg is Royalty { + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {} + + function _canSetRoyaltyInfo() internal view override returns (bool) { + return msg.sender == admin; + } +} + +contract UpgradeableRoyalty_SetRoyaltyInfoForToken is ExtensionUtilTest { + MyRoyaltyUpg internal ext; + address internal admin; + address internal caller; + address internal defaultRoyaltyRecipient; + uint256 internal defaultRoyaltyBps; + + address internal royaltyRecipientForToken; + uint256 internal royaltyBpsForToken; + uint256 internal tokenId; + + event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, uint256 royaltyBps); + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + caller = getActor(1); + defaultRoyaltyRecipient = getActor(2); + royaltyRecipientForToken = getActor(3); + defaultRoyaltyBps = 500; + tokenId = 1; + + ext = new MyRoyaltyUpg(address(admin)); + + vm.prank(address(admin)); + ext.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } + + function test_setRoyaltyInfoForToken_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert("Not authorized"); + ext.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + } + + modifier whenCallerAuthorized() { + caller = admin; + _; + } + + function test_setRoyaltyInfoForToken_exceedMaxBps() public whenCallerAuthorized { + royaltyBpsForToken = 10_001; + vm.prank(address(caller)); + vm.expectRevert("Exceeds max bps"); + ext.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + } + + modifier whenNotExceedMaxBps() { + royaltyBpsForToken = 1000; + _; + } + + function test_setRoyaltyInfoForToken() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + ext.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + + // get default royalty info + (address _defaultRecipient, uint16 _defaultRoyaltyBps) = ext.getDefaultRoyaltyInfo(); + assertEq(_defaultRecipient, defaultRoyaltyRecipient); + assertEq(_defaultRoyaltyBps, uint16(defaultRoyaltyBps)); + + // get royalty info for token + (address _royaltyRecipientForToken, uint16 _royaltyBpsForToken) = ext.getRoyaltyInfoForToken(tokenId); + assertEq(_royaltyRecipientForToken, royaltyRecipientForToken); + assertEq(_royaltyBpsForToken, uint16(royaltyBpsForToken)); + + // royaltyInfo - ERC2981: calculate for default + uint256 salePrice = 1000; + (address _royaltyRecipient, uint256 _royaltyAmount) = ext.royaltyInfo(0, salePrice); + assertEq(_royaltyRecipient, defaultRoyaltyRecipient); + assertEq(_royaltyAmount, (salePrice * defaultRoyaltyBps) / 10_000); + + // royaltyInfo - ERC2981: calculate for specific tokenId we set the royalty info for + (_royaltyRecipient, _royaltyAmount) = ext.royaltyInfo(tokenId, salePrice); + assertEq(_royaltyRecipient, royaltyRecipientForToken); + assertEq(_royaltyAmount, (salePrice * royaltyBpsForToken) / 10_000); + } + + function test_setRoyaltyInfoForToken_event() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + vm.expectEmit(true, true, false, true); + emit RoyaltyForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + ext.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + } +} diff --git a/src/test/sdk/extension/upgradeable/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.tree b/src/test/sdk/extension/upgradeable/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.tree new file mode 100644 index 000000000..e28295634 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.tree @@ -0,0 +1,15 @@ +function setRoyaltyInfoForToken( + uint256 _tokenId, + address _recipient, + uint256 _bps +) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + ├── when royalty bps input is greater than MAX_BPS + │ └── it should revert ✅ + └── when royalty bps input is less than or equal to MAX_BPS + └── it should update default royalty recipient ✅ + └── it should update default royalty bps ✅ + └── it should calculate royalty amount for a token-id based on default royalty info ✅ + └── it should emit DefaultRoyalty event ✅ \ No newline at end of file diff --git a/src/test/smart-wallet/Account.t.sol b/src/test/smart-wallet/Account.t.sol new file mode 100644 index 000000000..2693be086 --- /dev/null +++ b/src/test/smart-wallet/Account.t.sol @@ -0,0 +1,813 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test utils +import "../utils/BaseTest.sol"; + +// Account Abstraction setup for smart wallets. +import { EntryPoint, IEntryPoint } from "contracts/prebuilts/account/utils/EntryPoint.sol"; +import { PackedUserOperation } from "contracts/prebuilts/account/interfaces/PackedUserOperation.sol"; + +// Target +import { IAccountPermissions } from "contracts/extension/interface/IAccountPermissions.sol"; +import { AccountFactory } from "contracts/prebuilts/account/non-upgradeable/AccountFactory.sol"; +import { Account as SimpleAccount } from "contracts/prebuilts/account/non-upgradeable/Account.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +/// @dev This is a dummy contract to test contract interactions with Account. +contract Number { + uint256 public num; + + function setNum(uint256 _num) public { + num = _num; + } + + function doubleNum() public { + num *= 2; + } + + function incrementNum() public { + num += 1; + } +} + +contract SimpleAccountTest is BaseTest { + // Target contracts + EntryPoint private entrypoint; + AccountFactory private accountFactory; + + // Mocks + Number internal numberContract; + + // Test params + uint256 private accountAdminPKey = 100; + address private accountAdmin; + + uint256 private accountSignerPKey = 200; + address private accountSigner; + + uint256 private nonSignerPKey = 300; + address private nonSigner; + + // UserOp terminology: `sender` is the smart wallet. + address private sender = 0x0df2C3523703d165Aa7fA1a552f3F0B56275DfC6; + address payable private beneficiary = payable(address(0x45654)); + + bytes32 private uidCache = bytes32("random uid"); + + event AccountCreated(address indexed account, address indexed accountAdmin); + + function _prepareSignature( + IAccountPermissions.SignerPermissionRequest memory _req + ) internal view returns (bytes32 typedDataHash) { + bytes32 typehashSignerPermissionRequest = keccak256( + "SignerPermissionRequest(address signer,uint8 isAdmin,address[] approvedTargets,uint256 nativeTokenLimitPerTransaction,uint128 permissionStartTimestamp,uint128 permissionEndTimestamp,uint128 reqValidityStartTimestamp,uint128 reqValidityEndTimestamp,bytes32 uid)" + ); + bytes32 nameHash = keccak256(bytes("Account")); + bytes32 versionHash = keccak256(bytes("1")); + bytes32 typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + bytes32 domainSeparator = keccak256(abi.encode(typehashEip712, nameHash, versionHash, block.chainid, sender)); + + bytes memory encodedRequestStart = abi.encode( + typehashSignerPermissionRequest, + _req.signer, + _req.isAdmin, + keccak256(abi.encodePacked(_req.approvedTargets)), + _req.nativeTokenLimitPerTransaction + ); + + bytes memory encodedRequestEnd = abi.encode( + _req.permissionStartTimestamp, + _req.permissionEndTimestamp, + _req.reqValidityStartTimestamp, + _req.reqValidityEndTimestamp, + _req.uid + ); + + bytes32 structHash = keccak256(bytes.concat(encodedRequestStart, encodedRequestEnd)); + typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + } + + function _signSignerPermissionRequest( + IAccountPermissions.SignerPermissionRequest memory _req + ) internal view returns (bytes memory signature) { + bytes32 typedDataHash = _prepareSignature(_req); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(accountAdminPKey, typedDataHash); + signature = abi.encodePacked(r, s, v); + } + + function _setupUserOp( + uint256 _signerPKey, + bytes memory _initCode, + bytes memory _callDataForEntrypoint + ) internal returns (PackedUserOperation[] memory ops) { + uint256 nonce = entrypoint.getNonce(sender, 0); + PackedUserOperation memory op; + + { + uint128 verificationGasLimit = 500_000; + uint128 callGasLimit = 500_000; + bytes32 packedGasLimits = (bytes32(uint256(verificationGasLimit)) << 128) | bytes32(uint256(callGasLimit)); + + // Get user op fields + op = PackedUserOperation({ + sender: sender, + nonce: nonce, + initCode: _initCode, + callData: _callDataForEntrypoint, + accountGasLimits: packedGasLimits, + preVerificationGas: 500_000, + gasFees: 0, + paymasterAndData: bytes(""), + signature: bytes("") + }); + } + + // Sign UserOp + bytes32 opHash = EntryPoint(entrypoint).getUserOpHash(op); + bytes32 msgHash = ECDSA.toEthSignedMessageHash(opHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signerPKey, msgHash); + bytes memory userOpSignature = abi.encodePacked(r, s, v); + + address recoveredSigner = ECDSA.recover(msgHash, v, r, s); + address expectedSigner = vm.addr(_signerPKey); + assertEq(recoveredSigner, expectedSigner); + + op.signature = userOpSignature; + + // Store UserOp + ops = new PackedUserOperation[](1); + ops[0] = op; + } + + function _setupUserOpWithSender( + bytes memory _initCode, + bytes memory _callDataForEntrypoint, + address _sender + ) internal returns (PackedUserOperation[] memory ops) { + uint256 nonce = entrypoint.getNonce(_sender, 0); + PackedUserOperation memory op; + + { + uint128 verificationGasLimit = 500_000; + uint128 callGasLimit = 500_000; + bytes32 packedGasLimits = (bytes32(uint256(verificationGasLimit)) << 128) | bytes32(uint256(callGasLimit)); + + // Get user op fields + op = PackedUserOperation({ + sender: _sender, + nonce: nonce, + initCode: _initCode, + callData: _callDataForEntrypoint, + accountGasLimits: packedGasLimits, + preVerificationGas: 500_000, + gasFees: 0, + paymasterAndData: bytes(""), + signature: bytes("") + }); + } + + // Sign UserOp + bytes32 opHash = EntryPoint(entrypoint).getUserOpHash(op); + bytes32 msgHash = ECDSA.toEthSignedMessageHash(opHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(accountAdminPKey, msgHash); + bytes memory userOpSignature = abi.encodePacked(r, s, v); + + address recoveredSigner = ECDSA.recover(msgHash, v, r, s); + address expectedSigner = vm.addr(accountAdminPKey); + assertEq(recoveredSigner, expectedSigner); + + op.signature = userOpSignature; + + // Store UserOp + ops = new PackedUserOperation[](1); + ops[0] = op; + } + + function _setupUserOpExecuteWithSender( + bytes memory _initCode, + address _target, + uint256 _value, + bytes memory _callData, + address _sender + ) internal returns (PackedUserOperation[] memory) { + bytes memory callDataForEntrypoint = abi.encodeWithSignature( + "execute(address,uint256,bytes)", + _target, + _value, + _callData + ); + + return _setupUserOpWithSender(_initCode, callDataForEntrypoint, _sender); + } + + function _setupUserOpExecute( + uint256 _signerPKey, + bytes memory _initCode, + address _target, + uint256 _value, + bytes memory _callData + ) internal returns (PackedUserOperation[] memory) { + bytes memory callDataForEntrypoint = abi.encodeWithSignature( + "execute(address,uint256,bytes)", + _target, + _value, + _callData + ); + + return _setupUserOp(_signerPKey, _initCode, callDataForEntrypoint); + } + + function _setupUserOpExecuteBatch( + uint256 _signerPKey, + bytes memory _initCode, + address[] memory _target, + uint256[] memory _value, + bytes[] memory _callData + ) internal returns (PackedUserOperation[] memory) { + bytes memory callDataForEntrypoint = abi.encodeWithSignature( + "executeBatch(address[],uint256[],bytes[])", + _target, + _value, + _callData + ); + + return _setupUserOp(_signerPKey, _initCode, callDataForEntrypoint); + } + + /// @dev Returns the salt used when deploying an Account. + function _generateSalt(address _admin, bytes memory _data) internal view virtual returns (bytes32) { + return keccak256(abi.encode(_admin, _data)); + } + + function setUp() public override { + super.setUp(); + + // Setup signers. + accountAdmin = vm.addr(accountAdminPKey); + vm.deal(accountAdmin, 100 ether); + + accountSigner = vm.addr(accountSignerPKey); + nonSigner = vm.addr(nonSignerPKey); + + // Setup contracts + entrypoint = new EntryPoint(); + // deploy account factory + accountFactory = new AccountFactory(deployer, IEntryPoint(payable(address(entrypoint)))); + // deploy dummy contract + numberContract = new Number(); + } + + /*/////////////////////////////////////////////////////////////// + Test: creating an account + //////////////////////////////////////////////////////////////*/ + + /// @dev Create an account by directly calling the factory. + function test_state_createAccount_viaFactory() public { + vm.expectEmit(true, true, false, true); + emit AccountCreated(sender, accountAdmin); + accountFactory.createAccount(accountAdmin, bytes("")); + + address[] memory allAccounts = accountFactory.getAllAccounts(); + assertEq(allAccounts.length, 1); + assertEq(allAccounts[0], sender); + } + + /// @dev Create an account via Entrypoint. + function test_state_createAccount_viaEntrypoint() public { + bytes memory initCallData = abi.encodeWithSignature("createAccount(address,bytes)", accountAdmin, bytes("")); + bytes memory initCode = abi.encodePacked(abi.encodePacked(address(accountFactory)), initCallData); + + PackedUserOperation[] memory userOpCreateAccount = _setupUserOpExecute( + accountAdminPKey, + initCode, + address(0), + 0, + bytes("") + ); + + vm.expectEmit(true, true, false, true); + emit AccountCreated(sender, accountAdmin); + EntryPoint(entrypoint).handleOps(userOpCreateAccount, beneficiary); + + address[] memory allAccounts = accountFactory.getAllAccounts(); + assertEq(allAccounts.length, 1); + assertEq(allAccounts[0], sender); + } + + /// @dev Try registering with factory with a contract not deployed by factory. + function test_revert_onRegister_nonFactoryChildContract() public { + vm.prank(address(0x12345)); + vm.expectRevert("AccountFactory: not an account."); + accountFactory.onRegister(_generateSalt(accountAdmin, "")); + } + + /// @dev Create more than one accounts with the same admin. + function test_state_createAccount_viaEntrypoint_multipleAccountSameAdmin() public { + uint256 start = 0; + uint256 end = 0; + + assertEq(accountFactory.totalAccounts(), 0); + + vm.expectRevert("BaseAccountFactory: invalid indices"); + address[] memory accs = accountFactory.getAccounts(start, end); + + uint256 amount = 100; + + for (uint256 i = 0; i < amount; i += 1) { + bytes memory initCallData = abi.encodeWithSignature( + "createAccount(address,bytes)", + accountAdmin, + bytes(abi.encode(i)) + ); + bytes memory initCode = abi.encodePacked(abi.encodePacked(address(accountFactory)), initCallData); + + address expectedSenderAddress = Clones.predictDeterministicAddress( + accountFactory.accountImplementation(), + _generateSalt(accountAdmin, bytes(abi.encode(i))), + address(accountFactory) + ); + + PackedUserOperation[] memory userOpCreateAccount = _setupUserOpExecuteWithSender( + initCode, + address(0), + 0, + bytes(abi.encode(i)), + expectedSenderAddress + ); + + vm.expectEmit(true, true, false, true); + emit AccountCreated(expectedSenderAddress, accountAdmin); + EntryPoint(entrypoint).handleOps(userOpCreateAccount, beneficiary); + } + + address[] memory allAccounts = accountFactory.getAllAccounts(); + assertEq(allAccounts.length, amount); + assertEq(accountFactory.totalAccounts(), amount); + + for (uint256 i = 0; i < amount; i += 1) { + assertEq( + allAccounts[i], + Clones.predictDeterministicAddress( + accountFactory.accountImplementation(), + _generateSalt(accountAdmin, bytes(abi.encode(i))), + address(accountFactory) + ) + ); + } + + start = 25; + end = 75; + + address[] memory accountsPaginatedOne = accountFactory.getAccounts(start, end); + + for (uint256 i = 0; i < (end - start); i += 1) { + assertEq( + accountsPaginatedOne[i], + Clones.predictDeterministicAddress( + accountFactory.accountImplementation(), + _generateSalt(accountAdmin, bytes(abi.encode(start + i))), + address(accountFactory) + ) + ); + } + + start = 0; + end = amount; + + address[] memory accountsPaginatedTwo = accountFactory.getAccounts(start, end); + + for (uint256 i = 0; i < (end - start); i += 1) { + assertEq( + accountsPaginatedTwo[i], + Clones.predictDeterministicAddress( + accountFactory.accountImplementation(), + _generateSalt(accountAdmin, bytes(abi.encode(start + i))), + address(accountFactory) + ) + ); + } + + start = 75; + end = 25; + + vm.expectRevert("BaseAccountFactory: invalid indices"); + accs = accountFactory.getAccounts(start, end); + + start = 25; + end = amount + 1; + + vm.expectRevert("BaseAccountFactory: invalid indices"); + accs = accountFactory.getAccounts(start, end); + } + + /*/////////////////////////////////////////////////////////////// + Test: performing a contract call + //////////////////////////////////////////////////////////////*/ + + function _setup_executeTransaction() internal { + bytes memory initCallData = abi.encodeWithSignature("createAccount(address,bytes)", accountAdmin, bytes("")); + bytes memory initCode = abi.encodePacked(abi.encodePacked(address(accountFactory)), initCallData); + + PackedUserOperation[] memory userOpCreateAccount = _setupUserOpExecute( + accountAdminPKey, + initCode, + address(0), + 0, + bytes("") + ); + + EntryPoint(entrypoint).handleOps(userOpCreateAccount, beneficiary); + } + + /// @dev Perform a state changing transaction directly via account. + function test_state_executeTransaction() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + assertEq(numberContract.num(), 0); + + vm.prank(accountAdmin); + SimpleAccount(payable(account)).execute( + address(numberContract), + 0, + abi.encodeWithSignature("setNum(uint256)", 42) + ); + + assertEq(numberContract.num(), 42); + } + + /// @dev Perform many state changing transactions in a batch directly via account. + function test_state_executeBatchTransaction() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + assertEq(numberContract.num(), 0); + + uint256 count = 3; + address[] memory targets = new address[](count); + uint256[] memory values = new uint256[](count); + bytes[] memory callData = new bytes[](count); + + for (uint256 i = 0; i < count; i += 1) { + targets[i] = address(numberContract); + values[i] = 0; + callData[i] = abi.encodeWithSignature("incrementNum()", i); + } + + vm.prank(accountAdmin); + SimpleAccount(payable(account)).executeBatch(targets, values, callData); + + assertEq(numberContract.num(), count); + } + + /// @dev Perform a state changing transaction via Entrypoint. + function test_state_executeTransaction_viaEntrypoint() public { + _setup_executeTransaction(); + + assertEq(numberContract.num(), 0); + + PackedUserOperation[] memory userOp = _setupUserOpExecute( + accountAdminPKey, + bytes(""), + address(numberContract), + 0, + abi.encodeWithSignature("setNum(uint256)", 42) + ); + + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + + assertEq(numberContract.num(), 42); + } + + /// @dev Perform many state changing transactions in a batch via Entrypoint. + function test_state_executeBatchTransaction_viaEntrypoint() public { + _setup_executeTransaction(); + + assertEq(numberContract.num(), 0); + + uint256 count = 3; + address[] memory targets = new address[](count); + uint256[] memory values = new uint256[](count); + bytes[] memory callData = new bytes[](count); + + for (uint256 i = 0; i < count; i += 1) { + targets[i] = address(numberContract); + values[i] = 0; + callData[i] = abi.encodeWithSignature("incrementNum()", i); + } + + PackedUserOperation[] memory userOp = _setupUserOpExecuteBatch( + accountAdminPKey, + bytes(""), + targets, + values, + callData + ); + + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + + assertEq(numberContract.num(), count); + } + + /// @dev Perform many state changing transactions in a batch via Entrypoint. + function test_state_executeBatchTransaction_viaAccountSigner() public { + _setup_executeTransaction(); + + assertEq(numberContract.num(), 0); + + uint256 count = 3; + address[] memory targets = new address[](count); + uint256[] memory values = new uint256[](count); + bytes[] memory callData = new bytes[](count); + + for (uint256 i = 0; i < count; i += 1) { + targets[i] = address(numberContract); + values[i] = 0; + callData[i] = abi.encodeWithSignature("incrementNum()", i); + } + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + address[] memory approvedTargets = new address[](1); + approvedTargets[0] = address(numberContract); + + IAccountPermissions.SignerPermissionRequest memory permissionsReq = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 0, + approvedTargets, + 1 ether, + 0, + type(uint128).max, + 0, + type(uint128).max, + uidCache + ); + + vm.prank(accountAdmin); + bytes memory sig = _signSignerPermissionRequest(permissionsReq); + SimpleAccount(payable(account)).setPermissionsForSigner(permissionsReq, sig); + + PackedUserOperation[] memory userOp = _setupUserOpExecuteBatch( + accountSignerPKey, + bytes(""), + targets, + values, + callData + ); + + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + + assertEq(numberContract.num(), count); + } + + /// @dev Perform a state changing transaction via Entrypoint and a SIGNER_ROLE holder. + function test_state_executeTransaction_viaAccountSigner() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + address[] memory approvedTargets = new address[](1); + approvedTargets[0] = address(numberContract); + + IAccountPermissions.SignerPermissionRequest memory permissionsReq = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 0, + approvedTargets, + 1 ether, + 0, + type(uint128).max, + 0, + type(uint128).max, + uidCache + ); + + vm.prank(accountAdmin); + bytes memory sig = _signSignerPermissionRequest(permissionsReq); + SimpleAccount(payable(account)).setPermissionsForSigner(permissionsReq, sig); + + assertEq(numberContract.num(), 0); + + PackedUserOperation[] memory userOp = _setupUserOpExecute( + accountSignerPKey, + bytes(""), + address(numberContract), + 0, + abi.encodeWithSignature("setNum(uint256)", 42) + ); + + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + + assertEq(numberContract.num(), 42); + } + + /// @dev Revert: perform a state changing transaction via Entrypoint without appropriate permissions. + function test_revert_executeTransaction_nonSigner_viaEntrypoint() public { + _setup_executeTransaction(); + + assertEq(numberContract.num(), 0); + + PackedUserOperation[] memory userOp = _setupUserOpExecute( + accountSignerPKey, + bytes(""), + address(numberContract), + 0, + abi.encodeWithSignature("setNum(uint256)", 42) + ); + + vm.expectRevert(); + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + } + + /// @dev Revert: non-admin performs a state changing transaction directly via account contract. + function test_revert_executeTransaction_nonSigner_viaDirectCall() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + address[] memory approvedTargets = new address[](1); + approvedTargets[0] = address(numberContract); + IAccountPermissions.SignerPermissionRequest memory permissionsReq = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 0, + approvedTargets, + 1 ether, + 0, + type(uint128).max, + 0, + type(uint128).max, + uidCache + ); + + vm.prank(accountAdmin); + bytes memory sig = _signSignerPermissionRequest(permissionsReq); + SimpleAccount(payable(account)).setPermissionsForSigner(permissionsReq, sig); + + assertEq(numberContract.num(), 0); + + vm.prank(accountSigner); + vm.expectRevert("Account: not admin or EntryPoint."); + SimpleAccount(payable(account)).execute( + address(numberContract), + 0, + abi.encodeWithSignature("setNum(uint256)", 42) + ); + } + + /*/////////////////////////////////////////////////////////////// + Test: receiving and sending native tokens + //////////////////////////////////////////////////////////////*/ + + /// @dev Send native tokens to an account. + function test_state_accountReceivesNativeTokens() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + assertEq(address(account).balance, 0); + + vm.prank(accountAdmin); + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory data) = payable(account).call{ value: 1000 }(""); + + // Silence warning: Return value of low-level calls not used. + (success, data) = (success, data); + + assertEq(address(account).balance, 1000); + } + + /// @dev Transfer native tokens out of an account. + function test_state_transferOutsNativeTokens() public { + _setup_executeTransaction(); + + uint256 value = 1000; + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + vm.prank(accountAdmin); + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory data) = payable(account).call{ value: value }(""); + assertEq(address(account).balance, value); + + // Silence warning: Return value of low-level calls not used. + (success, data) = (success, data); + + address recipient = address(0x3456); + + PackedUserOperation[] memory userOp = _setupUserOpExecute( + accountAdminPKey, + bytes(""), + recipient, + value, + bytes("") + ); + + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + assertEq(address(account).balance, 0); + assertEq(recipient.balance, value); + } + + /// @dev Add and remove a deposit for the account from the Entrypoint. + + function test_state_addAndWithdrawDeposit() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + assertEq(EntryPoint(entrypoint).balanceOf(account), 0); + + vm.prank(accountAdmin); + SimpleAccount(payable(account)).addDeposit{ value: 1000 }(); + assertEq(EntryPoint(entrypoint).balanceOf(account), 1000); + + vm.prank(accountAdmin); + SimpleAccount(payable(account)).withdrawDepositTo(payable(accountSigner), 500); + assertEq(EntryPoint(entrypoint).balanceOf(account), 500); + } + + /*/////////////////////////////////////////////////////////////// + Test: receiving ERC-721 and ERC-1155 NFTs + //////////////////////////////////////////////////////////////*/ + + /// @dev Send an ERC-721 NFT to an account. + function test_state_receiveERC721NFT() public { + _setup_executeTransaction(); + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + assertEq(erc721.balanceOf(account), 0); + + erc721.mint(account, 1); + + assertEq(erc721.balanceOf(account), 1); + } + + /// @dev Send an ERC-1155 NFT to an account. + function test_state_receiveERC1155NFT() public { + _setup_executeTransaction(); + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + assertEq(erc1155.balanceOf(account, 0), 0); + + erc1155.mint(account, 0, 1); + + assertEq(erc1155.balanceOf(account, 0), 1); + } + + /*/////////////////////////////////////////////////////////////// + Test: setting contract metadata + //////////////////////////////////////////////////////////////*/ + + /// @dev Set contract metadata via admin or entrypoint. + function test_state_contractMetadata() public { + _setup_executeTransaction(); + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + vm.prank(accountAdmin); + SimpleAccount(payable(account)).setContractURI("https://example.com"); + assertEq(SimpleAccount(payable(account)).contractURI(), "https://example.com"); + + PackedUserOperation[] memory userOp = _setupUserOpExecute( + accountAdminPKey, + bytes(""), + address(account), + 0, + abi.encodeWithSignature("setContractURI(string)", "https://thirdweb.com") + ); + + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + assertEq(SimpleAccount(payable(account)).contractURI(), "https://thirdweb.com"); + + address[] memory approvedTargets = new address[](0); + + IAccountPermissions.SignerPermissionRequest memory permissionsReq = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 0, + approvedTargets, + 1 ether, + 0, + type(uint128).max, + 0, + type(uint128).max, + uidCache + ); + + vm.prank(accountAdmin); + bytes memory sig = _signSignerPermissionRequest(permissionsReq); + SimpleAccount(payable(account)).setPermissionsForSigner(permissionsReq, sig); + + PackedUserOperation[] memory userOpViaSigner = _setupUserOpExecute( + accountSignerPKey, + bytes(""), + address(account), + 0, + abi.encodeWithSignature("setContractURI(string)", "https://thirdweb.com") + ); + + vm.expectRevert(); + EntryPoint(entrypoint).handleOps(userOpViaSigner, beneficiary); + } +} diff --git a/src/test/smart-wallet/AccountVulnPOC.t.sol b/src/test/smart-wallet/AccountVulnPOC.t.sol new file mode 100644 index 000000000..fcb85f509 --- /dev/null +++ b/src/test/smart-wallet/AccountVulnPOC.t.sol @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test utils +import "../utils/BaseTest.sol"; +// Account Abstraction setup for smart wallets. +import { EntryPoint, IEntryPoint } from "contracts/prebuilts/account/utils/EntryPoint.sol"; +import { PackedUserOperation } from "contracts/prebuilts/account/interfaces/PackedUserOperation.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Target +import { IAccountPermissions } from "contracts/extension/interface/IAccountPermissions.sol"; +import { AccountFactory, Account as SimpleAccount } from "contracts/prebuilts/account/non-upgradeable/AccountFactory.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +library GPv2EIP1271 { + bytes4 internal constant MAGICVALUE = 0x1626ba7e; +} + +interface EIP1271Verifier { + function isValidSignature(bytes32 _hash, bytes memory _signature) external view returns (bytes4 magicValue); +} + +/// @dev This is a dummy contract to test contract interactions with Account. +contract Number { + uint256 public num; + + function setNum(uint256 _num) public { + num = _num; + } + + function doubleNum() public { + num *= 2; + } + + function incrementNum() public { + num += 1; + } + + function setNumBySignature(address owner, uint256 newNum, bytes calldata signature) public { + if (owner.code.length == 0) { + // Signature verification by ECDSA + } else { + // Signature verification by EIP1271 + bytes32 digest = keccak256(abi.encode(newNum)); + require( + EIP1271Verifier(owner).isValidSignature(digest, signature) == GPv2EIP1271.MAGICVALUE, + "GPv2: invalid eip1271 signature" + ); + num = newNum; + } + } +} + +contract SimpleAccountVulnPOCTest is BaseTest { + // Target contracts + EntryPoint private entrypoint; + AccountFactory private accountFactory; + + // Mocks + Number internal numberContract; + + // Test params + uint256 private accountAdminPKey = 100; + address private accountAdmin; + + uint256 private accountSignerPKey = 200; + address private accountSigner; + + uint256 private nonSignerPKey = 300; + address private nonSigner; + + // UserOp terminology: `sender` is the smart wallet. + address private sender = 0x0df2C3523703d165Aa7fA1a552f3F0B56275DfC6; + address payable private beneficiary = payable(address(0x45654)); + + bytes32 private uidCache = bytes32("random uid"); + + event AccountCreated(address indexed account, address indexed accountAdmin); + + function _prepareSignature( + IAccountPermissions.SignerPermissionRequest memory _req + ) internal view returns (bytes32 typedDataHash) { + bytes32 typehashSignerPermissionRequest = keccak256( + "SignerPermissionRequest(address signer,uint8 isAdmin,address[] approvedTargets,uint256 nativeTokenLimitPerTransaction,uint128 permissionStartTimestamp,uint128 permissionEndTimestamp,uint128 reqValidityStartTimestamp,uint128 reqValidityEndTimestamp,bytes32 uid)" + ); + bytes32 nameHash = keccak256(bytes("Account")); + bytes32 versionHash = keccak256(bytes("1")); + bytes32 typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + bytes32 domainSeparator = keccak256(abi.encode(typehashEip712, nameHash, versionHash, block.chainid, sender)); + + bytes memory encodedRequestStart = abi.encode( + typehashSignerPermissionRequest, + _req.signer, + _req.isAdmin, + keccak256(abi.encodePacked(_req.approvedTargets)), + _req.nativeTokenLimitPerTransaction + ); + + bytes memory encodedRequestEnd = abi.encode( + _req.permissionStartTimestamp, + _req.permissionEndTimestamp, + _req.reqValidityStartTimestamp, + _req.reqValidityEndTimestamp, + _req.uid + ); + + bytes32 structHash = keccak256(bytes.concat(encodedRequestStart, encodedRequestEnd)); + typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + } + + function _signSignerPermissionRequest( + IAccountPermissions.SignerPermissionRequest memory _req + ) internal view returns (bytes memory signature) { + bytes32 typedDataHash = _prepareSignature(_req); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(accountAdminPKey, typedDataHash); + signature = abi.encodePacked(r, s, v); + } + + function _setupUserOp( + uint256 _signerPKey, + bytes memory _initCode, + bytes memory _callDataForEntrypoint + ) internal returns (PackedUserOperation[] memory ops) { + uint256 nonce = entrypoint.getNonce(sender, 0); + PackedUserOperation memory op; + + { + uint128 verificationGasLimit = 500_000; + uint128 callGasLimit = 500_000; + bytes32 packedGasLimits = (bytes32(uint256(verificationGasLimit)) << 128) | bytes32(uint256(callGasLimit)); + + // Get user op fields + op = PackedUserOperation({ + sender: sender, + nonce: nonce, + initCode: _initCode, + callData: _callDataForEntrypoint, + accountGasLimits: packedGasLimits, + preVerificationGas: 500_000, + gasFees: 0, + paymasterAndData: bytes(""), + signature: bytes("") + }); + } + + // Sign UserOp + bytes32 opHash = EntryPoint(entrypoint).getUserOpHash(op); + bytes32 msgHash = ECDSA.toEthSignedMessageHash(opHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signerPKey, msgHash); + bytes memory userOpSignature = abi.encodePacked(r, s, v); + + address recoveredSigner = ECDSA.recover(msgHash, v, r, s); + address expectedSigner = vm.addr(_signerPKey); + assertEq(recoveredSigner, expectedSigner); + + op.signature = userOpSignature; + + // Store UserOp + ops = new PackedUserOperation[](1); + ops[0] = op; + } + + function _setupUserOpExecute( + uint256 _signerPKey, + bytes memory _initCode, + address _target, + uint256 _value, + bytes memory _callData + ) internal returns (PackedUserOperation[] memory) { + bytes memory callDataForEntrypoint = abi.encodeWithSignature( + "execute(address,uint256,bytes)", + _target, + _value, + _callData + ); + + return _setupUserOp(_signerPKey, _initCode, callDataForEntrypoint); + } + + function _setupUserOpExecuteBatch( + uint256 _signerPKey, + bytes memory _initCode, + address[] memory _target, + uint256[] memory _value, + bytes[] memory _callData + ) internal returns (PackedUserOperation[] memory) { + bytes memory callDataForEntrypoint = abi.encodeWithSignature( + "executeBatch(address[],uint256[],bytes[])", + _target, + _value, + _callData + ); + + return _setupUserOp(_signerPKey, _initCode, callDataForEntrypoint); + } + + function setUp() public override { + super.setUp(); + + // Setup signers. + accountAdmin = vm.addr(accountAdminPKey); + vm.deal(accountAdmin, 100 ether); + + accountSigner = vm.addr(accountSignerPKey); + nonSigner = vm.addr(nonSignerPKey); + + // Setup contracts + entrypoint = new EntryPoint(); + // deploy account factory + accountFactory = new AccountFactory(deployer, IEntryPoint(payable(address(entrypoint)))); + // deploy dummy contract + numberContract = new Number(); + } + + /*////////////////////////////////////////////////////////// + Test: performing a contract call + //////////////////////////////////////////////////////////////*/ + + function _setup_executeTransaction() internal { + bytes memory initCallData = abi.encodeWithSignature("createAccount(address,bytes)", accountAdmin, bytes("")); + bytes memory initCode = abi.encodePacked(abi.encodePacked(address(accountFactory)), initCallData); + + PackedUserOperation[] memory userOpCreateAccount = _setupUserOpExecute( + accountAdminPKey, + initCode, + address(0), + 0, + bytes("") + ); + + EntryPoint(entrypoint).handleOps(userOpCreateAccount, beneficiary); + } + + function test_POC() public { + _setup_executeTransaction(); + + /*////////////////////////////////////////////////////////// + Setup + //////////////////////////////////////////////////////////////*/ + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + address[] memory approvedTargets = new address[](1); + approvedTargets[0] = address(0x123); // allowing accountSigner permissions for some random contract, consider it as 0 address here + + IAccountPermissions.SignerPermissionRequest memory permissionsReq = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 0, + approvedTargets, + 1 ether, + 0, + type(uint128).max, + 0, + type(uint128).max, + uidCache + ); + + vm.prank(accountAdmin); + bytes memory sig = _signSignerPermissionRequest(permissionsReq); + IAccountPermissions(payable(account)).setPermissionsForSigner(permissionsReq, sig); + + // As expected, Account Signer is not be able to call setNum on numberContract since it doesnt have numberContract as approved target + assertEq(numberContract.num(), 0); + + vm.prank(accountSigner); + PackedUserOperation[] memory userOp = _setupUserOpExecute( + accountSignerPKey, + bytes(""), + address(numberContract), + 0, + abi.encodeWithSignature("setNum(uint256)", 42) + ); + + vm.expectRevert(); + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + + /*////////////////////////////////////////////////////////// + Attack + //////////////////////////////////////////////////////////////*/ + + // However they can bypass this by using signature verification on number contract instead + vm.prank(accountSigner); + bytes32 digest = keccak256(abi.encode(42)); + bytes32 toSign = SimpleAccount(payable(account)).getMessageHash(digest); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(accountSignerPKey, toSign); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert("Account: caller not approved target."); + numberContract.setNumBySignature(account, 42, signature); + assertEq(numberContract.num(), 0); + + // Signer can perform transaction if target is approved. + address[] memory newApprovedTargets = new address[](2); + newApprovedTargets[0] = address(0x123); // allowing accountSigner permissions for some random contract, consider it as 0 address here + newApprovedTargets[1] = address(numberContract); + + IAccountPermissions.SignerPermissionRequest memory updatedPermissionsReq = IAccountPermissions + .SignerPermissionRequest( + accountSigner, + 0, + newApprovedTargets, + 1 ether, + 0, + type(uint128).max, + 0, + type(uint128).max, + bytes32("another UID") + ); + + vm.prank(accountAdmin); + bytes memory sig2 = _signSignerPermissionRequest(updatedPermissionsReq); + IAccountPermissions(payable(account)).setPermissionsForSigner(updatedPermissionsReq, sig2); + + numberContract.setNumBySignature(account, 42, signature); + assertEq(numberContract.num(), 42); + } +} diff --git a/src/test/smart-wallet/DynamicAccount.t.sol b/src/test/smart-wallet/DynamicAccount.t.sol new file mode 100644 index 000000000..fce57c9cc --- /dev/null +++ b/src/test/smart-wallet/DynamicAccount.t.sol @@ -0,0 +1,867 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test utils +import "../utils/BaseTest.sol"; +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; +import { IAccountPermissions } from "contracts/extension/interface/IAccountPermissions.sol"; +import { AccountPermissions } from "contracts/extension/upgradeable/AccountPermissions.sol"; +import { AccountExtension } from "contracts/prebuilts/account/utils/AccountExtension.sol"; + +// Account Abstraction setup for smart wallets. +import { EntryPoint, IEntryPoint } from "contracts/prebuilts/account/utils/EntryPoint.sol"; +import { PackedUserOperation } from "contracts/prebuilts/account/interfaces/PackedUserOperation.sol"; + +// Target +import { Account as SimpleAccount } from "contracts/prebuilts/account/non-upgradeable/Account.sol"; +import { DynamicAccountFactory, DynamicAccount } from "contracts/prebuilts/account/dynamic/DynamicAccountFactory.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +/// @dev This is a dummy contract to test contract interactions with Account. +contract Number { + uint256 public num; + + function setNum(uint256 _num) public { + num = _num; + } + + function doubleNum() public { + num *= 2; + } + + function incrementNum() public { + num += 1; + } +} + +contract NFTRejector { + function onERC721Received(address, address, uint256, bytes memory) public virtual returns (bytes4) { + revert("NFTs not accepted"); + } +} + +contract DynamicAccountTest is BaseTest { + // Target contracts + EntryPoint private constant entrypoint = EntryPoint(payable(0x0000000071727De22E5E9d8BAf0edAc6f37da032)); + DynamicAccountFactory private accountFactory; + + // Mocks + Number internal numberContract; + + // Test params + uint256 private accountAdminPKey = 100; + address private accountAdmin; + + uint256 private accountSignerPKey = 200; + address private accountSigner; + + uint256 private nonSignerPKey = 300; + address private nonSigner; + + bytes internal data = bytes(""); + + // UserOp terminology: `sender` is the smart wallet. + address private sender = 0x78b942FBC9126b4Ed8384Bb9dd1420Ea865be91a; + address payable private beneficiary = payable(address(0x45654)); + + bytes32 private uidCache = bytes32("random uid"); + + event AccountCreated(address indexed account, address indexed accountAdmin); + + function _prepareSignature( + IAccountPermissions.SignerPermissionRequest memory _req + ) internal view returns (bytes32 typedDataHash) { + bytes32 typehashSignerPermissionRequest = keccak256( + "SignerPermissionRequest(address signer,uint8 isAdmin,address[] approvedTargets,uint256 nativeTokenLimitPerTransaction,uint128 permissionStartTimestamp,uint128 permissionEndTimestamp,uint128 reqValidityStartTimestamp,uint128 reqValidityEndTimestamp,bytes32 uid)" + ); + bytes32 nameHash = keccak256(bytes("Account")); + bytes32 versionHash = keccak256(bytes("1")); + bytes32 typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + bytes32 domainSeparator = keccak256(abi.encode(typehashEip712, nameHash, versionHash, block.chainid, sender)); + + bytes memory encodedRequestStart = abi.encode( + typehashSignerPermissionRequest, + _req.signer, + _req.isAdmin, + keccak256(abi.encodePacked(_req.approvedTargets)), + _req.nativeTokenLimitPerTransaction + ); + + bytes memory encodedRequestEnd = abi.encode( + _req.permissionStartTimestamp, + _req.permissionEndTimestamp, + _req.reqValidityStartTimestamp, + _req.reqValidityEndTimestamp, + _req.uid + ); + + bytes32 structHash = keccak256(bytes.concat(encodedRequestStart, encodedRequestEnd)); + typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + } + + function _signSignerPermissionRequest( + IAccountPermissions.SignerPermissionRequest memory _req + ) internal view returns (bytes memory signature) { + bytes32 typedDataHash = _prepareSignature(_req); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(accountAdminPKey, typedDataHash); + signature = abi.encodePacked(r, s, v); + } + + function _setupUserOp( + uint256 _signerPKey, + bytes memory _initCode, + bytes memory _callDataForEntrypoint + ) internal returns (PackedUserOperation[] memory ops) { + uint256 nonce = entrypoint.getNonce(sender, 0); + PackedUserOperation memory op; + + { + uint128 verificationGasLimit = 5_000_000; + uint128 callGasLimit = 5_000_000; + bytes32 packedGasLimits = (bytes32(uint256(verificationGasLimit)) << 128) | bytes32(uint256(callGasLimit)); + + // Get user op fields + op = PackedUserOperation({ + sender: sender, + nonce: nonce, + initCode: _initCode, + callData: _callDataForEntrypoint, + accountGasLimits: packedGasLimits, + preVerificationGas: 5_000_000, + gasFees: 0, + paymasterAndData: bytes(""), + signature: bytes("") + }); + } + + // Sign UserOp + bytes32 opHash = EntryPoint(entrypoint).getUserOpHash(op); + bytes32 msgHash = ECDSA.toEthSignedMessageHash(opHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signerPKey, msgHash); + bytes memory userOpSignature = abi.encodePacked(r, s, v); + + address recoveredSigner = ECDSA.recover(msgHash, v, r, s); + address expectedSigner = vm.addr(_signerPKey); + assertEq(recoveredSigner, expectedSigner); + + op.signature = userOpSignature; + + // Store UserOp + ops = new PackedUserOperation[](1); + ops[0] = op; + } + + function _setupUserOpWithSender( + bytes memory _initCode, + bytes memory _callDataForEntrypoint, + address _sender + ) internal returns (PackedUserOperation[] memory ops) { + uint256 nonce = entrypoint.getNonce(_sender, 0); + PackedUserOperation memory op; + + { + uint128 verificationGasLimit = 5_000_000; + uint128 callGasLimit = 5_000_000; + bytes32 packedGasLimits = (bytes32(uint256(verificationGasLimit)) << 128) | bytes32(uint256(callGasLimit)); + + // Get user op fields + op = PackedUserOperation({ + sender: _sender, + nonce: nonce, + initCode: _initCode, + callData: _callDataForEntrypoint, + accountGasLimits: packedGasLimits, + preVerificationGas: 5_000_000, + gasFees: 0, + paymasterAndData: bytes(""), + signature: bytes("") + }); + } + + // Sign UserOp + bytes32 opHash = EntryPoint(entrypoint).getUserOpHash(op); + bytes32 msgHash = ECDSA.toEthSignedMessageHash(opHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(accountAdminPKey, msgHash); + bytes memory userOpSignature = abi.encodePacked(r, s, v); + + address recoveredSigner = ECDSA.recover(msgHash, v, r, s); + address expectedSigner = vm.addr(accountAdminPKey); + assertEq(recoveredSigner, expectedSigner); + + op.signature = userOpSignature; + + // Store UserOp + ops = new PackedUserOperation[](1); + ops[0] = op; + } + + function _setupUserOpExecuteWithSender( + bytes memory _initCode, + address _target, + uint256 _value, + bytes memory _callData, + address _sender + ) internal returns (PackedUserOperation[] memory) { + bytes memory callDataForEntrypoint = abi.encodeWithSignature( + "execute(address,uint256,bytes)", + _target, + _value, + _callData + ); + + return _setupUserOpWithSender(_initCode, callDataForEntrypoint, _sender); + } + + function _setupUserOpExecute( + uint256 _signerPKey, + bytes memory _initCode, + address _target, + uint256 _value, + bytes memory _callData + ) internal returns (PackedUserOperation[] memory) { + bytes memory callDataForEntrypoint = abi.encodeWithSignature( + "execute(address,uint256,bytes)", + _target, + _value, + _callData + ); + + return _setupUserOp(_signerPKey, _initCode, callDataForEntrypoint); + } + + function _setupUserOpExecuteBatch( + uint256 _signerPKey, + bytes memory _initCode, + address[] memory _target, + uint256[] memory _value, + bytes[] memory _callData + ) internal returns (PackedUserOperation[] memory) { + bytes memory callDataForEntrypoint = abi.encodeWithSignature( + "executeBatch(address[],uint256[],bytes[])", + _target, + _value, + _callData + ); + + return _setupUserOp(_signerPKey, _initCode, callDataForEntrypoint); + } + + /// @dev Returns the salt used when deploying an Account. + function _generateSalt(address _admin, bytes memory _data) internal view virtual returns (bytes32) { + return keccak256(abi.encode(_admin, _data)); + } + + function setUp() public override { + super.setUp(); + + // Setup signers. + accountAdmin = vm.addr(accountAdminPKey); + vm.deal(accountAdmin, 100 ether); + + accountSigner = vm.addr(accountSignerPKey); + nonSigner = vm.addr(nonSignerPKey); + + // Setup contracts + address _deployedEntrypoint = address(new EntryPoint()); + vm.etch(address(entrypoint), bytes(_deployedEntrypoint.code)); + + // Setting up default extension. + IExtension.Extension memory defaultExtension; + + defaultExtension.metadata = IExtension.ExtensionMetadata({ + name: "AccountExtension", + metadataURI: "ipfs://AccountExtension", + implementation: address(new AccountExtension()) + }); + + defaultExtension.functions = new IExtension.ExtensionFunction[](9); + + defaultExtension.functions[0] = IExtension.ExtensionFunction( + AccountExtension.supportsInterface.selector, + "supportsInterface(bytes4)" + ); + defaultExtension.functions[1] = IExtension.ExtensionFunction( + AccountExtension.execute.selector, + "execute(address,uint256,bytes)" + ); + defaultExtension.functions[2] = IExtension.ExtensionFunction( + AccountExtension.executeBatch.selector, + "executeBatch(address[],uint256[],bytes[])" + ); + defaultExtension.functions[3] = IExtension.ExtensionFunction( + ERC721Holder.onERC721Received.selector, + "onERC721Received(address,address,uint256,bytes)" + ); + defaultExtension.functions[4] = IExtension.ExtensionFunction( + ERC1155Holder.onERC1155Received.selector, + "onERC1155Received(address,address,uint256,uint256,bytes)" + ); + defaultExtension.functions[5] = IExtension.ExtensionFunction( + bytes4(0), // Selector for `receive()` function. + "receive()" + ); + defaultExtension.functions[6] = IExtension.ExtensionFunction( + AccountExtension.isValidSignature.selector, + "isValidSignature(bytes32,bytes)" + ); + defaultExtension.functions[7] = IExtension.ExtensionFunction( + AccountExtension.addDeposit.selector, + "addDeposit()" + ); + defaultExtension.functions[8] = IExtension.ExtensionFunction( + AccountExtension.withdrawDepositTo.selector, + "withdrawDepositTo(address,uint256)" + ); + + IExtension.Extension[] memory extensions = new IExtension.Extension[](1); + extensions[0] = defaultExtension; + + // deploy account factory + accountFactory = new DynamicAccountFactory(deployer, extensions); + // deploy dummy contract + numberContract = new Number(); + } + + /*/////////////////////////////////////////////////////////////// + Test: creating an account + //////////////////////////////////////////////////////////////*/ + + /// @dev benchmark test for deployment gas cost + function test_deploy_dynamicAccount() public { + // Setting up default extension. + IExtension.Extension memory defaultExtension; + + defaultExtension.metadata = IExtension.ExtensionMetadata({ + name: "AccountExtension", + metadataURI: "ipfs://AccountExtension", + implementation: address(new AccountExtension()) + }); + + defaultExtension.functions = new IExtension.ExtensionFunction[](7); + + defaultExtension.functions[0] = IExtension.ExtensionFunction( + AccountExtension.supportsInterface.selector, + "supportsInterface(bytes4)" + ); + defaultExtension.functions[1] = IExtension.ExtensionFunction( + AccountExtension.execute.selector, + "execute(address,uint256,bytes)" + ); + defaultExtension.functions[2] = IExtension.ExtensionFunction( + AccountExtension.executeBatch.selector, + "executeBatch(address[],uint256[],bytes[])" + ); + defaultExtension.functions[3] = IExtension.ExtensionFunction( + ERC721Holder.onERC721Received.selector, + "onERC721Received(address,address,uint256,bytes)" + ); + defaultExtension.functions[4] = IExtension.ExtensionFunction( + ERC1155Holder.onERC1155Received.selector, + "onERC1155Received(address,address,uint256,uint256,bytes)" + ); + defaultExtension.functions[5] = IExtension.ExtensionFunction( + bytes4(0), // Selector for `receive()` function. + "receive()" + ); + defaultExtension.functions[6] = IExtension.ExtensionFunction( + AccountExtension.isValidSignature.selector, + "isValidSignature(bytes32,bytes)" + ); + + IExtension.Extension[] memory extensions = new IExtension.Extension[](1); + extensions[0] = defaultExtension; + + // deploy account factory + DynamicAccountFactory factory = new DynamicAccountFactory(deployer, extensions); + } + + /// @dev Create an account by directly calling the factory. + function test_state_createAccount_viaFactory() public { + vm.expectEmit(true, true, false, true); + emit AccountCreated(sender, accountAdmin); + accountFactory.createAccount(accountAdmin, data); + + address[] memory allAccounts = accountFactory.getAllAccounts(); + assertEq(allAccounts.length, 1); + assertEq(allAccounts[0], sender); + } + + /// @dev Create an account via Entrypoint. + function test_state_createAccount_viaEntrypoint() public { + bytes memory initCallData = abi.encodeWithSignature("createAccount(address,bytes)", accountAdmin, data); + bytes memory initCode = abi.encodePacked(abi.encodePacked(address(accountFactory)), initCallData); + + PackedUserOperation[] memory userOpCreateAccount = _setupUserOpExecute( + accountAdminPKey, + initCode, + address(0), + 0, + bytes("") + ); + + vm.expectEmit(true, true, false, true); + emit AccountCreated(sender, accountAdmin); + EntryPoint(entrypoint).handleOps(userOpCreateAccount, beneficiary); + + address[] memory allAccounts = accountFactory.getAllAccounts(); + assertEq(allAccounts.length, 1); + assertEq(allAccounts[0], sender); + } + + /// @dev Try registering with factory with a contract not deployed by factory. + function test_revert_onRegister_nonFactoryChildContract() public { + vm.prank(address(0x12345)); + vm.expectRevert("AccountFactory: not an account."); + accountFactory.onRegister(_generateSalt(accountAdmin, "")); + } + + /// @dev Create more than one accounts with the same admin. + function test_state_createAccount_viaEntrypoint_multipleAccountSameAdmin() public { + uint256 amount = 1; + + for (uint256 i = 0; i < amount; i += 1) { + bytes memory initCallData = abi.encodeWithSignature( + "createAccount(address,bytes)", + accountAdmin, + bytes(abi.encode(i)) + ); + bytes memory initCode = abi.encodePacked(abi.encodePacked(address(accountFactory)), initCallData); + + address expectedSenderAddress = Clones.predictDeterministicAddress( + accountFactory.accountImplementation(), + _generateSalt(accountAdmin, bytes(abi.encode(i))), + address(accountFactory) + ); + + PackedUserOperation[] memory userOpCreateAccount = _setupUserOpExecuteWithSender( + initCode, + address(0), + 0, + bytes(abi.encode(i)), + expectedSenderAddress + ); + + vm.expectEmit(true, true, false, true); + emit AccountCreated(expectedSenderAddress, accountAdmin); + EntryPoint(entrypoint).handleOps(userOpCreateAccount, beneficiary); + } + + address[] memory allAccounts = accountFactory.getAllAccounts(); + assertEq(allAccounts.length, amount); + + for (uint256 i = 0; i < amount; i += 1) { + assertEq( + allAccounts[i], + Clones.predictDeterministicAddress( + accountFactory.accountImplementation(), + _generateSalt(accountAdmin, bytes(abi.encode(i))), + address(accountFactory) + ) + ); + } + } + + /*/////////////////////////////////////////////////////////////// + Test: performing a contract call + //////////////////////////////////////////////////////////////*/ + + function _setup_executeTransaction() internal { + bytes memory initCallData = abi.encodeWithSignature("createAccount(address,bytes)", accountAdmin, data); + bytes memory initCode = abi.encodePacked(abi.encodePacked(address(accountFactory)), initCallData); + + PackedUserOperation[] memory userOpCreateAccount = _setupUserOpExecute( + accountAdminPKey, + initCode, + address(0), + 0, + bytes("") + ); + + EntryPoint(entrypoint).handleOps(userOpCreateAccount, beneficiary); + } + + /// @dev Perform a state changing transaction directly via account. + function test_state_executeTransaction() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + assertEq(numberContract.num(), 0); + + vm.prank(accountAdmin); + SimpleAccount(payable(account)).execute( + address(numberContract), + 0, + abi.encodeWithSignature("setNum(uint256)", 42) + ); + + assertEq(numberContract.num(), 42); + } + + /// @dev Perform many state changing transactions in a batch directly via account. + function test_state_executeBatchTransaction() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + assertEq(numberContract.num(), 0); + + uint256 count = 3; + address[] memory targets = new address[](count); + uint256[] memory values = new uint256[](count); + bytes[] memory callData = new bytes[](count); + + for (uint256 i = 0; i < count; i += 1) { + targets[i] = address(numberContract); + values[i] = 0; + callData[i] = abi.encodeWithSignature("incrementNum()", i); + } + + vm.prank(accountAdmin); + SimpleAccount(payable(account)).executeBatch(targets, values, callData); + + assertEq(numberContract.num(), count); + } + + /// @dev Perform a state changing transaction via Entrypoint. + function test_state_executeTransaction_viaEntrypoint() public { + _setup_executeTransaction(); + + assertEq(numberContract.num(), 0); + + PackedUserOperation[] memory userOp = _setupUserOpExecute( + accountAdminPKey, + bytes(""), + address(numberContract), + 0, + abi.encodeWithSignature("setNum(uint256)", 42) + ); + + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + + assertEq(numberContract.num(), 42); + } + + /// @dev Perform many state changing transactions in a batch via Entrypoint. + function test_state_executeBatchTransaction_viaEntrypoint() public { + _setup_executeTransaction(); + + assertEq(numberContract.num(), 0); + + uint256 count = 3; + address[] memory targets = new address[](count); + uint256[] memory values = new uint256[](count); + bytes[] memory callData = new bytes[](count); + + for (uint256 i = 0; i < count; i += 1) { + targets[i] = address(numberContract); + values[i] = 0; + callData[i] = abi.encodeWithSignature("incrementNum()", i); + } + + PackedUserOperation[] memory userOp = _setupUserOpExecuteBatch( + accountAdminPKey, + bytes(""), + targets, + values, + callData + ); + + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + + assertEq(numberContract.num(), count); + } + + /// @dev Perform many state changing transactions in a batch via Entrypoint. + function test_state_executeBatchTransaction_viaAccountSigner() public { + _setup_executeTransaction(); + + assertEq(numberContract.num(), 0); + + uint256 count = 3; + address[] memory targets = new address[](count); + uint256[] memory values = new uint256[](count); + bytes[] memory callData = new bytes[](count); + + for (uint256 i = 0; i < count; i += 1) { + targets[i] = address(numberContract); + values[i] = 0; + callData[i] = abi.encodeWithSignature("incrementNum()", i); + } + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + address[] memory approvedTargets = new address[](1); + approvedTargets[0] = address(numberContract); + + IAccountPermissions.SignerPermissionRequest memory permissionsReq = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 0, + approvedTargets, + 1 ether, + 0, + type(uint128).max, + 0, + type(uint128).max, + uidCache + ); + + vm.prank(accountAdmin); + bytes memory sig = _signSignerPermissionRequest(permissionsReq); + SimpleAccount(payable(account)).setPermissionsForSigner(permissionsReq, sig); + + PackedUserOperation[] memory userOp = _setupUserOpExecuteBatch( + accountSignerPKey, + bytes(""), + targets, + values, + callData + ); + + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + + assertEq(numberContract.num(), count); + } + + /// @dev Perform a state changing transaction via Entrypoint and a SIGNER_ROLE holder. + function test_state_executeTransaction_viaAccountSigner() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + address[] memory approvedTargets = new address[](1); + approvedTargets[0] = address(numberContract); + + IAccountPermissions.SignerPermissionRequest memory permissionsReq = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 0, + approvedTargets, + 1 ether, + 0, + type(uint128).max, + 0, + type(uint128).max, + uidCache + ); + + vm.prank(accountAdmin); + bytes memory sig = _signSignerPermissionRequest(permissionsReq); + SimpleAccount(payable(account)).setPermissionsForSigner(permissionsReq, sig); + + assertEq(numberContract.num(), 0); + + PackedUserOperation[] memory userOp = _setupUserOpExecute( + accountSignerPKey, + bytes(""), + address(numberContract), + 0, + abi.encodeWithSignature("setNum(uint256)", 42) + ); + + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + + assertEq(numberContract.num(), 42); + } + + /// @dev Revert: perform a state changing transaction via Entrypoint without appropriate permissions. + function test_revert_executeTransaction_nonSigner_viaEntrypoint() public { + _setup_executeTransaction(); + + assertEq(numberContract.num(), 0); + + PackedUserOperation[] memory userOp = _setupUserOpExecute( + accountSignerPKey, + bytes(""), + address(numberContract), + 0, + abi.encodeWithSignature("setNum(uint256)", 42) + ); + + vm.expectRevert(); + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + } + + /// @dev Revert: non-admin performs a state changing transaction directly via account contract. + function test_revert_executeTransaction_nonSigner_viaDirectCall() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + address[] memory approvedTargets = new address[](1); + approvedTargets[0] = address(numberContract); + + IAccountPermissions.SignerPermissionRequest memory permissionsReq = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 0, + approvedTargets, + 1 ether, + 0, + type(uint128).max, + 0, + type(uint128).max, + uidCache + ); + + vm.prank(accountAdmin); + bytes memory sig = _signSignerPermissionRequest(permissionsReq); + SimpleAccount(payable(account)).setPermissionsForSigner(permissionsReq, sig); + assertEq(numberContract.num(), 0); + + vm.prank(accountSigner); + vm.expectRevert("Account: not admin or EntryPoint."); + SimpleAccount(payable(account)).execute( + address(numberContract), + 0, + abi.encodeWithSignature("setNum(uint256)", 42) + ); + } + + /*/////////////////////////////////////////////////////////////// + Test: receiving and sending native tokens + //////////////////////////////////////////////////////////////*/ + + /// @dev Send native tokens to an account. + function test_state_accountReceivesNativeTokens() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + assertEq(address(account).balance, 0); + + vm.prank(accountAdmin); + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory ret) = payable(account).call{ value: 1000 }(""); + + // Silence warning: Return value of low-level calls not used. + (success, ret) = (success, ret); + + assertEq(address(account).balance, 1000); + } + + /// @dev Transfer native tokens out of an account. + function test_state_transferOutsNativeTokens() public { + _setup_executeTransaction(); + + uint256 value = 1000; + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + vm.prank(accountAdmin); + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory ret) = payable(account).call{ value: value }(""); + assertEq(address(account).balance, value); + + // Silence warning: Return value of low-level calls not used. + (success, ret) = (success, ret); + + address recipient = address(0x3456); + + PackedUserOperation[] memory userOp = _setupUserOpExecute( + accountAdminPKey, + bytes(""), + recipient, + value, + bytes("") + ); + + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + assertEq(address(account).balance, 0); + assertEq(recipient.balance, value); + } + + /// @dev Add and remove a deposit for the account from the Entrypoint. + + function test_state_addAndWithdrawDeposit() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + assertEq(EntryPoint(entrypoint).balanceOf(account), 0); + + vm.prank(accountAdmin); + SimpleAccount(payable(account)).addDeposit{ value: 1000 }(); + assertEq(EntryPoint(entrypoint).balanceOf(account), 1000); + + vm.prank(accountAdmin); + SimpleAccount(payable(account)).withdrawDepositTo(payable(accountSigner), 500); + assertEq(EntryPoint(entrypoint).balanceOf(account), 500); + } + + /*/////////////////////////////////////////////////////////////// + Test: receiving ERC-721 and ERC-1155 NFTs + //////////////////////////////////////////////////////////////*/ + + /// @dev Send an ERC-721 NFT to an account. + function test_state_receiveERC721NFT() public { + _setup_executeTransaction(); + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + assertEq(erc721.balanceOf(account), 0); + + erc721.mint(account, 1); + + assertEq(erc721.balanceOf(account), 1); + } + + /// @dev Send an ERC-1155 NFT to an account. + function test_state_receiveERC1155NFT() public { + _setup_executeTransaction(); + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + assertEq(erc1155.balanceOf(account, 0), 0); + + erc1155.mint(account, 0, 1); + + assertEq(erc1155.balanceOf(account, 0), 1); + } + + /*/////////////////////////////////////////////////////////////// + Test: change an extension on the account + //////////////////////////////////////////////////////////////*/ + + /// @dev Make the account reject ERC-721 NFTs instead of accepting them. + function test_scenario_changeExtensionForFunction() public { + _setup_executeTransaction(); + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + // The account can initially receive NFTs. + assertEq(erc721.balanceOf(account), 0); + erc721.mint(account, 1); + assertEq(erc721.balanceOf(account), 1); + + // Make the account reject ERC-721 NFTs going forward. + IExtension.Extension memory extension; + + extension.metadata = IExtension.ExtensionMetadata({ + name: "NFTRejector", + metadataURI: "ipfs://NFTRejector", + implementation: address(new NFTRejector()) + }); + + extension.functions = new IExtension.ExtensionFunction[](1); + + extension.functions[0] = IExtension.ExtensionFunction( + NFTRejector.onERC721Received.selector, + "onERC721Received(address,address,uint256,bytes)" + ); + + vm.prank(accountAdmin); + DynamicAccount(payable(account)).disableFunctionInExtension( + "AccountExtension", + NFTRejector.onERC721Received.selector + ); + + vm.prank(accountAdmin); + DynamicAccount(payable(account)).addExtension(extension); + + // Transfer NFTs to the account + erc721.mint(accountSigner, 1); + assertEq(erc721.ownerOf(1), accountSigner); + vm.prank(accountSigner); + vm.expectRevert("NFTs not accepted"); + erc721.safeTransferFrom(accountSigner, account, 1); + } +} diff --git a/src/test/smart-wallet/ManagedAccount.t.sol b/src/test/smart-wallet/ManagedAccount.t.sol new file mode 100644 index 000000000..5ae76968a --- /dev/null +++ b/src/test/smart-wallet/ManagedAccount.t.sol @@ -0,0 +1,876 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test utils +import "../utils/BaseTest.sol"; +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; +import { IAccountPermissions } from "contracts/extension/interface/IAccountPermissions.sol"; +import { AccountPermissions } from "contracts/extension/upgradeable/AccountPermissions.sol"; +import { AccountExtension } from "contracts/prebuilts/account/utils/AccountExtension.sol"; + +// Account Abstraction setup for smart wallets. +import { EntryPoint, IEntryPoint } from "contracts/prebuilts/account/utils/EntryPoint.sol"; +import { PackedUserOperation } from "contracts/prebuilts/account/interfaces/PackedUserOperation.sol"; + +// Target +import { Account as SimpleAccount } from "contracts/prebuilts/account/non-upgradeable/Account.sol"; +import { ManagedAccountFactory, ManagedAccount } from "contracts/prebuilts/account/managed/ManagedAccountFactory.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +/// @dev This is a dummy contract to test contract interactions with Account. +contract Number { + uint256 public num; + + function setNum(uint256 _num) public { + num = _num; + } + + function doubleNum() public { + num *= 2; + } + + function incrementNum() public { + num += 1; + } +} + +contract NFTRejector { + function onERC721Received(address, address, uint256, bytes memory) public virtual returns (bytes4) { + revert("NFTs not accepted"); + } +} + +contract ManagedAccountTest is BaseTest { + // Target contracts + EntryPoint private entrypoint; + ManagedAccountFactory private accountFactory; + + // Mocks + Number internal numberContract; + + // Test params + address private factoryDeployer = address(0x9876); + bytes internal data = bytes(""); + + uint256 private accountAdminPKey = 100; + address private accountAdmin; + + uint256 private accountSignerPKey = 200; + address private accountSigner; + + uint256 private nonSignerPKey = 300; + address private nonSigner; + + // UserOp terminology: `sender` is the smart wallet. + address private sender = 0xbEA1Fa134A1727187A8f2e7E714B660f3a95478D; + address payable private beneficiary = payable(address(0x45654)); + + bytes32 private uidCache = bytes32("random uid"); + + event AccountCreated(address indexed account, address indexed accountAdmin); + + function _prepareSignature( + IAccountPermissions.SignerPermissionRequest memory _req + ) internal view returns (bytes32 typedDataHash) { + bytes32 typehashSignerPermissionRequest = keccak256( + "SignerPermissionRequest(address signer,uint8 isAdmin,address[] approvedTargets,uint256 nativeTokenLimitPerTransaction,uint128 permissionStartTimestamp,uint128 permissionEndTimestamp,uint128 reqValidityStartTimestamp,uint128 reqValidityEndTimestamp,bytes32 uid)" + ); + bytes32 nameHash = keccak256(bytes("Account")); + bytes32 versionHash = keccak256(bytes("1")); + bytes32 typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + bytes32 domainSeparator = keccak256(abi.encode(typehashEip712, nameHash, versionHash, block.chainid, sender)); + + bytes memory encodedRequestStart = abi.encode( + typehashSignerPermissionRequest, + _req.signer, + _req.isAdmin, + keccak256(abi.encodePacked(_req.approvedTargets)), + _req.nativeTokenLimitPerTransaction + ); + + bytes memory encodedRequestEnd = abi.encode( + _req.permissionStartTimestamp, + _req.permissionEndTimestamp, + _req.reqValidityStartTimestamp, + _req.reqValidityEndTimestamp, + _req.uid + ); + + bytes32 structHash = keccak256(bytes.concat(encodedRequestStart, encodedRequestEnd)); + typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + } + + function _signSignerPermissionRequest( + IAccountPermissions.SignerPermissionRequest memory _req + ) internal view returns (bytes memory signature) { + bytes32 typedDataHash = _prepareSignature(_req); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(accountAdminPKey, typedDataHash); + signature = abi.encodePacked(r, s, v); + } + + function _setupUserOp( + uint256 _signerPKey, + bytes memory _initCode, + bytes memory _callDataForEntrypoint + ) internal returns (PackedUserOperation[] memory ops) { + uint256 nonce = entrypoint.getNonce(sender, 0); + PackedUserOperation memory op; + + { + uint128 verificationGasLimit = 500_000; + uint128 callGasLimit = 500_000; + bytes32 packedGasLimits = (bytes32(uint256(verificationGasLimit)) << 128) | bytes32(uint256(callGasLimit)); + + // Get user op fields + op = PackedUserOperation({ + sender: sender, + nonce: nonce, + initCode: _initCode, + callData: _callDataForEntrypoint, + accountGasLimits: packedGasLimits, + preVerificationGas: 500_000, + gasFees: 0, + paymasterAndData: bytes(""), + signature: bytes("") + }); + } + + // Sign UserOp + bytes32 opHash = EntryPoint(entrypoint).getUserOpHash(op); + bytes32 msgHash = ECDSA.toEthSignedMessageHash(opHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signerPKey, msgHash); + bytes memory userOpSignature = abi.encodePacked(r, s, v); + + address recoveredSigner = ECDSA.recover(msgHash, v, r, s); + address expectedSigner = vm.addr(_signerPKey); + assertEq(recoveredSigner, expectedSigner); + + op.signature = userOpSignature; + + // Store UserOp + ops = new PackedUserOperation[](1); + ops[0] = op; + } + + function _setupUserOpWithSender( + bytes memory _initCode, + bytes memory _callDataForEntrypoint, + address _sender + ) internal returns (PackedUserOperation[] memory ops) { + uint256 nonce = entrypoint.getNonce(_sender, 0); + PackedUserOperation memory op; + + { + uint128 verificationGasLimit = 5_000_000; + uint128 callGasLimit = 5_000_000; + bytes32 packedGasLimits = (bytes32(uint256(verificationGasLimit)) << 128) | bytes32(uint256(callGasLimit)); + + // Get user op fields + op = PackedUserOperation({ + sender: _sender, + nonce: nonce, + initCode: _initCode, + callData: _callDataForEntrypoint, + accountGasLimits: packedGasLimits, + preVerificationGas: 5_000_000, + gasFees: 0, + paymasterAndData: bytes(""), + signature: bytes("") + }); + } + + // Sign UserOp + bytes32 opHash = EntryPoint(entrypoint).getUserOpHash(op); + bytes32 msgHash = ECDSA.toEthSignedMessageHash(opHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(accountAdminPKey, msgHash); + bytes memory userOpSignature = abi.encodePacked(r, s, v); + + address recoveredSigner = ECDSA.recover(msgHash, v, r, s); + address expectedSigner = vm.addr(accountAdminPKey); + assertEq(recoveredSigner, expectedSigner); + + op.signature = userOpSignature; + + // Store UserOp + ops = new PackedUserOperation[](1); + ops[0] = op; + } + + function _setupUserOpExecuteWithSender( + bytes memory _initCode, + address _target, + uint256 _value, + bytes memory _callData, + address _sender + ) internal returns (PackedUserOperation[] memory) { + bytes memory callDataForEntrypoint = abi.encodeWithSignature( + "execute(address,uint256,bytes)", + _target, + _value, + _callData + ); + + return _setupUserOpWithSender(_initCode, callDataForEntrypoint, _sender); + } + + function _setupUserOpExecute( + uint256 _signerPKey, + bytes memory _initCode, + address _target, + uint256 _value, + bytes memory _callData + ) internal returns (PackedUserOperation[] memory) { + bytes memory callDataForEntrypoint = abi.encodeWithSignature( + "execute(address,uint256,bytes)", + _target, + _value, + _callData + ); + + return _setupUserOp(_signerPKey, _initCode, callDataForEntrypoint); + } + + function _setupUserOpExecuteBatch( + uint256 _signerPKey, + bytes memory _initCode, + address[] memory _target, + uint256[] memory _value, + bytes[] memory _callData + ) internal returns (PackedUserOperation[] memory) { + bytes memory callDataForEntrypoint = abi.encodeWithSignature( + "executeBatch(address[],uint256[],bytes[])", + _target, + _value, + _callData + ); + + return _setupUserOp(_signerPKey, _initCode, callDataForEntrypoint); + } + + /// @dev Returns the salt used when deploying an Account. + function _generateSalt(address _admin, bytes memory _data) internal view virtual returns (bytes32) { + return keccak256(abi.encode(_admin, _data)); + } + + function setUp() public override { + super.setUp(); + + // Setup signers. + accountAdmin = vm.addr(accountAdminPKey); + vm.deal(accountAdmin, 100 ether); + + accountSigner = vm.addr(accountSignerPKey); + nonSigner = vm.addr(nonSignerPKey); + + // Setup contracts + entrypoint = new EntryPoint(); + + // Setting up default extension. + IExtension.Extension memory defaultExtension; + + defaultExtension.metadata = IExtension.ExtensionMetadata({ + name: "AccountExtension", + metadataURI: "ipfs://AccountExtension", + implementation: address(new AccountExtension()) + }); + + defaultExtension.functions = new IExtension.ExtensionFunction[](9); + + defaultExtension.functions[0] = IExtension.ExtensionFunction( + AccountExtension.supportsInterface.selector, + "supportsInterface(bytes4)" + ); + defaultExtension.functions[1] = IExtension.ExtensionFunction( + AccountExtension.execute.selector, + "execute(address,uint256,bytes)" + ); + defaultExtension.functions[2] = IExtension.ExtensionFunction( + AccountExtension.executeBatch.selector, + "executeBatch(address[],uint256[],bytes[])" + ); + defaultExtension.functions[3] = IExtension.ExtensionFunction( + ERC721Holder.onERC721Received.selector, + "onERC721Received(address,address,uint256,bytes)" + ); + defaultExtension.functions[4] = IExtension.ExtensionFunction( + ERC1155Holder.onERC1155Received.selector, + "onERC1155Received(address,address,uint256,uint256,bytes)" + ); + defaultExtension.functions[5] = IExtension.ExtensionFunction( + bytes4(0), // Selector for `receive()` function. + "receive()" + ); + defaultExtension.functions[6] = IExtension.ExtensionFunction( + AccountExtension.isValidSignature.selector, + "isValidSignature(bytes32,bytes)" + ); + defaultExtension.functions[7] = IExtension.ExtensionFunction( + AccountExtension.addDeposit.selector, + "addDeposit()" + ); + defaultExtension.functions[8] = IExtension.ExtensionFunction( + AccountExtension.withdrawDepositTo.selector, + "withdrawDepositTo(address,uint256)" + ); + + IExtension.Extension[] memory extensions = new IExtension.Extension[](1); + extensions[0] = defaultExtension; + + // deploy account factory + vm.prank(factoryDeployer); + accountFactory = new ManagedAccountFactory( + factoryDeployer, + IEntryPoint(payable(address(entrypoint))), + extensions + ); + // deploy dummy contract + numberContract = new Number(); + } + + /// @dev benchmark test for deployment gas cost + function test_deploy_managedAccount() public { + // Setting up default extension. + IExtension.Extension memory defaultExtension; + + defaultExtension.metadata = IExtension.ExtensionMetadata({ + name: "AccountExtension", + metadataURI: "ipfs://AccountExtension", + implementation: address(new AccountExtension()) + }); + + defaultExtension.functions = new IExtension.ExtensionFunction[](7); + + defaultExtension.functions[0] = IExtension.ExtensionFunction( + AccountExtension.supportsInterface.selector, + "supportsInterface(bytes4)" + ); + defaultExtension.functions[1] = IExtension.ExtensionFunction( + AccountExtension.execute.selector, + "execute(address,uint256,bytes)" + ); + defaultExtension.functions[2] = IExtension.ExtensionFunction( + AccountExtension.executeBatch.selector, + "executeBatch(address[],uint256[],bytes[])" + ); + defaultExtension.functions[3] = IExtension.ExtensionFunction( + ERC721Holder.onERC721Received.selector, + "onERC721Received(address,address,uint256,bytes)" + ); + defaultExtension.functions[4] = IExtension.ExtensionFunction( + ERC1155Holder.onERC1155Received.selector, + "onERC1155Received(address,address,uint256,uint256,bytes)" + ); + defaultExtension.functions[5] = IExtension.ExtensionFunction( + bytes4(0), // Selector for `receive()` function. + "receive()" + ); + defaultExtension.functions[6] = IExtension.ExtensionFunction( + AccountExtension.isValidSignature.selector, + "isValidSignature(bytes32,bytes)" + ); + + IExtension.Extension[] memory extensions = new IExtension.Extension[](1); + extensions[0] = defaultExtension; + + // deploy account factory + vm.prank(factoryDeployer); + ManagedAccountFactory factory = new ManagedAccountFactory( + factoryDeployer, + IEntryPoint(payable(address(entrypoint))), + extensions + ); + assertTrue(address(factory) != address(0), "factory address should not be zero"); + } + + /*/////////////////////////////////////////////////////////////// + Test: creating an account + //////////////////////////////////////////////////////////////*/ + + /// @dev Create an account by directly calling the factory. + function test_state_createAccount_viaFactory() public { + vm.expectEmit(true, true, false, true); + emit AccountCreated(sender, accountAdmin); + accountFactory.createAccount(accountAdmin, data); + + address[] memory allAccounts = accountFactory.getAllAccounts(); + assertEq(allAccounts.length, 1); + assertEq(allAccounts[0], sender); + } + + /// @dev Create an account via Entrypoint. + function test_state_createAccount_viaEntrypoint() public { + bytes memory initCallData = abi.encodeWithSignature("createAccount(address,bytes)", accountAdmin, data); + bytes memory initCode = abi.encodePacked(abi.encodePacked(address(accountFactory)), initCallData); + + PackedUserOperation[] memory userOpCreateAccount = _setupUserOpExecute( + accountAdminPKey, + initCode, + address(0), + 0, + bytes("") + ); + + vm.expectEmit(true, true, false, true); + emit AccountCreated(sender, accountAdmin); + EntryPoint(entrypoint).handleOps(userOpCreateAccount, beneficiary); + + address[] memory allAccounts = accountFactory.getAllAccounts(); + assertEq(allAccounts.length, 1); + assertEq(allAccounts[0], sender); + } + + /// @dev Try registering with factory with a contract not deployed by factory. + function test_revert_onRegister_nonFactoryChildContract() public { + vm.prank(address(0x12345)); + vm.expectRevert("AccountFactory: not an account."); + accountFactory.onRegister(_generateSalt(accountAdmin, "")); + } + + /// @dev Create more than one accounts with the same admin. + function test_state_createAccount_viaEntrypoint_multipleAccountSameAdmin() public { + uint256 amount = 1; + + for (uint256 i = 0; i < amount; i += 1) { + bytes memory initCallData = abi.encodeWithSignature( + "createAccount(address,bytes)", + accountAdmin, + bytes(abi.encode(i)) + ); + bytes memory initCode = abi.encodePacked(abi.encodePacked(address(accountFactory)), initCallData); + + address expectedSenderAddress = Clones.predictDeterministicAddress( + accountFactory.accountImplementation(), + _generateSalt(accountAdmin, bytes(abi.encode(i))), + address(accountFactory) + ); + + PackedUserOperation[] memory userOpCreateAccount = _setupUserOpExecuteWithSender( + initCode, + address(0), + 0, + bytes(abi.encode(i)), + expectedSenderAddress + ); + + vm.expectEmit(true, true, false, true); + emit AccountCreated(expectedSenderAddress, accountAdmin); + EntryPoint(entrypoint).handleOps(userOpCreateAccount, beneficiary); + } + + address[] memory allAccounts = accountFactory.getAllAccounts(); + assertEq(allAccounts.length, amount); + + for (uint256 i = 0; i < amount; i += 1) { + assertEq( + allAccounts[i], + Clones.predictDeterministicAddress( + accountFactory.accountImplementation(), + _generateSalt(accountAdmin, bytes(abi.encode(i))), + address(accountFactory) + ) + ); + } + } + + /*/////////////////////////////////////////////////////////////// + Test: performing a contract call + //////////////////////////////////////////////////////////////*/ + + function _setup_executeTransaction() internal { + bytes memory initCallData = abi.encodeWithSignature("createAccount(address,bytes)", accountAdmin, data); + bytes memory initCode = abi.encodePacked(abi.encodePacked(address(accountFactory)), initCallData); + + PackedUserOperation[] memory userOpCreateAccount = _setupUserOpExecute( + accountAdminPKey, + initCode, + address(0), + 0, + bytes("") + ); + + EntryPoint(entrypoint).handleOps(userOpCreateAccount, beneficiary); + } + + /// @dev Perform a state changing transaction directly via account. + function test_state_executeTransaction() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + assertEq(numberContract.num(), 0); + + vm.prank(accountAdmin); + SimpleAccount(payable(account)).execute( + address(numberContract), + 0, + abi.encodeWithSignature("setNum(uint256)", 42) + ); + + assertEq(numberContract.num(), 42); + } + + /// @dev Perform many state changing transactions in a batch directly via account. + function test_state_executeBatchTransaction() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + assertEq(numberContract.num(), 0); + + uint256 count = 3; + address[] memory targets = new address[](count); + uint256[] memory values = new uint256[](count); + bytes[] memory callData = new bytes[](count); + + for (uint256 i = 0; i < count; i += 1) { + targets[i] = address(numberContract); + values[i] = 0; + callData[i] = abi.encodeWithSignature("incrementNum()", i); + } + + vm.prank(accountAdmin); + SimpleAccount(payable(account)).executeBatch(targets, values, callData); + + assertEq(numberContract.num(), count); + } + + /// @dev Perform a state changing transaction via Entrypoint. + function test_state_executeTransaction_viaEntrypoint() public { + _setup_executeTransaction(); + + assertEq(numberContract.num(), 0); + + PackedUserOperation[] memory userOp = _setupUserOpExecute( + accountAdminPKey, + bytes(""), + address(numberContract), + 0, + abi.encodeWithSignature("setNum(uint256)", 42) + ); + + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + + assertEq(numberContract.num(), 42); + } + + /// @dev Perform many state changing transactions in a batch via Entrypoint. + function test_state_executeBatchTransaction_viaEntrypoint() public { + _setup_executeTransaction(); + + assertEq(numberContract.num(), 0); + + uint256 count = 3; + address[] memory targets = new address[](count); + uint256[] memory values = new uint256[](count); + bytes[] memory callData = new bytes[](count); + + for (uint256 i = 0; i < count; i += 1) { + targets[i] = address(numberContract); + values[i] = 0; + callData[i] = abi.encodeWithSignature("incrementNum()", i); + } + + PackedUserOperation[] memory userOp = _setupUserOpExecuteBatch( + accountAdminPKey, + bytes(""), + targets, + values, + callData + ); + + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + + assertEq(numberContract.num(), count); + } + + /// @dev Perform a state changing transaction via Entrypoint and a SIGNER_ROLE holder. + function test_state_executeTransaction_viaAccountSigner() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + address[] memory approvedTargets = new address[](1); + approvedTargets[0] = address(numberContract); + + IAccountPermissions.SignerPermissionRequest memory permissionsReq = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 0, + approvedTargets, + 1 ether, + 0, + type(uint128).max, + 0, + type(uint128).max, + uidCache + ); + + vm.prank(accountAdmin); + bytes memory sig = _signSignerPermissionRequest(permissionsReq); + SimpleAccount(payable(account)).setPermissionsForSigner(permissionsReq, sig); + + assertEq(numberContract.num(), 0); + + PackedUserOperation[] memory userOp = _setupUserOpExecute( + accountSignerPKey, + bytes(""), + address(numberContract), + 0, + abi.encodeWithSignature("setNum(uint256)", 42) + ); + + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + + assertEq(numberContract.num(), 42); + } + + /// @dev Revert: perform a state changing transaction via Entrypoint without appropriate permissions. + function test_revert_executeTransaction_nonSigner_viaEntrypoint() public { + _setup_executeTransaction(); + + assertEq(numberContract.num(), 0); + + PackedUserOperation[] memory userOp = _setupUserOpExecute( + accountSignerPKey, + bytes(""), + address(numberContract), + 0, + abi.encodeWithSignature("setNum(uint256)", 42) + ); + + vm.expectRevert(); + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + } + + /// @dev Perform many state changing transactions in a batch via Entrypoint. + function test_state_executeBatchTransaction_viaAccountSigner() public { + _setup_executeTransaction(); + + assertEq(numberContract.num(), 0); + + uint256 count = 3; + address[] memory targets = new address[](count); + uint256[] memory values = new uint256[](count); + bytes[] memory callData = new bytes[](count); + + for (uint256 i = 0; i < count; i += 1) { + targets[i] = address(numberContract); + values[i] = 0; + callData[i] = abi.encodeWithSignature("incrementNum()", i); + } + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + address[] memory approvedTargets = new address[](1); + approvedTargets[0] = address(numberContract); + + IAccountPermissions.SignerPermissionRequest memory permissionsReq = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 0, + approvedTargets, + 1 ether, + 0, + type(uint128).max, + 0, + type(uint128).max, + uidCache + ); + + vm.prank(accountAdmin); + bytes memory sig = _signSignerPermissionRequest(permissionsReq); + SimpleAccount(payable(account)).setPermissionsForSigner(permissionsReq, sig); + + PackedUserOperation[] memory userOp = _setupUserOpExecuteBatch( + accountSignerPKey, + bytes(""), + targets, + values, + callData + ); + + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + + assertEq(numberContract.num(), count); + } + + /// @dev Revert: non-admin performs a state changing transaction directly via account contract. + function test_revert_executeTransaction_nonSigner_viaDirectCall() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + address[] memory approvedTargets = new address[](1); + approvedTargets[0] = address(numberContract); + + IAccountPermissions.SignerPermissionRequest memory permissionsReq = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 0, + approvedTargets, + 1 ether, + 0, + type(uint128).max, + 0, + type(uint128).max, + uidCache + ); + + vm.prank(accountAdmin); + bytes memory sig = _signSignerPermissionRequest(permissionsReq); + SimpleAccount(payable(account)).setPermissionsForSigner(permissionsReq, sig); + + assertEq(numberContract.num(), 0); + + vm.prank(accountSigner); + vm.expectRevert("Account: not admin or EntryPoint."); + SimpleAccount(payable(account)).execute( + address(numberContract), + 0, + abi.encodeWithSignature("setNum(uint256)", 42) + ); + } + + /*/////////////////////////////////////////////////////////////// + Test: receiving and sending native tokens + //////////////////////////////////////////////////////////////*/ + + /// @dev Send native tokens to an account. + function test_state_accountReceivesNativeTokens() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + assertEq(address(account).balance, 0); + + vm.prank(accountAdmin); + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory ret) = payable(account).call{ value: 1000 }(""); + + assertEq(address(account).balance, 1000); + + // Silence warning: Return value of low-level calls not used. + (success, ret) = (success, ret); + } + + /// @dev Transfer native tokens out of an account. + function test_state_transferOutsNativeTokens() public { + _setup_executeTransaction(); + + uint256 value = 1000; + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + vm.prank(accountAdmin); + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory ret) = payable(account).call{ value: value }(""); + assertEq(address(account).balance, value); + + // Silence warning: Return value of low-level calls not used. + (success, ret) = (success, ret); + + address recipient = address(0x3456); + + PackedUserOperation[] memory userOp = _setupUserOpExecute( + accountAdminPKey, + bytes(""), + recipient, + value, + bytes("") + ); + + EntryPoint(entrypoint).handleOps(userOp, beneficiary); + assertEq(address(account).balance, 0); + assertEq(recipient.balance, value); + } + + /// @dev Add and remove a deposit for the account from the Entrypoint. + + function test_state_addAndWithdrawDeposit() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + assertEq(EntryPoint(entrypoint).balanceOf(account), 0); + + vm.prank(accountAdmin); + SimpleAccount(payable(account)).addDeposit{ value: 1000 }(); + assertEq(EntryPoint(entrypoint).balanceOf(account), 1000); + + vm.prank(accountAdmin); + SimpleAccount(payable(account)).withdrawDepositTo(payable(accountSigner), 500); + assertEq(EntryPoint(entrypoint).balanceOf(account), 500); + } + + /*/////////////////////////////////////////////////////////////// + Test: receiving ERC-721 and ERC-1155 NFTs + //////////////////////////////////////////////////////////////*/ + + /// @dev Send an ERC-721 NFT to an account. + function test_state_receiveERC721NFT() public { + _setup_executeTransaction(); + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + assertEq(erc721.balanceOf(account), 0); + + erc721.mint(account, 1); + + assertEq(erc721.balanceOf(account), 1); + } + + /// @dev Send an ERC-1155 NFT to an account. + function test_state_receiveERC1155NFT() public { + _setup_executeTransaction(); + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + assertEq(erc1155.balanceOf(account, 0), 0); + + erc1155.mint(account, 0, 1); + + assertEq(erc1155.balanceOf(account, 0), 1); + } + + /*/////////////////////////////////////////////////////////////// + Test: change an extension on the account + //////////////////////////////////////////////////////////////*/ + + /// @dev Make the account reject ERC-721 NFTs instead of accepting them. + function test_scenario_changeExtensionForFunction() public { + _setup_executeTransaction(); + address account = accountFactory.getAddress(accountAdmin, bytes("")); + + // The account can initially receive NFTs. + assertEq(erc721.balanceOf(account), 0); + erc721.mint(account, 1); + assertEq(erc721.balanceOf(account), 1); + + // Make the account reject ERC-721 NFTs going forward. + IExtension.Extension memory extension; + + extension.metadata = IExtension.ExtensionMetadata({ + name: "NFTRejector", + metadataURI: "ipfs://NFTRejector", + implementation: address(new NFTRejector()) + }); + + extension.functions = new IExtension.ExtensionFunction[](1); + + extension.functions[0] = IExtension.ExtensionFunction( + NFTRejector.onERC721Received.selector, + "onERC721Received(address,address,uint256,bytes)" + ); + + vm.prank(factoryDeployer); + accountFactory.disableFunctionInExtension("AccountExtension", NFTRejector.onERC721Received.selector); + + vm.prank(factoryDeployer); + accountFactory.addExtension(extension); + + // Transfer NFTs to the account + erc721.mint(accountSigner, 1); + assertEq(erc721.ownerOf(1), accountSigner); + vm.prank(accountSigner); + vm.expectRevert("NFTs not accepted"); + erc721.safeTransferFrom(accountSigner, account, 1); + } +} diff --git a/src/test/smart-wallet/account-core/isValidSigner.t.sol b/src/test/smart-wallet/account-core/isValidSigner.t.sol new file mode 100644 index 000000000..59bb0e1bf --- /dev/null +++ b/src/test/smart-wallet/account-core/isValidSigner.t.sol @@ -0,0 +1,568 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test utils +import { BaseTest } from "../../utils/BaseTest.sol"; +import "contracts/external-deps/openzeppelin/proxy/Clones.sol"; +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; +import { IAccountPermissions } from "contracts/extension/interface/IAccountPermissions.sol"; +import { AccountPermissions, EnumerableSet, ECDSA } from "contracts/extension/upgradeable/AccountPermissions.sol"; + +// Account Abstraction setup for smart wallets. +import { EntryPoint, IEntryPoint } from "contracts/prebuilts/account/utils/EntryPoint.sol"; +import { PackedUserOperation } from "contracts/prebuilts/account/interfaces/PackedUserOperation.sol"; + +// Target +import { DynamicAccountFactory, DynamicAccount, BaseAccountFactory } from "contracts/prebuilts/account/dynamic/DynamicAccountFactory.sol"; + +/// @dev This is a dummy contract to test contract interactions with Account. +contract Number { + uint256 public num; + + function setNum(uint256 _num) public { + num = _num; + } + + function doubleNum() public { + num *= 2; + } + + function incrementNum() public { + num += 1; + } +} + +contract MyDynamicAccount is DynamicAccount { + using EnumerableSet for EnumerableSet.AddressSet; + + constructor( + IEntryPoint _entrypoint, + Extension[] memory _defaultExtensions + ) DynamicAccount(_entrypoint, _defaultExtensions) {} + + function setPermissionsForSigner( + address _signer, + uint256 _nativeTokenLimit, + uint256 _startTimestamp, + uint256 _endTimestamp + ) public { + _accountPermissionsStorage().signerPermissions[_signer] = SignerPermissionsStatic( + _nativeTokenLimit, + uint128(_startTimestamp), + uint128(_endTimestamp) + ); + } + + function setApprovedTargetsForSigner(address _signer, address[] memory _approvedTargets) public { + uint256 len = _approvedTargets.length; + for (uint256 i = 0; i < len; i += 1) { + _accountPermissionsStorage().approvedTargets[_signer].add(_approvedTargets[i]); + } + } + + function _setAdmin(address _account, bool _isAdmin) internal virtual override { + _accountPermissionsStorage().isAdmin[_account] = _isAdmin; + } + + function _isAuthorizedCallToUpgrade() internal view virtual override returns (bool) {} +} + +contract AccountCoreTest_isValidSigner is BaseTest { + // Target contracts + EntryPoint private entrypoint; + DynamicAccountFactory private accountFactory; + MyDynamicAccount private account; + + // Mocks + Number internal numberContract; + + // Test params + uint256 private accountAdminPKey = 100; + address private accountAdmin; + + uint256 private accountSignerPKey = 200; + address private accountSigner; + + uint256 private nonSignerPKey = 300; + address private nonSigner; + + address private opSigner; + uint256 private startTimestamp; + uint256 private endTimestamp; + uint256 private nativeTokenLimit; + PackedUserOperation private op; + + bytes internal data = bytes(""); + + function _setupUserOp( + uint256 _signerPKey, + bytes memory _initCode, + bytes memory _callDataForEntrypoint + ) internal returns (PackedUserOperation memory) { + uint256 nonce = entrypoint.getNonce(address(account), 0); + PackedUserOperation memory op; + + { + uint128 verificationGasLimit = 5_000_000; + uint128 callGasLimit = 5_000_000; + bytes32 packedGasLimits = (bytes32(uint256(verificationGasLimit)) << 128) | bytes32(uint256(callGasLimit)); + + // Get user op fields + op = PackedUserOperation({ + sender: address(account), + nonce: nonce, + initCode: _initCode, + callData: _callDataForEntrypoint, + accountGasLimits: packedGasLimits, + preVerificationGas: 5_000_000, + gasFees: 0, + paymasterAndData: bytes(""), + signature: bytes("") + }); + } + + // Sign UserOp + bytes32 opHash = EntryPoint(entrypoint).getUserOpHash(op); + bytes32 msgHash = ECDSA.toEthSignedMessageHash(opHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signerPKey, msgHash); + bytes memory userOpSignature = abi.encodePacked(r, s, v); + + address recoveredSigner = ECDSA.recover(msgHash, v, r, s); + address expectedSigner = vm.addr(_signerPKey); + assertEq(recoveredSigner, expectedSigner); + + op.signature = userOpSignature; + + return op; + } + + function _setupUserOpExecute( + uint256 _signerPKey, + bytes memory _initCode, + address _target, + uint256 _value, + bytes memory _callData + ) internal returns (PackedUserOperation memory) { + bytes memory callDataForEntrypoint = abi.encodeWithSignature( + "execute(address,uint256,bytes)", + _target, + _value, + _callData + ); + + return _setupUserOp(_signerPKey, _initCode, callDataForEntrypoint); + } + + function _setupUserOpExecuteBatch( + uint256 _signerPKey, + bytes memory _initCode, + address[] memory _targets, + uint256[] memory _values, + bytes[] memory _callData + ) internal returns (PackedUserOperation memory) { + bytes memory callDataForEntrypoint = abi.encodeWithSignature( + "executeBatch(address[],uint256[],bytes[])", + _targets, + _values, + _callData + ); + + return _setupUserOp(_signerPKey, _initCode, callDataForEntrypoint); + } + + function _setupUserOpInvalidFunction( + uint256 _signerPKey, + bytes memory _initCode + ) internal returns (PackedUserOperation memory) { + bytes memory callDataForEntrypoint = abi.encodeWithSignature("invalidFunction()"); + + return _setupUserOp(_signerPKey, _initCode, callDataForEntrypoint); + } + + function setUp() public override { + super.setUp(); + + // Setup signers. + accountAdmin = vm.addr(accountAdminPKey); + vm.deal(accountAdmin, 100 ether); + + accountSigner = vm.addr(accountSignerPKey); + nonSigner = vm.addr(nonSignerPKey); + + // Setup contracts + entrypoint = new EntryPoint(); + + IExtension.Extension[] memory extensions; + + // deploy account factory + accountFactory = new DynamicAccountFactory(deployer, extensions); + // deploy dummy contract + numberContract = new Number(); + + address accountImpl = address(new MyDynamicAccount(IEntryPoint(payable(address(entrypoint))), extensions)); + address _account = Clones.cloneDeterministic(accountImpl, "salt"); + account = MyDynamicAccount(payable(_account)); + account.initialize(accountAdmin, ""); + } + + function test_isValidSigner_whenSignerIsAdmin() public { + opSigner = accountAdmin; + PackedUserOperation memory _op; // empty op since it's not relevant for this check + bool isValid = DynamicAccount(payable(account)).isValidSigner(opSigner, _op); + + assertTrue(isValid); + } + + modifier whenNotAdmin() { + opSigner = accountSigner; + _; + } + + function test_isValidSigner_invalidTimestamps() public whenNotAdmin { + PackedUserOperation memory _op; // empty op since it's not relevant for this check + startTimestamp = 100; + endTimestamp = 200; + account.setPermissionsForSigner(opSigner, nativeTokenLimit, startTimestamp, endTimestamp); + + vm.warp(201); // block timestamp greater than end timestamp + bool isValid = account.isValidSigner(opSigner, _op); + + assertFalse(isValid); + + vm.warp(200); // block timestamp equal to end timestamp + isValid = account.isValidSigner(opSigner, _op); + + assertFalse(isValid); + + vm.warp(99); // block timestamp less than start timestamp + isValid = account.isValidSigner(opSigner, _op); + + assertFalse(isValid); + } + + modifier whenValidTimestamps() { + startTimestamp = 100; + endTimestamp = 200; + vm.warp(150); // block timestamp within start and end timestamps + _; + } + + function test_isValidSigner_noApprovedTargets() public whenNotAdmin whenValidTimestamps { + PackedUserOperation memory _op; // empty op since it's not relevant for this check + address[] memory _approvedTargets; + account.setPermissionsForSigner(opSigner, nativeTokenLimit, startTimestamp, endTimestamp); + account.setApprovedTargetsForSigner(opSigner, _approvedTargets); + + bool isValid = account.isValidSigner(opSigner, _op); + + assertFalse(isValid); + } + + // ================== + // ======= Test branch: wildcard + // ================== + + function test_isValidSigner_wildcardExecute_breachNativeTokenLimit() public whenNotAdmin whenValidTimestamps { + // set wildcard + address[] memory _approvedTargets = new address[](1); + _approvedTargets[0] = address(0); + account.setApprovedTargetsForSigner(opSigner, _approvedTargets); + + // user op execute + op = _setupUserOpExecute(accountSignerPKey, bytes(""), address(0x123), 10, bytes("")); + + account.setPermissionsForSigner(opSigner, nativeTokenLimit, startTimestamp, endTimestamp); + + bool isValid = account.isValidSigner(opSigner, op); + + assertFalse(isValid); + } + + function test_isValidSigner_wildcardExecuteBatch_breachNativeTokenLimit() public whenNotAdmin whenValidTimestamps { + // set wildcard + address[] memory _approvedTargets = new address[](1); + _approvedTargets[0] = address(0); + account.setApprovedTargetsForSigner(opSigner, _approvedTargets); + + // user op execute + uint256 count = 3; + address[] memory targets = new address[](count); + uint256[] memory values = new uint256[](count); + bytes[] memory callData = new bytes[](count); + + for (uint256 i = 0; i < count; i += 1) { + targets[i] = address(numberContract); + values[i] = 10; + callData[i] = abi.encodeWithSignature("incrementNum()", i); + } + + op = _setupUserOpExecuteBatch(accountSignerPKey, bytes(""), targets, values, callData); + + account.setPermissionsForSigner(opSigner, nativeTokenLimit, startTimestamp, endTimestamp); + + bool isValid = account.isValidSigner(opSigner, op); + + assertFalse(isValid); + } + + modifier whenWithinNativeTokenLimit() { + nativeTokenLimit = 1000; + _; + } + + function test_isValidSigner_wildcardExecute() public whenNotAdmin whenValidTimestamps whenWithinNativeTokenLimit { + // set wildcard + address[] memory _approvedTargets = new address[](1); + _approvedTargets[0] = address(0); + account.setApprovedTargetsForSigner(opSigner, _approvedTargets); + + // user op execute + op = _setupUserOpExecute(accountSignerPKey, bytes(""), address(0x123), 10, bytes("")); + + account.setPermissionsForSigner(opSigner, nativeTokenLimit, startTimestamp, endTimestamp); + + bool isValid = account.isValidSigner(opSigner, op); + + assertTrue(isValid); + } + + function test_isValidSigner_wildcardExecuteBatch() + public + whenNotAdmin + whenValidTimestamps + whenWithinNativeTokenLimit + { + // set wildcard + address[] memory _approvedTargets = new address[](1); + _approvedTargets[0] = address(0); + account.setApprovedTargetsForSigner(opSigner, _approvedTargets); + + // user op execute + uint256 count = 3; + address[] memory targets = new address[](count); + uint256[] memory values = new uint256[](count); + bytes[] memory callData = new bytes[](count); + + for (uint256 i = 0; i < count; i += 1) { + targets[i] = address(numberContract); + values[i] = 10; + callData[i] = abi.encodeWithSignature("incrementNum()", i); + } + + op = _setupUserOpExecuteBatch(accountSignerPKey, bytes(""), targets, values, callData); + + account.setPermissionsForSigner(opSigner, nativeTokenLimit, startTimestamp, endTimestamp); + + bool isValid = account.isValidSigner(opSigner, op); + + assertTrue(isValid); + } + + function test_isValidSigner_wildcardInvalidFunction() + public + whenNotAdmin + whenValidTimestamps + whenWithinNativeTokenLimit + { + // set wildcard + address[] memory _approvedTargets = new address[](1); + _approvedTargets[0] = address(0); + account.setApprovedTargetsForSigner(opSigner, _approvedTargets); + + // user op execute + op = _setupUserOpInvalidFunction(accountSignerPKey, bytes("")); + + account.setPermissionsForSigner(opSigner, nativeTokenLimit, startTimestamp, endTimestamp); + + bool isValid = account.isValidSigner(opSigner, op); + + assertFalse(isValid); + } + + // ================== + // ======= Test branch: not wildcard + // ================== + + function test_isValidSigner_execute_callingWrongTarget() + public + whenNotAdmin + whenValidTimestamps + whenWithinNativeTokenLimit + { + // set wildcard + address[] memory _approvedTargets = new address[](1); + _approvedTargets[0] = address(numberContract); + account.setApprovedTargetsForSigner(opSigner, _approvedTargets); + + // user op execute + address wrongTarget = address(0x123); + op = _setupUserOpExecute(accountSignerPKey, bytes(""), wrongTarget, 10, bytes("")); + + account.setPermissionsForSigner(opSigner, nativeTokenLimit, startTimestamp, endTimestamp); + + bool isValid = account.isValidSigner(opSigner, op); + + assertFalse(isValid); + } + + function test_isValidSigner_executeBatch_callingWrongTarget() + public + whenNotAdmin + whenValidTimestamps + whenWithinNativeTokenLimit + { + // set wildcard + address[] memory _approvedTargets = new address[](1); + _approvedTargets[0] = address(numberContract); + account.setApprovedTargetsForSigner(opSigner, _approvedTargets); + + // user op execute + uint256 count = 3; + address[] memory targets = new address[](count); + uint256[] memory values = new uint256[](count); + bytes[] memory callData = new bytes[](count); + address wrongTarget = address(0x123); + for (uint256 i = 0; i < count; i += 1) { + targets[i] = wrongTarget; + values[i] = 10; + callData[i] = abi.encodeWithSignature("incrementNum()", i); + } + + op = _setupUserOpExecuteBatch(accountSignerPKey, bytes(""), targets, values, callData); + + account.setPermissionsForSigner(opSigner, nativeTokenLimit, startTimestamp, endTimestamp); + + bool isValid = account.isValidSigner(opSigner, op); + + assertFalse(isValid); + } + + modifier whenCorrectTarget() { + _; + } + + function test_isValidSigner_execute_breachNativeTokenLimit() + public + whenNotAdmin + whenValidTimestamps + whenCorrectTarget + { + // set wildcard + address[] memory _approvedTargets = new address[](1); + _approvedTargets[0] = address(numberContract); + account.setApprovedTargetsForSigner(opSigner, _approvedTargets); + + // user op execute + op = _setupUserOpExecute(accountSignerPKey, bytes(""), address(numberContract), 10, bytes("")); + + account.setPermissionsForSigner(opSigner, nativeTokenLimit, startTimestamp, endTimestamp); + + bool isValid = account.isValidSigner(opSigner, op); + + assertFalse(isValid); + } + + function test_isValidSigner_executeBatch_breachNativeTokenLimit() + public + whenNotAdmin + whenValidTimestamps + whenCorrectTarget + { + // set wildcard + address[] memory _approvedTargets = new address[](1); + _approvedTargets[0] = address(numberContract); + account.setApprovedTargetsForSigner(opSigner, _approvedTargets); + + // user op execute + uint256 count = 3; + address[] memory targets = new address[](count); + uint256[] memory values = new uint256[](count); + bytes[] memory callData = new bytes[](count); + + for (uint256 i = 0; i < count; i += 1) { + targets[i] = address(numberContract); + values[i] = 10; + callData[i] = abi.encodeWithSignature("incrementNum()", i); + } + + op = _setupUserOpExecuteBatch(accountSignerPKey, bytes(""), targets, values, callData); + + account.setPermissionsForSigner(opSigner, nativeTokenLimit, startTimestamp, endTimestamp); + + bool isValid = account.isValidSigner(opSigner, op); + + assertFalse(isValid); + } + + function test_isValidSigner_execute() + public + whenNotAdmin + whenValidTimestamps + whenWithinNativeTokenLimit + whenCorrectTarget + { + // set wildcard + address[] memory _approvedTargets = new address[](1); + _approvedTargets[0] = address(numberContract); + account.setApprovedTargetsForSigner(opSigner, _approvedTargets); + + // user op execute + op = _setupUserOpExecute(accountSignerPKey, bytes(""), address(numberContract), 10, bytes("")); + + account.setPermissionsForSigner(opSigner, nativeTokenLimit, startTimestamp, endTimestamp); + + bool isValid = account.isValidSigner(opSigner, op); + + assertTrue(isValid); + } + + function test_isValidSigner_executeBatch() + public + whenNotAdmin + whenValidTimestamps + whenWithinNativeTokenLimit + whenCorrectTarget + { + // set wildcard + address[] memory _approvedTargets = new address[](1); + _approvedTargets[0] = address(numberContract); + account.setApprovedTargetsForSigner(opSigner, _approvedTargets); + + // user op execute + uint256 count = 3; + address[] memory targets = new address[](count); + uint256[] memory values = new uint256[](count); + bytes[] memory callData = new bytes[](count); + + for (uint256 i = 0; i < count; i += 1) { + targets[i] = address(numberContract); + values[i] = 10; + callData[i] = abi.encodeWithSignature("incrementNum()", i); + } + + op = _setupUserOpExecuteBatch(accountSignerPKey, bytes(""), targets, values, callData); + + account.setPermissionsForSigner(opSigner, nativeTokenLimit, startTimestamp, endTimestamp); + + bool isValid = account.isValidSigner(opSigner, op); + + assertTrue(isValid); + } + + function test_isValidSigner_invalidFunction() public whenNotAdmin whenValidTimestamps whenWithinNativeTokenLimit { + // set wildcard + address[] memory _approvedTargets = new address[](1); + _approvedTargets[0] = address(numberContract); + account.setApprovedTargetsForSigner(opSigner, _approvedTargets); + + // user op execute + op = _setupUserOpInvalidFunction(accountSignerPKey, bytes("")); + + account.setPermissionsForSigner(opSigner, nativeTokenLimit, startTimestamp, endTimestamp); + + bool isValid = account.isValidSigner(opSigner, op); + + assertFalse(isValid); + } +} diff --git a/src/test/smart-wallet/account-core/isValidSigner.tree b/src/test/smart-wallet/account-core/isValidSigner.tree new file mode 100644 index 000000000..cc4497260 --- /dev/null +++ b/src/test/smart-wallet/account-core/isValidSigner.tree @@ -0,0 +1,46 @@ +isValidSigner(address _signer, UserOperation calldata _userOp) +├── when `_signer` is admin +│ └── it should return true +├── when `_signer` is not admin + └── when timestamp is invalid + │ └── it should return false + └── when timestamp is valid + └── when no approved targets + │ └── it should return false + │ + │ // Case - Wildcard + └── when approved targets length is equal to 1 and contains address(0) + │ └── when calling `execute` function + │ │ └── when the decoded `value` is more than nativeTokenLimitPerTransaction + │ │ │ └── it should return false + │ │ └── when the decoded `value` is within nativeTokenLimitPerTransaction + │ │ └── it should return true + │ └── when calling `batchExecute` function + │ │ └── when any item in the decoded `values` array is more than nativeTokenLimitPerTransaction + │ │ │ └── it should return false + │ │ └── when all items in the decoded `values` array are within nativeTokenLimitPerTransaction + │ │ └── it should return true + │ └── when calling an invalid function + │ └── it should return false + │ + │ // Case - No Wildcard + └── when approved targets length is greater than 1, or doesn't contain address(0) + └── when calling `execute` function + │ └── when approvedTargets doesn't contain the decoded `target` + │ │ └── it should return false + │ └── when approvedTargets contains the decoded `target` + │ └── when the decoded `value` is more than nativeTokenLimitPerTransaction + │ │ └── it should return false + │ └── when the decoded `value` is within nativeTokenLimitPerTransaction + │ └── it should return true + └── when calling `batchExecute` function + │ └── when approvedTargets doesn't contain one or more items in the decoded `targets` array + │ │ └── it should return false + │ └── when approvedTargets contains all items in the decdoded `targets` array + │ └── when any item in the decoded `values` array is more than nativeTokenLimitPerTransaction + │ │ └── it should return false + │ └── when all items in the decoded `values` array are within nativeTokenLimitPerTransaction + │ └── it should return true + └── when calling an invalid function + └── it should return false + \ No newline at end of file diff --git a/src/test/smart-wallet/account-permissions/setPermissionsForSigner.t.sol b/src/test/smart-wallet/account-permissions/setPermissionsForSigner.t.sol new file mode 100644 index 000000000..d5f63e08a --- /dev/null +++ b/src/test/smart-wallet/account-permissions/setPermissionsForSigner.t.sol @@ -0,0 +1,635 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test utils +import "../../utils/BaseTest.sol"; +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; +import { IAccountPermissions } from "contracts/extension/interface/IAccountPermissions.sol"; +import { AccountPermissions } from "contracts/extension/upgradeable/AccountPermissions.sol"; +import { AccountExtension } from "contracts/prebuilts/account/utils/AccountExtension.sol"; + +// Account Abstraction setup for smart wallets. +import { EntryPoint, IEntryPoint } from "contracts/prebuilts/account/utils/EntryPoint.sol"; +import { PackedUserOperation } from "contracts/prebuilts/account/interfaces/PackedUserOperation.sol"; + +// Target +import { Account as SimpleAccount } from "contracts/prebuilts/account/non-upgradeable/Account.sol"; +import { DynamicAccountFactory, DynamicAccount } from "contracts/prebuilts/account/dynamic/DynamicAccountFactory.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +/// @dev This is a dummy contract to test contract interactions with Account. +contract Number { + uint256 public num; + + function setNum(uint256 _num) public { + num = _num; + } + + function doubleNum() public { + num *= 2; + } + + function incrementNum() public { + num += 1; + } +} + +contract NFTRejector { + function onERC721Received(address, address, uint256, bytes memory) public virtual returns (bytes4) { + revert("NFTs not accepted"); + } +} + +contract AccountPermissionsTest_setPermissionsForSigner is BaseTest { + event AdminUpdated(address indexed signer, bool isAdmin); + + event SignerPermissionsUpdated( + address indexed authorizingSigner, + address indexed targetSigner, + IAccountPermissions.SignerPermissionRequest permissions + ); + + // Target contracts + EntryPoint private constant entrypoint = EntryPoint(payable(0x0000000071727De22E5E9d8BAf0edAc6f37da032)); + DynamicAccountFactory private accountFactory; + + // Mocks + Number internal numberContract; + + // Test params + uint256 private accountAdminPKey = 100; + address private accountAdmin; + + uint256 private accountSignerPKey = 200; + address private accountSigner; + + uint256 private nonSignerPKey = 300; + address private nonSigner; + + bytes internal data = bytes(""); + + // UserOp terminology: `sender` is the smart wallet. + address private sender = 0x78b942FBC9126b4Ed8384Bb9dd1420Ea865be91a; + address payable private beneficiary = payable(address(0x45654)); + + bytes32 private uidCache = bytes32("random uid"); + + event AccountCreated(address indexed account, address indexed accountAdmin); + + function _prepareSignature( + IAccountPermissions.SignerPermissionRequest memory _req + ) internal view returns (bytes32 typedDataHash) { + bytes32 typehashSignerPermissionRequest = keccak256( + "SignerPermissionRequest(address signer,uint8 isAdmin,address[] approvedTargets,uint256 nativeTokenLimitPerTransaction,uint128 permissionStartTimestamp,uint128 permissionEndTimestamp,uint128 reqValidityStartTimestamp,uint128 reqValidityEndTimestamp,bytes32 uid)" + ); + bytes32 nameHash = keccak256(bytes("Account")); + bytes32 versionHash = keccak256(bytes("1")); + bytes32 typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + bytes32 domainSeparator = keccak256(abi.encode(typehashEip712, nameHash, versionHash, block.chainid, sender)); + + bytes memory encodedRequestStart = abi.encode( + typehashSignerPermissionRequest, + _req.signer, + _req.isAdmin, + keccak256(abi.encodePacked(_req.approvedTargets)), + _req.nativeTokenLimitPerTransaction + ); + + bytes memory encodedRequestEnd = abi.encode( + _req.permissionStartTimestamp, + _req.permissionEndTimestamp, + _req.reqValidityStartTimestamp, + _req.reqValidityEndTimestamp, + _req.uid + ); + + bytes32 structHash = keccak256(bytes.concat(encodedRequestStart, encodedRequestEnd)); + typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + } + + function _signSignerPermissionRequest( + IAccountPermissions.SignerPermissionRequest memory _req + ) internal view returns (bytes memory signature) { + bytes32 typedDataHash = _prepareSignature(_req); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(accountAdminPKey, typedDataHash); + signature = abi.encodePacked(r, s, v); + } + + function _signSignerPermissionRequestInvalid( + IAccountPermissions.SignerPermissionRequest memory _req + ) internal view returns (bytes memory signature) { + bytes32 typedDataHash = _prepareSignature(_req); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(0x111, typedDataHash); + signature = abi.encodePacked(r, s, v); + } + + function _setupUserOp( + uint256 _signerPKey, + bytes memory _initCode, + bytes memory _callDataForEntrypoint + ) internal returns (PackedUserOperation[] memory ops) { + uint256 nonce = entrypoint.getNonce(sender, 0); + PackedUserOperation memory op; + + { + uint128 verificationGasLimit = 5_000_000; + uint128 callGasLimit = 5_000_000; + bytes32 packedGasLimits = (bytes32(uint256(verificationGasLimit)) << 128) | bytes32(uint256(callGasLimit)); + + // Get user op fields + op = PackedUserOperation({ + sender: sender, + nonce: nonce, + initCode: _initCode, + callData: _callDataForEntrypoint, + accountGasLimits: packedGasLimits, + preVerificationGas: 5_000_000, + gasFees: 0, + paymasterAndData: bytes(""), + signature: bytes("") + }); + } + + // Sign UserOp + bytes32 opHash = EntryPoint(entrypoint).getUserOpHash(op); + bytes32 msgHash = ECDSA.toEthSignedMessageHash(opHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signerPKey, msgHash); + bytes memory userOpSignature = abi.encodePacked(r, s, v); + + address recoveredSigner = ECDSA.recover(msgHash, v, r, s); + address expectedSigner = vm.addr(_signerPKey); + assertEq(recoveredSigner, expectedSigner); + + op.signature = userOpSignature; + + // Store UserOp + ops = new PackedUserOperation[](1); + ops[0] = op; + } + + function _setupUserOpExecute( + uint256 _signerPKey, + bytes memory _initCode, + address _target, + uint256 _value, + bytes memory _callData + ) internal returns (PackedUserOperation[] memory) { + bytes memory callDataForEntrypoint = abi.encodeWithSignature( + "execute(address,uint256,bytes)", + _target, + _value, + _callData + ); + + return _setupUserOp(_signerPKey, _initCode, callDataForEntrypoint); + } + + function _setupUserOpExecuteBatch( + uint256 _signerPKey, + bytes memory _initCode, + address[] memory _target, + uint256[] memory _value, + bytes[] memory _callData + ) internal returns (PackedUserOperation[] memory) { + bytes memory callDataForEntrypoint = abi.encodeWithSignature( + "executeBatch(address[],uint256[],bytes[])", + _target, + _value, + _callData + ); + + return _setupUserOp(_signerPKey, _initCode, callDataForEntrypoint); + } + + function setUp() public override { + super.setUp(); + + // Setup signers. + accountAdmin = vm.addr(accountAdminPKey); + vm.deal(accountAdmin, 100 ether); + + accountSigner = vm.addr(accountSignerPKey); + nonSigner = vm.addr(nonSignerPKey); + + // Setup contracts + address _deployedEntrypoint = address(new EntryPoint()); + vm.etch(address(entrypoint), bytes(_deployedEntrypoint.code)); + + // Setting up default extension. + IExtension.Extension memory defaultExtension; + + defaultExtension.metadata = IExtension.ExtensionMetadata({ + name: "AccountExtension", + metadataURI: "ipfs://AccountExtension", + implementation: address(new AccountExtension()) + }); + + defaultExtension.functions = new IExtension.ExtensionFunction[](7); + + defaultExtension.functions[0] = IExtension.ExtensionFunction( + AccountExtension.supportsInterface.selector, + "supportsInterface(bytes4)" + ); + defaultExtension.functions[1] = IExtension.ExtensionFunction( + AccountExtension.execute.selector, + "execute(address,uint256,bytes)" + ); + defaultExtension.functions[2] = IExtension.ExtensionFunction( + AccountExtension.executeBatch.selector, + "executeBatch(address[],uint256[],bytes[])" + ); + defaultExtension.functions[3] = IExtension.ExtensionFunction( + ERC721Holder.onERC721Received.selector, + "onERC721Received(address,address,uint256,bytes)" + ); + defaultExtension.functions[4] = IExtension.ExtensionFunction( + ERC1155Holder.onERC1155Received.selector, + "onERC1155Received(address,address,uint256,uint256,bytes)" + ); + defaultExtension.functions[5] = IExtension.ExtensionFunction( + bytes4(0), // Selector for `receive()` function. + "receive()" + ); + defaultExtension.functions[6] = IExtension.ExtensionFunction( + AccountExtension.isValidSignature.selector, + "isValidSignature(bytes32,bytes)" + ); + + IExtension.Extension[] memory extensions = new IExtension.Extension[](1); + extensions[0] = defaultExtension; + + // deploy account factory + accountFactory = new DynamicAccountFactory(deployer, extensions); + // deploy dummy contract + numberContract = new Number(); + } + + function _setup_executeTransaction() internal { + bytes memory initCallData = abi.encodeWithSignature("createAccount(address,bytes)", accountAdmin, data); + bytes memory initCode = abi.encodePacked(abi.encodePacked(address(accountFactory)), initCallData); + + PackedUserOperation[] memory userOpCreateAccount = _setupUserOpExecute( + accountAdminPKey, + initCode, + address(0), + 0, + bytes("") + ); + + EntryPoint(entrypoint).handleOps(userOpCreateAccount, beneficiary); + } + + function test_state_targetAdminNotAdmin() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + address[] memory approvedTargets = new address[](0); + + bool adminStatusBefore = SimpleAccount(payable(account)).isAdmin(accountSigner); + + IAccountPermissions.SignerPermissionRequest memory permissionsReq = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 1, + approvedTargets, + 0, + 0, + type(uint128).max, + 0, + type(uint128).max, + uidCache + ); + + vm.prank(accountAdmin); + bytes memory sig = _signSignerPermissionRequest(permissionsReq); + SimpleAccount(payable(account)).setPermissionsForSigner(permissionsReq, sig); + + bool adminStatusAfter = SimpleAccount(payable(account)).isAdmin(accountSigner); + + assertEq(adminStatusBefore, false); + assertEq(adminStatusAfter, true); + } + + function test_state_targetAdminIsAdmin() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + address[] memory approvedTargets = new address[](0); + + { + IAccountPermissions.SignerPermissionRequest memory request = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 1, + approvedTargets, + 1, + 0, + type(uint128).max, + 0, + type(uint128).max, + uidCache + ); + + bytes memory sig2 = _signSignerPermissionRequest(request); + SimpleAccount(payable(account)).setPermissionsForSigner(request, sig2); + + address[] memory adminsBefore = SimpleAccount(payable(account)).getAllAdmins(); + assertEq(adminsBefore[1], accountSigner); + } + + bool adminStatusBefore = SimpleAccount(payable(account)).isAdmin(accountAdmin); + + uidCache = bytes32("new uid"); + + IAccountPermissions.SignerPermissionRequest memory req = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 2, + approvedTargets, + 1, + 0, + type(uint128).max, + 0, + type(uint128).max, + uidCache + ); + + bytes memory sig3 = _signSignerPermissionRequest(req); + SimpleAccount(payable(account)).setPermissionsForSigner(req, sig3); + + bool adminStatusAfter = SimpleAccount(payable(account)).isAdmin(accountSigner); + address[] memory adminsAfter = SimpleAccount(payable(account)).getAllAdmins(); + + assertEq(adminStatusBefore, true); + assertEq(adminStatusAfter, false); + assertEq(adminsAfter.length, 1); + } + + function test_revert_attemptReplayUID() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + address[] memory approvedTargets = new address[](0); + + IAccountPermissions.SignerPermissionRequest memory permissionsReq = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 1, + approvedTargets, + 1, + 0, + type(uint128).max, + 0, + type(uint128).max, + uidCache + ); + + bytes memory sig = _signSignerPermissionRequest(permissionsReq); + SimpleAccount(payable(account)).setPermissionsForSigner(permissionsReq, sig); + + // Attempt replay UID + + IAccountPermissions.SignerPermissionRequest memory permissionsReqTwo = IAccountPermissions + .SignerPermissionRequest( + accountSigner, + 1, + approvedTargets, + 0, + 0, + type(uint128).max, + 0, + type(uint128).max, + uidCache + ); + + sig = _signSignerPermissionRequest(permissionsReqTwo); + vm.expectRevert(); + SimpleAccount(payable(account)).setPermissionsForSigner(permissionsReqTwo, sig); + } + + function test_event_addAdmin_AdminUpdated() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + address[] memory approvedTargets = new address[](0); + + IAccountPermissions.SignerPermissionRequest memory permissionsReq = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 1, + approvedTargets, + 1, + 0, + type(uint128).max, + 0, + type(uint128).max, + uidCache + ); + + bytes memory sig = _signSignerPermissionRequest(permissionsReq); + + vm.expectEmit(true, false, false, true); + emit AdminUpdated(accountSigner, true); + SimpleAccount(payable(account)).setPermissionsForSigner(permissionsReq, sig); + } + + function test_event_removeAdmin_AdminUpdated() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + address[] memory approvedTargets = new address[](0); + + IAccountPermissions.SignerPermissionRequest memory permissionsReq = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 2, + approvedTargets, + 1, + 0, + type(uint128).max, + 0, + type(uint128).max, + uidCache + ); + + bytes memory sig = _signSignerPermissionRequest(permissionsReq); + + vm.expectEmit(true, false, false, true); + emit AdminUpdated(accountSigner, false); + SimpleAccount(payable(account)).setPermissionsForSigner(permissionsReq, sig); + } + + function test_revert_timeBeforeStart() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + address[] memory approvedTargets = new address[](0); + + IAccountPermissions.SignerPermissionRequest memory permissionsReq = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 1, + approvedTargets, + 0, + 0, + type(uint128).max, + uint128(block.timestamp + 1000), + type(uint128).max, + uidCache + ); + + vm.prank(accountAdmin); + bytes memory sig = _signSignerPermissionRequest(permissionsReq); + vm.expectRevert("!period"); + SimpleAccount(payable(account)).setPermissionsForSigner(permissionsReq, sig); + } + + function test_revert_timeAfterExpiry() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + address[] memory approvedTargets = new address[](0); + + IAccountPermissions.SignerPermissionRequest memory permissionsReq = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 1, + approvedTargets, + 0, + 0, + type(uint128).max, + 0, + uint128(block.timestamp - 1), + uidCache + ); + + vm.prank(accountAdmin); + bytes memory sig = _signSignerPermissionRequest(permissionsReq); + vm.expectRevert("!period"); + SimpleAccount(payable(account)).setPermissionsForSigner(permissionsReq, sig); + } + + function test_revert_SignerNotAdmin() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + address[] memory approvedTargets = new address[](0); + + IAccountPermissions.SignerPermissionRequest memory permissionsReq = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 0, + approvedTargets, + 0, + 0, + type(uint128).max, + 0, + type(uint128).max, + uidCache + ); + + vm.prank(accountAdmin); + bytes memory sig = _signSignerPermissionRequestInvalid(permissionsReq); + vm.expectRevert(bytes("!sig")); + SimpleAccount(payable(account)).setPermissionsForSigner(permissionsReq, sig); + } + + function test_revert_SignerAlreadyAdmin() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + address[] memory approvedTargets = new address[](0); + + { + //set admin status + IAccountPermissions.SignerPermissionRequest memory req = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 1, + approvedTargets, + 0, + 0, + type(uint128).max, + 0, + type(uint128).max, + uidCache + ); + + vm.prank(accountAdmin); + bytes memory sig2 = _signSignerPermissionRequest(req); + SimpleAccount(payable(account)).setPermissionsForSigner(req, sig2); + } + + //test set signerPerms as admin + + uidCache = bytes32("new uid"); + + IAccountPermissions.SignerPermissionRequest memory permissionsReq = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 0, + approvedTargets, + 0, + 0, + type(uint128).max, + 0, + type(uint128).max, + uidCache + ); + + vm.prank(accountAdmin); + bytes memory sig3 = _signSignerPermissionRequest(permissionsReq); + vm.expectRevert("admin"); + SimpleAccount(payable(account)).setPermissionsForSigner(permissionsReq, sig3); + } + + function test_state_setPermissionsForSigner() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + address[] memory approvedTargets = new address[](1); + approvedTargets[0] = address(numberContract); + + IAccountPermissions.SignerPermissionRequest memory permissionsReq = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 0, + approvedTargets, + 1, + 0, + type(uint128).max, + 0, + type(uint128).max, + uidCache + ); + + vm.prank(accountAdmin); + bytes memory sig = _signSignerPermissionRequest(permissionsReq); + SimpleAccount(payable(account)).setPermissionsForSigner(permissionsReq, sig); + + IAccountPermissions.SignerPermissions[] memory allSigners = SimpleAccount(payable(account)).getAllSigners(); + assertEq(allSigners[0].signer, accountSigner); + assertEq(allSigners[0].approvedTargets[0], address(numberContract)); + assertEq(allSigners[0].nativeTokenLimitPerTransaction, 1); + assertEq(allSigners[0].startTimestamp, 0); + assertEq(allSigners[0].endTimestamp, type(uint128).max); + } + + function test_event_addSigner() public { + _setup_executeTransaction(); + + address account = accountFactory.getAddress(accountAdmin, bytes("")); + address[] memory approvedTargets = new address[](1); + approvedTargets[0] = address(numberContract); + + IAccountPermissions.SignerPermissionRequest memory permissionsReq = IAccountPermissions.SignerPermissionRequest( + accountSigner, + 0, + approvedTargets, + 1, + 0, + type(uint128).max, + 0, + type(uint128).max, + uidCache + ); + + vm.prank(accountAdmin); + bytes memory sig = _signSignerPermissionRequest(permissionsReq); + + vm.expectEmit(true, true, false, true); + emit SignerPermissionsUpdated(accountAdmin, accountSigner, permissionsReq); + SimpleAccount(payable(account)).setPermissionsForSigner(permissionsReq, sig); + } +} diff --git a/src/test/smart-wallet/account-permissions/setPermissionsForSigner.tree b/src/test/smart-wallet/account-permissions/setPermissionsForSigner.tree new file mode 100644 index 000000000..fbe90351d --- /dev/null +++ b/src/test/smart-wallet/account-permissions/setPermissionsForSigner.tree @@ -0,0 +1,33 @@ +function setPermissionsForSigner(SignerPermissionRequest calldata _req, bytes calldata _signature) +├── when reqValidityStartTimestamp is greater than block.timestamp +│ └── it should revert ✅ +├── when reqValidityEndTimestamp is less than block.timestamp +│ └── it should revert ✅ +├── when uid is executed +│ └── it should revert ✅ +├── when the recovered signer is not an admin +│ └── it should revert ✅ +└── when the reqValidityStartTimestamp is less than block.timestamp + └── when reqValidityEndTimestamp is greater than block.timestamp + └── when recovered signer is an admin ✅ + └── when req.uid has not been marked as executed + └── when _req.isAdmin is greater than zero + ├── it should mark req.uid as executed ✅ + ├── when _req.isAdmin is one + │ ├── it should set isAdmin[(targetAdmin)] as true ✅ + │ ├── it should add targetAdmin to allAdmins ✅ + │ └── it should emit AdminUpdated with the parameters targetAdmin, true ✅ + ├── when _req.isAdmin is greater than one + │ ├── it should set isAdmin[(targetAdmin)] as false ✅ + │ ├── it should remove targetAdmin from allAdmins ✅ + │ └── it should emit the event AdminUpdated with the parameters targetAdmin, false ✅ + └── when _req.isAdmin is equal to zero + ├── when targetSigner is an admin + │ └── it should revert ✅ + └── when targetSigner is not an admin + ├── it should mark req.uid as executed ✅ + ├── it should add targetSigner to allSigners ✅ + ├── it should set signerPermissions[(targetSigner)] as a SignerPermissionsStatic(nativeTokenLimitPerTransaction, permissionStartTimestamp, permissionEndTimestamp) ✅ + ├── it should remove current approved targets for targetSigner ✅ + ├── it should add the new approved targets for targetSigner ✅ + └── it should emit the event SignerPermissionsUpdated with the parameters signer, targetSigner, SignerPermissionRequest ✅ \ No newline at end of file diff --git a/src/test/smart-wallet/token-paymaster/TokenPaymaster.t.sol b/src/test/smart-wallet/token-paymaster/TokenPaymaster.t.sol new file mode 100644 index 000000000..e6b3507ef --- /dev/null +++ b/src/test/smart-wallet/token-paymaster/TokenPaymaster.t.sol @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test utils +import "../../utils/BaseTest.sol"; +import { MockERC20CustomDecimals } from "../../mocks/MockERC20CustomDecimals.sol"; +import { TestUniswap } from "../../mocks/TestUniswap.sol"; +import { TestOracle2 } from "../../mocks/TestOracle2.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +// Account Abstraction setup for smart wallets. +import { EntryPoint, IEntryPoint } from "contracts/prebuilts/account/utils/EntryPoint.sol"; +import { PackedUserOperation } from "contracts/prebuilts/account/interfaces/PackedUserOperation.sol"; + +// Target +import { IAccountPermissions } from "contracts/extension/interface/IAccountPermissions.sol"; +import { AccountFactory } from "contracts/prebuilts/account/non-upgradeable/AccountFactory.sol"; +import { Account as SimpleAccount } from "contracts/prebuilts/account/non-upgradeable/Account.sol"; +import { TokenPaymaster, IERC20Metadata } from "contracts/prebuilts/account/token-paymaster/TokenPaymaster.sol"; +import { OracleHelper, IOracle } from "contracts/prebuilts/account/utils/OracleHelper.sol"; +import { UniswapHelper, IV3SwapRouter } from "contracts/prebuilts/account/utils/UniswapHelper.sol"; + +/// @dev This is a dummy contract to test contract interactions with Account. +contract Number { + uint256 public num; + + function setNum(uint256 _num) public { + num = _num; + } + + function doubleNum() public { + num *= 2; + } + + function incrementNum() public { + num += 1; + } +} + +contract TokenPaymasterTest is BaseTest { + EntryPoint private entrypoint; + AccountFactory private accountFactory; + SimpleAccount private account; + MockERC20CustomDecimals private token; + TestUniswap private testUniswap; + TestOracle2 private nativeAssetOracle; + TestOracle2 private tokenOracle; + TokenPaymaster private paymaster; + + Number private numberContract; + + int256 initialPriceToken = 100000000; // USD per TOK + int256 initialPriceEther = 500000000; // USD per ETH + + uint256 priceDenominator = 10 ** 26; + uint128 minEntryPointBalance = 1e17; + + address payable private beneficiary = payable(address(0x45654)); + + uint256 private accountAdminPKey = 100; + address private accountAdmin; + + uint256 private accountSignerPKey = 200; + address private accountSigner; + + uint256 private nonSignerPKey = 300; + address private nonSigner; + + uint256 private paymasterOwnerPKey = 400; + address private paymasterOwner; + address private paymasterAddress; + + function setUp() public override { + super.setUp(); + + // Setup signers. + accountAdmin = vm.addr(accountAdminPKey); + vm.deal(accountAdmin, 100 ether); + + accountSigner = vm.addr(accountSignerPKey); + nonSigner = vm.addr(nonSignerPKey); + paymasterOwner = vm.addr(paymasterOwnerPKey); + + // Setup contracts + entrypoint = new EntryPoint(); + testUniswap = new TestUniswap(weth); + accountFactory = new AccountFactory(deployer, IEntryPoint(payable(address(entrypoint)))); + account = SimpleAccount(payable(accountFactory.createAccount(accountAdmin, bytes("")))); + token = new MockERC20CustomDecimals(6); + nativeAssetOracle = new TestOracle2(initialPriceEther, 8); + tokenOracle = new TestOracle2(initialPriceToken, 8); + numberContract = new Number(); + + weth.deposit{ value: 1 ether }(); + weth.transfer(address(testUniswap), 1 ether); + + TokenPaymaster.TokenPaymasterConfig memory tokenPaymasterConfig = TokenPaymaster.TokenPaymasterConfig({ + priceMarkup: (priceDenominator * 15) / 10, // +50% + minEntryPointBalance: minEntryPointBalance, + refundPostopCost: 40000, + priceMaxAge: 86400 + }); + + OracleHelper.OracleHelperConfig memory oracleHelperConfig = OracleHelper.OracleHelperConfig({ + cacheTimeToLive: 0, + maxOracleRoundAge: 0, + nativeOracle: IOracle(address(nativeAssetOracle)), + nativeOracleReverse: false, + priceUpdateThreshold: (priceDenominator * 12) / 100, // 20% + tokenOracle: IOracle(address(tokenOracle)), + tokenOracleReverse: false, + tokenToNativeOracle: false + }); + + UniswapHelper.UniswapHelperConfig memory uniswapHelperConfig = UniswapHelper.UniswapHelperConfig({ + minSwapAmount: 1, + slippage: 5, + uniswapPoolFee: 3, + wethIsNativeAsset: false + }); + + paymaster = new TokenPaymaster( + IERC20Metadata(address(token)), + entrypoint, + weth, + IV3SwapRouter(address(testUniswap)), + tokenPaymasterConfig, + oracleHelperConfig, + uniswapHelperConfig, + paymasterOwner + ); + paymasterAddress = address(paymaster); + + token.mint(paymasterOwner, 10_000 ether); + vm.deal(paymasterOwner, 10_000 ether); + + vm.startPrank(paymasterOwner); + token.transfer(address(paymaster), 100); + paymaster.updateCachedPrice(true); + entrypoint.depositTo{ value: 1000 ether }(address(paymaster)); + paymaster.addStake{ value: 2 ether }(1); + vm.stopPrank(); + } + + // test utils + function _packPaymasterStaticFields( + address paymaster, + uint128 validationGasLimit, + uint128 postOpGasLimit + ) internal pure returns (bytes memory) { + return abi.encodePacked(bytes20(paymaster), bytes16(validationGasLimit), bytes16(postOpGasLimit)); + } + + function _setupUserOpWithSenderAndPaymaster( + bytes memory _initCode, + bytes memory _callDataForEntrypoint, + address _sender, + address _paymaster, + uint128 _paymasterVerificationGasLimit, + uint128 _paymasterPostOpGasLimit + ) internal returns (PackedUserOperation[] memory ops) { + uint256 nonce = entrypoint.getNonce(_sender, 0); + PackedUserOperation memory op; + + { + uint128 verificationGasLimit = 500_000; + uint128 callGasLimit = 500_000; + bytes32 packedAccountGasLimits = (bytes32(uint256(verificationGasLimit)) << 128) | + bytes32(uint256(callGasLimit)); + bytes32 packedGasLimits = (bytes32(uint256(1e9)) << 128) | bytes32(uint256(1e9)); + + // Get user op fields + op = PackedUserOperation({ + sender: _sender, + nonce: nonce, + initCode: _initCode, + callData: _callDataForEntrypoint, + accountGasLimits: packedAccountGasLimits, + preVerificationGas: 500_000, + gasFees: packedGasLimits, + paymasterAndData: _packPaymasterStaticFields( + _paymaster, + _paymasterVerificationGasLimit, + _paymasterPostOpGasLimit + ), + signature: bytes("") + }); + } + + // Sign UserOp + bytes32 opHash = EntryPoint(entrypoint).getUserOpHash(op); + bytes32 msgHash = ECDSA.toEthSignedMessageHash(opHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(accountAdminPKey, msgHash); + bytes memory userOpSignature = abi.encodePacked(r, s, v); + + address recoveredSigner = ECDSA.recover(msgHash, v, r, s); + address expectedSigner = vm.addr(accountAdminPKey); + assertEq(recoveredSigner, expectedSigner); + + op.signature = userOpSignature; + + // Store UserOp + ops = new PackedUserOperation[](1); + ops[0] = op; + } + + // Should be able to sponsor the UserOp while charging correct amount of ERC-20 tokens + function test_validatePaymasterUserOp_correctERC20() public { + token.mint(address(account), 1 ether); + vm.prank(address(account)); + token.approve(address(paymaster), type(uint256).max); + + PackedUserOperation[] memory ops = _setupUserOpWithSenderAndPaymaster( + bytes(""), + abi.encodeWithSignature( + "execute(address,uint256,bytes)", + address(numberContract), + 0, + abi.encodeWithSignature("setNum(uint256)", 42) + ), + address(account), + address(paymaster), + 3e5, + 3e5 + ); + + entrypoint.handleOps(ops, beneficiary); + } +} diff --git a/src/test/smart-wallet/utils/AABenchmarkArtifacts.sol b/src/test/smart-wallet/utils/AABenchmarkArtifacts.sol new file mode 100644 index 000000000..b71c16bcc --- /dev/null +++ b/src/test/smart-wallet/utils/AABenchmarkArtifacts.sol @@ -0,0 +1,14 @@ + +pragma solidity ^0.8.0; +interface ThirdwebAccountFactory { + function createAccount(address _admin, bytes calldata _data) external returns (address); + function getAddress(address _adminSigner, bytes calldata _data) external view returns (address); +} +interface ThirdwebAccount { + function execute(address _target, uint256 _value, bytes calldata _calldata) external; +} +address constant THIRDWEB_ACCOUNT_FACTORY_ADDRESS = 0x2e234DAe75C793f67A35089C9d99245E1C58470b; +address constant THIRDWEB_ACCOUNT_IMPL_ADDRESS = 0xffD4505B3452Dc22f8473616d50503bA9E1710Ac; +bytes constant THIRDWEB_ACCOUNT_FACTORY_BYTECODE = hex"608060405234801561001057600080fd5b50600436106101285760003560e01c806308e93d0a1461012d5780630b61e12b1461014b5780630e6254fd1461016057806311464fbe14610173578063248a9ca3146101b25780632f2ff15d146101d357806336568abe146101e657806358451f97146101f957806383a03f8c146102015780638878ed33146102145780639010d07c1461022757806391d148541461023a5780639387a3801461025d578063938e3d7b14610270578063a217fddf14610283578063a32fa5b31461028b578063a65d69d41461029e578063ac9650d8146102c5578063c3c5a547146102e5578063ca15c873146102f8578063d547741f1461030b578063d8fd8f441461031e578063e68a7c3b14610331578063e8a3d48514610344575b600080fd5b610135610359565b6040516101429190611945565b60405180910390f35b61015e6101593660046119ae565b61036a565b005b61013561016e3660046119d8565b61040b565b61019a7f000000000000000000000000ffd4505b3452dc22f8473616d50503ba9e1710ac81565b6040516001600160a01b039091168152602001610142565b6101c56101c03660046119f3565b610435565b604051908152602001610142565b61015e6101e1366004611a0c565b610453565b61015e6101f4366004611a0c565b6104fd565b6101c561055c565b61015e61020f3660046119f3565b610568565b61019a610222366004611a38565b6105b6565b61019a610235366004611aba565b610630565b61024d610248366004611a0c565b61073e565b6040519015158152602001610142565b61015e61026b3660046119ae565b610772565b61015e61027e366004611af2565b610809565b6101c5600081565b61024d610299366004611a0c565b61085a565b61019a7f0000000000000000000000000000000071727de22e5e9d8baf0edac6f37da03281565b6102d86102d3366004611ba2565b6108bd565b6040516101429190611c66565b61024d6102f33660046119d8565b610a19565b6101c56103063660046119f3565b610a25565b61015e610319366004611a0c565b610ac2565b61019a61032c366004611a38565b610acd565b61013561033f366004611aba565b610c18565b61034c610d49565b6040516101429190611cca565b60606103656000610de1565b905090565b336103758183610dee565b61039a5760405162461bcd60e51b815260040161039190611cdd565b60405180910390fd5b6001600160a01b03831660009081526002602052604081206103bc9083610e32565b9050801561040557836001600160a01b0316826001600160a01b03167f12146497b3b826918ec47f0cac7272a09ed06b30c16c030e99ec48ff5dd60b4760405160405180910390a35b50505050565b6001600160a01b038116600090815260026020526040902060609061042f90610de1565b92915050565b600061043f610e47565b600092835260010160205250604090205490565b61047761045e610e47565b6000848152600191909101602052604090205433610e6b565b61047f610e47565b6000838152602091825260408082206001600160a01b0385168352909252205460ff16156104ef5760405162461bcd60e51b815260206004820152601d60248201527f43616e206f6e6c79206772616e7420746f206e6f6e20686f6c646572730000006044820152606401610391565b6104f98282610ef0565b5050565b336001600160a01b038216146105525760405162461bcd60e51b815260206004820152601a60248201527921b0b71037b7363c903932b737bab731b2903337b91039b2b63360311b6044820152606401610391565b6104f98282610f04565b60006103656000610f18565b336105738183610dee565b61058f5760405162461bcd60e51b815260040161039190611cdd565b61059a600082610e32565b6104f95760405162461bcd60e51b815260040161039190611d14565b6000806105f98585858080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250610f2292505050565b90506106257f000000000000000000000000ffd4505b3452dc22f8473616d50503ba9e1710ac82610f55565b9150505b9392505050565b60008061063b610fb5565b600085815260209190915260408120549150805b82811015610735576000610661610fb5565b60008881526020918252604080822085835260010190925220546001600160a01b0316146106d9578482036106c757610698610fb5565b600087815260209182526040808220938252600190930190915220546001600160a01b0316925061042f915050565b6106d2600183611d74565b9150610723565b6106e486600061073e565b801561071057506106f3610fb5565b600087815260209182526040808220828052600201909252205481145b1561072357610720600183611d74565b91505b61072e600182611d74565b905061064f565b50505092915050565b6000610748610e47565b6000938452602090815260408085206001600160a01b039490941685529290525090205460ff1690565b3361077d8183610dee565b6107995760405162461bcd60e51b815260040161039190611cdd565b6001600160a01b03831660009081526002602052604081206107bb9083610fbf565b9050801561040557836001600160a01b0316826001600160a01b03167f98d1ebbe00ae92a5de96a0f49742a8afa89f42363592bc2e7cfaaed68b45e7a660405160405180910390a350505050565b610811610fd4565b61084e5760405162461bcd60e51b815260206004820152600e60248201526d139bdd08185d5d1a1bdc9a5e995960921b6044820152606401610391565b61085781610fe0565b50565b6000610864610e47565b600084815260209182526040808220828052909252205460ff166108b45761088a610e47565b6000848152602091825260408082206001600160a01b0386168352909252205460ff16905061042f565b50600192915050565b6060816001600160401b038111156108d7576108d7611adc565b60405190808252806020026020018201604052801561090a57816020015b60608152602001906001900390816108f55790505b509050336000805b848110156107355781156109915761096f3087878481811061093657610936611d87565b90506020028101906109489190611d9d565b8660405160200161095b93929190611dea565b6040516020818303038152906040526110c7565b84828151811061098157610981611d87565b6020026020010181905250610a11565b6109f3308787848181106109a7576109a7611d87565b90506020028101906109b99190611d9d565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506110c792505050565b848281518110610a0557610a05611d87565b60200260200101819052505b600101610912565b600061042f81836110ec565b600080610a30610fb5565b6000848152602091909152604081205491505b81811015610a9d576000610a55610fb5565b60008681526020918252604080822085835260010190925220546001600160a01b031614610a8b57610a88600184611d74565b92505b610a96600182611d74565b9050610a43565b50610aa983600061073e565b15610abc57610ab9600183611d74565b91505b50919050565b61055261045e610e47565b6000807f000000000000000000000000ffd4505b3452dc22f8473616d50503ba9e1710ac90506000610b358686868080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250610f2292505050565b90506000610b438383610f55565b90506001600160a01b0381163b15610b5f579250610629915050565b610b69838361110e565b9050336001600160a01b037f0000000000000000000000000000000071727de22e5e9d8baf0edac6f37da0321614610bc257610ba6600082610e32565b610bc25760405162461bcd60e51b815260040161039190611d14565b610bce818888886111a5565b866001600160a01b0316816001600160a01b03167fac631f3001b55ea1509cf3d7e74898f85392a61a76e8149181ae1259622dabc860405160405180910390a39695505050505050565b60608183108015610c325750610c2e6000610f18565b8211155b610c8a5760405162461bcd60e51b815260206004820152602360248201527f426173654163636f756e74466163746f72793a20696e76616c696420696e646960448201526263657360e81b6064820152608401610391565b6000610c968484611e0b565b9050610ca28484611e0b565b6001600160401b03811115610cb957610cb9611adc565b604051908082528060200260200182016040528015610ce2578160200160208202803683370190505b50915060005b81811015610d4157610d05610cfd8683611d74565b60009061120d565b838281518110610d1757610d17611d87565b6001600160a01b0390921660209283029190910190910152610d3a600182611d74565b9050610ce8565b505092915050565b6060610d53611219565b8054610d5e90611e1e565b80601f0160208091040260200160405190810160405280929190818152602001828054610d8a90611e1e565b8015610dd75780601f10610dac57610100808354040283529160200191610dd7565b820191906000526020600020905b815481529060010190602001808311610dba57829003601f168201915b5050505050905090565b606060006106298361123d565b600080610e1b7f000000000000000000000000ffd4505b3452dc22f8473616d50503ba9e1710ac84610f55565b6001600160a01b0385811691161491505092915050565b6000610629836001600160a01b038416611299565b7f0a7b0f5c59907924802379ebe98cdc23e2ee7820f63d30126e10b3752010e50090565b610e73610e47565b6000838152602091825260408082206001600160a01b0385168352909252205460ff166104f957610eae816001600160a01b031660146112e8565b610eb98360206112e8565b604051602001610eca929190611e52565b60408051601f198184030181529082905262461bcd60e51b825261039191600401611cca565b610efa8282611483565b6104f982826114ec565b610f0e82826115ab565b6104f98282611614565b600061042f825490565b60008282604051602001610f37929190611ebf565b60405160208183030381529060405280519060200120905092915050565b6040513060388201526f5af43d82803e903d91602b57fd5bf3ff602482015260148101839052733d602d80600a3d3981f3363d3d373d3d3d363d738152605881018290526037600c82012060788201526055604390910120600090610629565b60006103656116a3565b6000610629836001600160a01b038416611705565b6000610365813361073e565b6000610fea611219565b8054610ff590611e1e565b80601f016020809104026020016040519081016040528092919081815260200182805461102190611e1e565b801561106e5780601f106110435761010080835404028352916020019161106e565b820191906000526020600020905b81548152906001019060200180831161105157829003601f168201915b505050505090508161107e611219565b906110899082611f34565b507fc9c7c3fe08b88b4df9d4d47ef47d2c43d55c025a0ba88ca442580ed9e7348a1681836040516110bb929190611ff3565b60405180910390a15050565b606061062983836040518060600160405280602781526020016120b9602791396117f8565b6001600160a01b03811660009081526001830160205260408120541515610629565b6000763d602d80600a3d3981f3363d3d373d3d3d363d730000008360601b60e81c176000526e5af43d82803e903d91602b57fd5bf38360781b1760205281603760096000f590506001600160a01b03811661042f5760405162461bcd60e51b8152602060048201526017602482015276115490cc4c4d8dce8818dc99585d194c8819985a5b1959604a1b6044820152606401610391565b60405163347d5e2560e21b81526001600160a01b0385169063d1f57894906111d590869086908690600401612018565b600060405180830381600087803b1580156111ef57600080fd5b505af1158015611203573d6000803e3d6000fd5b5050505050505050565b60006106298383611870565b7f4bc804ba64359c0e35e5ed5d90ee596ecaa49a3a930ddcb1470ea0dd625da90090565b60608160000180548060200260200160405190810160405280929190818152602001828054801561128d57602002820191906000526020600020905b815481526020019060010190808311611279575b50505050509050919050565b60008181526001830160205260408120546112e05750815460018181018455600084815260208082209093018490558454848252828601909352604090209190915561042f565b50600061042f565b606060006112f7836002612058565b611302906002611d74565b6001600160401b0381111561131957611319611adc565b6040519080825280601f01601f191660200182016040528015611343576020820181803683370190505b509050600360fc1b8160008151811061135e5761135e611d87565b60200101906001600160f81b031916908160001a905350600f60fb1b8160018151811061138d5761138d611d87565b60200101906001600160f81b031916908160001a90535060006113b1846002612058565b6113bc906001611d74565b90505b6001811115611434576f181899199a1a9b1b9c1cb0b131b232b360811b85600f16601081106113f0576113f0611d87565b1a60f81b82828151811061140657611406611d87565b60200101906001600160f81b031916908160001a90535060049490941c9361142d8161206f565b90506113bf565b5083156106295760405162461bcd60e51b815260206004820181905260248201527f537472696e67733a20686578206c656e67746820696e73756666696369656e746044820152606401610391565b600161148d610e47565b6000848152602091825260408082206001600160a01b0386168084529352808220805460ff1916941515949094179093559151339285917f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d9190a45050565b60006114f6610fb5565b6000848152602091909152604090205490506001611512610fb5565b6000858152602091909152604081208054909190611531908490611d74565b90915550829050611540610fb5565b6000858152602091825260408082208583526001019092522080546001600160a01b0319166001600160a01b039290921691909117905580611580610fb5565b6000948552602090815260408086206001600160a01b03909516865260029094019052919092205550565b6115b58282610e6b565b6115bd610e47565b6000838152602091825260408082206001600160a01b0385168084529352808220805460ff191690555133929185917ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b9190a45050565b600061161e610fb5565b6000848152602091825260408082206001600160a01b03861683526002019092522054905061164b610fb5565b6000848152602091825260408082208483526001019092522080546001600160a01b031916905561167a610fb5565b6000938452602090815260408085206001600160a01b0390941685526002909301905250812055565b60008060ff196116d460017f0c4ba382c0009cf238e4c1ca1a52f51c61e6248a70bdfb34e5ed49d5578a5c0c611e0b565b6040516020016116e691815260200190565b60408051601f1981840301815291905280516020909101201692915050565b600081815260018301602052604081205480156117ee576000611729600183611e0b565b855490915060009061173d90600190611e0b565b90508181146117a257600086600001828154811061175d5761175d611d87565b906000526020600020015490508087600001848154811061178057611780611d87565b6000918252602080832090910192909255918252600188019052604090208390555b85548690806117b3576117b3612086565b60019003818190600052602060002001600090559055856001016000868152602001908152602001600020600090556001935050505061042f565b600091505061042f565b6060600080856001600160a01b031685604051611815919061209c565b600060405180830381855af49150503d8060008114611850576040519150601f19603f3d011682016040523d82523d6000602084013e611855565b606091505b50915091506118668683838761189a565b9695505050505050565b600082600001828154811061188757611887611d87565b9060005260206000200154905092915050565b60608315611909578251600003611902576001600160a01b0385163b6119025760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606401610391565b5081611913565b611913838361191b565b949350505050565b81511561192b5781518083602001fd5b8060405162461bcd60e51b81526004016103919190611cca565b6020808252825182820181905260009190848201906040850190845b818110156119865783516001600160a01b031683529284019291840191600101611961565b50909695505050505050565b80356001600160a01b03811681146119a957600080fd5b919050565b600080604083850312156119c157600080fd5b6119ca83611992565b946020939093013593505050565b6000602082840312156119ea57600080fd5b61062982611992565b600060208284031215611a0557600080fd5b5035919050565b60008060408385031215611a1f57600080fd5b82359150611a2f60208401611992565b90509250929050565b600080600060408486031215611a4d57600080fd5b611a5684611992565b925060208401356001600160401b0380821115611a7257600080fd5b818601915086601f830112611a8657600080fd5b813581811115611a9557600080fd5b876020828501011115611aa757600080fd5b6020830194508093505050509250925092565b60008060408385031215611acd57600080fd5b50508035926020909101359150565b634e487b7160e01b600052604160045260246000fd5b600060208284031215611b0457600080fd5b81356001600160401b0380821115611b1b57600080fd5b818401915084601f830112611b2f57600080fd5b813581811115611b4157611b41611adc565b604051601f8201601f19908116603f01168101908382118183101715611b6957611b69611adc565b81604052828152876020848701011115611b8257600080fd5b826020860160208301376000928101602001929092525095945050505050565b60008060208385031215611bb557600080fd5b82356001600160401b0380821115611bcc57600080fd5b818501915085601f830112611be057600080fd5b813581811115611bef57600080fd5b8660208260051b8501011115611c0457600080fd5b60209290920196919550909350505050565b60005b83811015611c31578181015183820152602001611c19565b50506000910152565b60008151808452611c52816020860160208601611c16565b601f01601f19169290920160200192915050565b600060208083016020845280855180835260408601915060408160051b87010192506020870160005b82811015611cbd57603f19888603018452611cab858351611c3a565b94509285019290850190600101611c8f565b5092979650505050505050565b6020815260006106296020830184611c3a565b6020808252601f908201527f4163636f756e74466163746f72793a206e6f7420616e206163636f756e742e00604082015260600190565b6020808252602a908201527f4163636f756e74466163746f72793a206163636f756e7420616c7265616479206040820152691c9959da5cdd195c995960b21b606082015260800190565b634e487b7160e01b600052601160045260246000fd5b8082018082111561042f5761042f611d5e565b634e487b7160e01b600052603260045260246000fd5b6000808335601e19843603018112611db457600080fd5b8301803591506001600160401b03821115611dce57600080fd5b602001915036819003821315611de357600080fd5b9250929050565b8284823760609190911b6001600160601b0319169101908152601401919050565b8181038181111561042f5761042f611d5e565b600181811c90821680611e3257607f821691505b602082108103610abc57634e487b7160e01b600052602260045260246000fd5b7402832b936b4b9b9b4b7b7399d1030b1b1b7bab73a1605d1b815260008351611e82816015850160208801611c16565b7001034b99036b4b9b9b4b733903937b6329607d1b6015918401918201528351611eb3816026840160208801611c16565b01602601949350505050565b6001600160a01b038316815260406020820181905260009061191390830184611c3a565b601f821115611f2f576000816000526020600020601f850160051c81016020861015611f0c5750805b601f850160051c820191505b81811015611f2b57828155600101611f18565b5050505b505050565b81516001600160401b03811115611f4d57611f4d611adc565b611f6181611f5b8454611e1e565b84611ee3565b602080601f831160018114611f965760008415611f7e5750858301515b600019600386901b1c1916600185901b178555611f2b565b600085815260208120601f198616915b82811015611fc557888601518255948401946001909101908401611fa6565b5085821015611fe35787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b6040815260006120066040830185611c3a565b82810360208401526106258185611c3a565b6001600160a01b03841681526040602082018190528101829052818360608301376000818301606090810191909152601f909201601f1916010192915050565b808202811582820484141761042f5761042f611d5e565b60008161207e5761207e611d5e565b506000190190565b634e487b7160e01b600052603160045260246000fd5b600082516120ae818460208701611c16565b919091019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a2646970667358221220538cf797b69d0577cfb61215e8aa13191cf496b4c95d27eda1df0ff0fa18189264736f6c63430008170033"; +bytes constant THIRDWEB_ACCOUNT_IMPL_BYTECODE = hex"60806040526004361061014b5760003560e01c806301ffc9a714610157578063150b7a021461018c5780631626ba7e146101c557806319822f7c146101e557806324d7806c14610213578063399b77da1461023357806347e1da2a146102535780634a58db19146102755780634d44560d1461027d5780635892e2361461029d5780637dff5a79146102bd5780638b52d723146102dd578063938e3d7b146102ff578063a9082d841461031f578063ac9650d81461035e578063b0d691fe1461038b578063b61d27f6146103ad578063b76464d5146103cd578063bc197c81146103ed578063bc66cea214610419578063c45a015514610439578063d087d2881461046d578063d1f5789414610482578063d42f2f35146104a2578063e8a3d485146104b7578063e9523c97146104d9578063f15d424e146104fb578063f23a6e611461052857600080fd5b3661015257005b600080fd5b34801561016357600080fd5b50610177610172366004612d97565b610554565b60405190151581526020015b60405180910390f35b34801561019857600080fd5b506101ac6101a7366004612ea3565b61059a565b6040516001600160e01b03199091168152602001610183565b3480156101d157600080fd5b506101ac6101e0366004612f0e565b6105ab565b3480156101f157600080fd5b50610205610200366004612f6d565b6106ca565b604051908152602001610183565b34801561021f57600080fd5b5061017761022e366004612fba565b6106f0565b34801561023f57600080fd5b5061020561024e366004612fd7565b61071f565b34801561025f57600080fd5b5061027361026e366004613034565b6107ea565b005b610273610951565b34801561028957600080fd5b506102736102983660046130cd565b6109b9565b3480156102a957600080fd5b506102736102b836600461313a565b610a2c565b3480156102c957600080fd5b506101776102d8366004612fba565b610de9565b3480156102e957600080fd5b506102f2610ea2565b6040516101839190613244565b34801561030b57600080fd5b5061027361031a3660046132a8565b6110e9565b34801561032b57600080fd5b5061033f61033a36600461313a565b61113a565b6040805192151583526001600160a01b03909116602083015201610183565b34801561036a57600080fd5b5061037e6103793660046132f0565b611191565b6040516101839190613381565b34801561039757600080fd5b506103a06112f6565b60405161018391906133d8565b3480156103b957600080fd5b506102736103c83660046133ec565b61133f565b3480156103d957600080fd5b506102736103e8366004612fba565b6113cf565b3480156103f957600080fd5b506101ac6104083660046134d9565b63bc197c8160e01b95945050505050565b34801561042557600080fd5b50610177610434366004613586565b611401565b34801561044557600080fd5b506103a07f0000000000000000000000002e234dae75c793f67a35089c9d99245e1c58470b81565b34801561047957600080fd5b506102056116c5565b34801561048e57600080fd5b5061027361049d3660046135cb565b611745565b3480156104ae57600080fd5b506102f26118fd565b3480156104c357600080fd5b506104cc611a6e565b6040516101839190613612565b3480156104e557600080fd5b506104ee611b06565b6040516101839190613625565b34801561050757600080fd5b5061051b610516366004612fba565b611b18565b6040516101839190613672565b34801561053457600080fd5b506101ac610543366004613685565b63f23a6e6160e01b95945050505050565b60006001600160e01b03198216630271189760e51b148061058557506001600160e01b03198216630a85bd0160e11b145b80610594575061059482611bf0565b92915050565b630a85bd0160e11b5b949350505050565b6000806105b78461071f565b905060006105c58285611c25565b90506105d0816106f0565b156105e75750630b135d3f60e11b91506105949050565b3360006105f2611c49565b6001600160a01b038416600090815260069190910160205260409020905061061a8183611c6d565b8061064a575061062981611c8f565b600114801561064a5750600061063f8282611c99565b6001600160a01b0316145b6106a75760405162461bcd60e51b8152602060048201526024808201527f4163636f756e743a2063616c6c6572206e6f7420617070726f7665642074617260448201526333b2ba1760e11b60648201526084015b60405180910390fd5b6106b083610de9565b156106c057630b135d3f60e11b94505b5050505092915050565b60006106d4611ca5565b6106de8484611d0e565b90506106e982611e53565b9392505050565b60006106fa611c49565b6001600160a01b03909216600090815260049290920160205250604090205460ff1690565b6000808260405160200161073591815260200190565b60405160208183030381529060405280519060200120905060007f82cac545155fcbf147f2a9013809613677ac7d65498556e6d19ce43bcbf6c2848260405160200161078b929190918252602082015260400190565b6040516020818303038152906040528051906020012090506107ab611ea0565b60405161190160f01b60208201526022810191909152604281018290526062016040516020818303038152906040528051906020012092505050919050565b6107f26112f6565b6001600160a01b0316336001600160a01b031614806108155750610815336106f0565b6108315760405162461bcd60e51b815260040161069e906136ed565b610839611fc7565b848114801561084757508483145b6108935760405162461bcd60e51b815260206004820152601d60248201527f4163636f756e743a2077726f6e67206172726179206c656e677468732e000000604482015260640161069e565b60005b858110156109485761093f8787838181106108b3576108b361372e565b90506020020160208101906108c89190612fba565b8686848181106108da576108da61372e565b905060200201358585858181106108f3576108f361372e565b90506020028101906109059190613744565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506120ad92505050565b50600101610896565b50505050505050565b6109596112f6565b6001600160a01b031663b760faf934306040518363ffffffff1660e01b815260040161098591906133d8565b6000604051808303818588803b15801561099e57600080fd5b505af11580156109b2573d6000803e3d6000fd5b5050505050565b6109c161211e565b6109c96112f6565b6001600160a01b031663205c287883836040518363ffffffff1660e01b81526004016109f692919061378a565b600060405180830381600087803b158015610a1057600080fd5b505af1158015610a24573d6000803e3d6000fd5b505050505050565b6000610a3b6020850185612fba565b905042610a4e60e0860160c087016137ba565b6001600160801b031611158015610a7d5750610a71610100850160e086016137ba565b6001600160801b031642105b610ab35760405162461bcd60e51b8152602060048201526007602482015266085c195c9a5bd960ca1b604482015260640161069e565b600080610ac186868661113a565b9150915081610afb5760405162461bcd60e51b815260040161069e906020808252600490820152632173696760e01b604082015260600190565b6001610b05611c49565b610100880135600090815260079190910160209081526040808320805460ff1916941515949094179093559091610b41919089019089016137e6565b60ff161115610b6e576000610b5c60408801602089016137e6565b60ff166001149050610948848261215c565b610b77836106f0565b15610bac5760405162461bcd60e51b815260206004820152600560248201526430b236b4b760d91b604482015260640161069e565b610bc183610bb8611c49565b60020190612231565b50604051806060016040528087606001358152602001876080016020810190610bea91906137ba565b6001600160801b03168152602001610c0860c0890160a08a016137ba565b6001600160801b03169052610c1b611c49565b6001600160a01b03851660009081526005919091016020908152604080832084518155918401519301516001600160801b03908116600160801b02931692909217600190920191909155610c91610c70611c49565b6001600160a01b038616600090815260069190910160205260409020612246565b805190915060005b81811015610cfb57610ce8838281518110610cb657610cb661372e565b6020026020010151610cc6611c49565b6001600160a01b03891660009081526006919091016020526040902090612253565b50610cf4600182613817565b9050610c99565b50610d09604089018961382a565b9050905060005b81811015610d8a57610d77610d2860408b018b61382a565b83818110610d3857610d3861372e565b9050602002016020810190610d4d9190612fba565b610d55611c49565b6001600160a01b03891660009081526006919091016020526040902090612231565b50610d83600182613817565b9050610d10565b50610d9488612268565b846001600160a01b0316836001600160a01b03167ff21d10c26e35863a8df291aca54181ee8c4a3bc8e00246c3f7a5a14b69d826a78a604051610dd79190613904565b60405180910390a35050505050505050565b600080610df4611c49565b6001600160a01b038416600090815260059190910160209081526040918290208251606081018452815481526001909101546001600160801b03808216938301849052600160801b90910416928101929092529091504210801590610e65575080604001516001600160801b031642105b80156106e957506000610e9a610e79611c49565b6001600160a01b038616600090815260069190910160205260409020611c8f565b119392505050565b60606000610eb9610eb1611c49565b600201612246565b80519091506000805b82811015610f4a57610eec848281518110610edf57610edf61372e565b6020026020010151610de9565b15610f035781610efb816139ef565b925050610f38565b6000848281518110610f1757610f1761372e565b60200260200101906001600160a01b031690816001600160a01b0316815250505b610f43600182613817565b9050610ec2565b50806001600160401b03811115610f6357610f63612de6565b604051908082528060200260200182016040528015610f9c57816020015b610f89612d4d565b815260200190600190039081610f815790505b5093506000805b838110156110e15760006001600160a01b0316858281518110610fc857610fc861372e565b60200260200101516001600160a01b0316146110cf576000858281518110610ff257610ff261372e565b602002602001015190506000611006611c49565b6001600160a01b038316600081815260059290920160209081526040928390208351606081018552815481526001909101546001600160801b0380821683850152600160801b9091041681850152835160a081019094529183529092508101611070610c70611c49565b81526020018260000151815260200182602001516001600160801b0316815260200182604001516001600160801b03168152508885806110af906139ef565b9650815181106110c1576110c161372e565b602002602001018190525050505b6110da600182613817565b9050610fa3565b505050505090565b6110f16122fd565b61112e5760405162461bcd60e51b815260206004820152600e60248201526d139bdd08185d5d1a1bdc9a5e995960921b604482015260640161069e565b61113781612315565b50565b600080611150611149866123fc565b8585612540565b905061115a611c49565b6101008601356000908152600791909101602052604090205460ff161580156111875750611187816106f0565b9150935093915050565b6060816001600160401b038111156111ab576111ab612de6565b6040519080825280602002602001820160405280156111de57816020015b60608152602001906001900390816111c95790505b509050336000805b848110156112ed578115611265576112433087878481811061120a5761120a61372e565b905060200281019061121c9190613744565b8660405160200161122f93929190613a08565b604051602081830303815290604052612592565b8482815181106112555761125561372e565b60200260200101819052506112e5565b6112c73087878481811061127b5761127b61372e565b905060200281019061128d9190613744565b8080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525061259292505050565b8482815181106112d9576112d961372e565b60200260200101819052505b6001016111e6565b50505092915050565b6000806113016125b7565b546001600160a01b03169050801561131857919050565b7f0000000000000000000000000000000071727de22e5e9d8baf0edac6f37da03291505090565b6113476112f6565b6001600160a01b0316336001600160a01b0316148061136a575061136a336106f0565b6113865760405162461bcd60e51b815260040161069e906136ed565b61138e611fc7565b6109b2848484848080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506120ad92505050565b6113d761211e565b806113e06125b7565b80546001600160a01b0319166001600160a01b039290921691909117905550565b600061140b611c49565b6001600160a01b0384166000908152600491909101602052604090205460ff161561143857506001610594565b6000611442611c49565b6001600160a01b0385166000908152600591909101602090815260408083208151606081018352815481526001909101546001600160801b0380821694830194909452600160801b900490921690820152915061149d611c49565b6006016000866001600160a01b03166001600160a01b0316815260200190815260200160002090504282602001516001600160801b031611806114ed575081604001516001600160801b03164210155b806114fe57506114fc81611c8f565b155b1561150e57600092505050610594565b60006115256115206060870187613744565b6125db565b9050600061153283611c8f565b6001148015611553575060006115488482611c99565b6001600160a01b0316145b90506324f16c0560e11b6001600160e01b03198316016115ca5760008061158561158060608a018a613744565b612615565b91509150826115ab576115988583611c6d565b6115ab5760009650505050505050610594565b85518111156115c35760009650505050505050610594565b50506116b8565b635c0f12eb60e11b6001600160e01b03198316016116ab576000806115fa6115f560608a018a613744565b61267a565b50915091508261165a5760005b82518110156116585761163c8382815181106116255761162561372e565b602002602001015187611c6d90919063ffffffff16565b611650576000975050505050505050610594565b600101611607565b505b60005b82518110156116a3578181815181106116785761167861372e565b60200260200101518760000151101561169b576000975050505050505050610594565b60010161165d565b5050506116b8565b6000945050505050610594565b5060019695505050505050565b60006116cf6112f6565b604051631aab3f0d60e11b8152306004820152600060248201526001600160a01b0391909116906335567e1a90604401602060405180830381865afa15801561171c573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906117409190613a29565b905090565b600061174f6126c7565b5460ff169050600061175f6126c7565b54610100900460ff169050801580801561177c575060018360ff16105b8061179b575061178b306126eb565b15801561179b57508260ff166001145b6117fe5760405162461bcd60e51b815260206004820152602e60248201527f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160448201526d191e481a5b9a5d1a585b1a5e995960921b606482015260840161069e565b60016118086126c7565b805460ff191660ff92909216919091179055801561184157600161182a6126c7565b80549115156101000261ff00199092169190911790555b6118818686868080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506126fa92505050565b6118896125b7565b6001018190555061189b86600161215c565b8015610a245760006118ab6126c7565b80549115156101000261ff0019909216919091179055604051600181527f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb38474024989060200160405180910390a1505050505050565b6060600061190c610eb1611c49565b8051909150806001600160401b0381111561192957611929612de6565b60405190808252806020026020018201604052801561196257816020015b61194f612d4d565b8152602001906001900390816119475790505b50925060005b81811015611a685760008382815181106119845761198461372e565b602002602001015190506000611998611c49565b6001600160a01b038316600081815260059290920160209081526040928390208351606081018552815481526001909101546001600160801b0380821683850152600160801b9091041681850152835160a081019094529183529092508101611a02610c70611c49565b81526020018260000151815260200182602001516001600160801b0316815260200182604001516001600160801b0316815250868481518110611a4757611a4761372e565b60200260200101819052505050600181611a619190613817565b9050611968565b50505090565b6060611a7861272d565b8054611a8390613a42565b80601f0160208091040260200160405190810160405280929190818152602001828054611aaf90613a42565b8015611afc5780601f10611ad157610100808354040283529160200191611afc565b820191906000526020600020905b815481529060010190602001808311611adf57829003601f168201915b5050505050905090565b6060611740611b13611c49565b612246565b611b20612d4d565b6000611b2a611c49565b6001600160a01b038416600081815260059290920160209081526040928390208351606081018552815481526001909101546001600160801b0380821683850152600160801b9091041681850152835160a081019094529183529092508101611bb5611b94611c49565b6001600160a01b038716600090815260069190910160205260409020612246565b81526020018260000151815260200182602001516001600160801b0316815260200182604001516001600160801b0316815250915050919050565b60006001600160e01b03198216630271189760e51b148061059457506301ffc9a760e01b6001600160e01b0319831614610594565b6000806000611c348585612751565b91509150611c4181612796565b509392505050565b7f3181e78fc1b109bc611fd2406150bf06e33faa75f71cba12c3e1fd670f2def0090565b6001600160a01b038116600090815260018301602052604081205415156106e9565b6000610594825490565b60006106e983836128db565b611cad6112f6565b6001600160a01b0316336001600160a01b031614611d0c5760405162461bcd60e51b815260206004820152601c60248201527b1858d8dbdd5b9d0e881b9bdd08199c9bdb48115b9d1c9e541bda5b9d60221b604482015260640161069e565b565b7b0ca2ba3432b932bab69029b4b3b732b21026b2b9b9b0b3b29d05199960211b6000908152601c829052603c81206000611d8c611d4f610100870187613744565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508693925050611c259050565b9050611d988186611401565b611da757600192505050610594565b6000611db1611c49565b6001600160a01b03929092166000908152600590920160209081526040808420815160608082018452825482526001909201546001600160801b0380821683870152600160801b8204908116928501929092528351928301845295825265ffffffffffff8087169483019490945292831691015260d09290921b6001600160d01b03191660a09290921b65ffffffffffff60a01b169190911795945050505050565b801561113757604051600090339060001990849084818181858888f193505050503d80600081146109b2576040519150601f19603f3d011682016040523d82523d6000602084013e6109b2565b6000306001600160a01b037f000000000000000000000000ffd4505b3452dc22f8473616d50503ba9e1710ac16148015611ef957507f0000000000000000000000000000000000000000000000000000000000007a6946145b15611f2357507fbcdadf6444930a967ffda04923d78c49b3dd65df3ed39abb04a1e3eb1190553790565b50604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f6020808301919091527ff0729608244859f656d32ae4cbc6b0367695d68d8e941a28f5e2d33c6d5182dd828401527fc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc660608301524660808301523060a0808401919091528351808403909101815260c0909201909252805191012090565b60405163c3c5a54760e01b81527f0000000000000000000000002e234dae75c793f67a35089c9d99245e1c58470b906001600160a01b0382169063c3c5a547906120159030906004016133d8565b602060405180830381865afa158015612032573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906120569190613a76565b61113757806001600160a01b03166383a03f8c6120716125b7565b600101546040518263ffffffff1660e01b815260040161209391815260200190565b600060405180830381600087803b15801561099e57600080fd5b60606000846001600160a01b031684846040516120ca9190613a98565b60006040518083038185875af1925050503d8060008114612107576040519150601f19603f3d011682016040523d82523d6000602084013e61210c565b606091505b509250905080611c4157815160208301fd5b612127336106f0565b611d0c5760405162461bcd60e51b815260206004820152600660248201526510b0b236b4b760d11b604482015260640161069e565b6121668282612905565b6001600160a01b037f0000000000000000000000002e234dae75c793f67a35089c9d99245e1c58470b163b1561222d5780156121f5577f0000000000000000000000002e234dae75c793f67a35089c9d99245e1c58470b6001600160a01b0316630b61e12b836121d46125b7565b600101546040518363ffffffff1660e01b81526004016109f692919061378a565b7f0000000000000000000000002e234dae75c793f67a35089c9d99245e1c58470b6001600160a01b0316639387a380836121d46125b7565b5050565b60006106e9836001600160a01b0384166129b4565b606060006106e983612a03565b60006106e9836001600160a01b038416612a5f565b6001600160a01b037f0000000000000000000000002e234dae75c793f67a35089c9d99245e1c58470b163b15611137576001600160a01b037f0000000000000000000000002e234dae75c793f67a35089c9d99245e1c58470b16630b61e12b6122d46020840184612fba565b6122dc6125b7565b600101546040518363ffffffff1660e01b815260040161209392919061378a565b6000612308336106f0565b8061174057505030331490565b600061231f61272d565b805461232a90613a42565b80601f016020809104026020016040519081016040528092919081815260200182805461235690613a42565b80156123a35780601f10612378576101008083540402835291602001916123a3565b820191906000526020600020905b81548152906001019060200180831161238657829003601f168201915b50505050509050816123b361272d565b906123be9082613b01565b507fc9c7c3fe08b88b4df9d4d47ef47d2c43d55c025a0ba88ca442580ed9e7348a1681836040516123f0929190613bc0565b60405180910390a15050565b60607f3fd4a1a1a267c84185e3b7eecd57c68783c0581d538b9d6e5f23e4670497c1e961242c6020840184612fba565b61243c60408501602086016137e6565b612449604086018661382a565b60405160200161245a929190613bee565b60408051601f198184030181529190528051602090910120606086013561248760a08801608089016137ba565b61249760c0890160a08a016137ba565b6124a760e08a0160c08b016137ba565b6124b86101008b0160e08c016137ba565b60408051602081019a909a526001600160a01b039098169789019790975260ff9095166060880152608087019390935260a08601919091526001600160801b0390811660c086015290811660e0850152908116610100848101919091529116610120830152830135610140820152610160016040516020818303038152906040529050919050565b60006105a383838080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250508751602089012061258c92509050612b52565b90611c25565b60606106e98383604051806060016040528060278152602001613e7160279139612b7f565b7f036f52c1827dab135f7fd44ca0bddde297e2f659c710e0ec53e975f22b54830090565b600060048210156125fe5760405162461bcd60e51b815260040161069e90613c30565b61260c600460008486613c4f565b6106e991613c79565b60008060448310156126395760405162461bcd60e51b815260040161069e90613c30565b612647602460048587613c4f565b8101906126549190612fba565b9150612664604460248587613c4f565b8101906126719190612fd7565b90509250929050565b60608080606484101561269f5760405162461bcd60e51b815260040161069e90613c30565b6126ac8460048188613c4f565b8101906126b99190613d28565b919790965090945092505050565b7f322cf19c484104d3b1a9c2982ebae869ede3fa5f6c4703ca41b9a48c76ee030090565b6001600160a01b03163b151590565b6000828260405160200161270f929190613e0d565b60405160208183030381529060405280519060200120905092915050565b7f4bc804ba64359c0e35e5ed5d90ee596ecaa49a3a930ddcb1470ea0dd625da90090565b60008082516041036127875760208301516040840151606085015160001a61277b87828585612bf7565b9450945050505061278f565b506000905060025b9250929050565b60008160048111156127aa576127aa613e31565b036127b25750565b60018160048111156127c6576127c6613e31565b0361280e5760405162461bcd60e51b815260206004820152601860248201527745434453413a20696e76616c6964207369676e617475726560401b604482015260640161069e565b600281600481111561282257612822613e31565b0361286f5760405162461bcd60e51b815260206004820152601f60248201527f45434453413a20696e76616c6964207369676e6174757265206c656e67746800604482015260640161069e565b600381600481111561288357612883613e31565b036111375760405162461bcd60e51b815260206004820152602260248201527f45434453413a20696e76616c6964207369676e6174757265202773272076616c604482015261756560f01b606482015260840161069e565b60008260000182815481106128f2576128f261372e565b9060005260206000200154905092915050565b8061290e611c49565b6001600160a01b038416600090815260049190910160205260409020805460ff19169115159190911790558015612957576129518261294b611c49565b90612231565b5061296b565b61296982612963611c49565b90612253565b505b816001600160a01b03167f235bc17e7930760029e9f4d860a2a8089976de5b381cf8380fc11c1d88a11133826040516129a8911515815260200190565b60405180910390a25050565b60008181526001830160205260408120546129fb57508154600181810184556000848152602080822090930184905584548482528286019093526040902091909155610594565b506000610594565b606081600001805480602002602001604051908101604052809291908181526020018280548015612a5357602002820191906000526020600020905b815481526020019060010190808311612a3f575b50505050509050919050565b60008181526001830160205260408120548015612b48576000612a83600183613e47565b8554909150600090612a9790600190613e47565b9050818114612afc576000866000018281548110612ab757612ab761372e565b9060005260206000200154905080876000018481548110612ada57612ada61372e565b6000918252602080832090910192909255918252600188019052604090208390555b8554869080612b0d57612b0d613e5a565b600190038181906000526020600020016000905590558560010160008681526020019081526020016000206000905560019350505050610594565b6000915050610594565b6000610594612b5f611ea0565b8360405161190160f01b8152600281019290925260228201526042902090565b6060600080856001600160a01b031685604051612b9c9190613a98565b600060405180830381855af49150503d8060008114612bd7576040519150601f19603f3d011682016040523d82523d6000602084013e612bdc565b606091505b5091509150612bed86838387612cb1565b9695505050505050565b6000806fa2a8918ca85bafe22016d0b997e4df60600160ff1b03831115612c245750600090506003612ca8565b6040805160008082526020820180845289905260ff881692820192909252606081018690526080810185905260019060a0016020604051602081039080840390855afa158015612c78573d6000803e3d6000fd5b5050604051601f1901519150506001600160a01b038116612ca157600060019250925050612ca8565b9150600090505b94509492505050565b60608315612d1e578251600003612d1757612ccb856126eb565b612d175760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015260640161069e565b50816105a3565b6105a38383815115612d335781518083602001fd5b8060405162461bcd60e51b815260040161069e9190613612565b6040518060a0016040528060006001600160a01b03168152602001606081526020016000815260200160006001600160801b0316815260200160006001600160801b031681525090565b600060208284031215612da957600080fd5b81356001600160e01b0319811681146106e957600080fd5b6001600160a01b038116811461113757600080fd5b8035612de181612dc1565b919050565b634e487b7160e01b600052604160045260246000fd5b604051601f8201601f191681016001600160401b0381118282101715612e2457612e24612de6565b604052919050565b60006001600160401b03831115612e4557612e45612de6565b612e58601f8401601f1916602001612dfc565b9050828152838383011115612e6c57600080fd5b828260208301376000602084830101529392505050565b600082601f830112612e9457600080fd5b6106e983833560208501612e2c565b60008060008060808587031215612eb957600080fd5b8435612ec481612dc1565b93506020850135612ed481612dc1565b92506040850135915060608501356001600160401b03811115612ef657600080fd5b612f0287828801612e83565b91505092959194509250565b60008060408385031215612f2157600080fd5b8235915060208301356001600160401b03811115612f3e57600080fd5b612f4a85828601612e83565b9150509250929050565b60006101208284031215612f6757600080fd5b50919050565b600080600060608486031215612f8257600080fd5b83356001600160401b03811115612f9857600080fd5b612fa486828701612f54565b9660208601359650604090950135949350505050565b600060208284031215612fcc57600080fd5b81356106e981612dc1565b600060208284031215612fe957600080fd5b5035919050565b60008083601f84011261300257600080fd5b5081356001600160401b0381111561301957600080fd5b6020830191508360208260051b850101111561278f57600080fd5b6000806000806000806060878903121561304d57600080fd5b86356001600160401b038082111561306457600080fd5b6130708a838b01612ff0565b9098509650602089013591508082111561308957600080fd5b6130958a838b01612ff0565b909650945060408901359150808211156130ae57600080fd5b506130bb89828a01612ff0565b979a9699509497509295939492505050565b600080604083850312156130e057600080fd5b82356130eb81612dc1565b946020939093013593505050565b60008083601f84011261310b57600080fd5b5081356001600160401b0381111561312257600080fd5b60208301915083602082850101111561278f57600080fd5b60008060006040848603121561314f57600080fd5b83356001600160401b038082111561316657600080fd5b61317287838801612f54565b9450602086013591508082111561318857600080fd5b50613195868287016130f9565b9497909650939450505050565b6001600160801b03169052565b80516001600160a01b03908116835260208083015160a082860181905281519086018190526000939183019290849060c08801905b80831015613206578551851682529483019460019290920191908301906131e4565b50604087015160408901526060870151945061322560608901866131a2565b6080870151945061323960808901866131a2565b979650505050505050565b600060208083016020845280855180835260408601915060408160051b87010192506020870160005b8281101561329b57603f198886030184526132898583516131af565b9450928501929085019060010161326d565b5092979650505050505050565b6000602082840312156132ba57600080fd5b81356001600160401b038111156132d057600080fd5b8201601f810184136132e157600080fd5b6105a384823560208401612e2c565b6000806020838503121561330357600080fd5b82356001600160401b0381111561331957600080fd5b61332585828601612ff0565b90969095509350505050565b60005b8381101561334c578181015183820152602001613334565b50506000910152565b6000815180845261336d816020860160208601613331565b601f01601f19169290920160200192915050565b600060208083016020845280855180835260408601915060408160051b87010192506020870160005b8281101561329b57603f198886030184526133c6858351613355565b945092850192908501906001016133aa565b6001600160a01b0391909116815260200190565b6000806000806060858703121561340257600080fd5b843561340d81612dc1565b93506020850135925060408501356001600160401b0381111561342f57600080fd5b61343b878288016130f9565b95989497509550505050565b60006001600160401b0382111561346057613460612de6565b5060051b60200190565b600082601f83011261347b57600080fd5b8135602061349061348b83613447565b612dfc565b8083825260208201915060208460051b8701019350868411156134b257600080fd5b602086015b848110156134ce57803583529183019183016134b7565b509695505050505050565b600080600080600060a086880312156134f157600080fd5b85356134fc81612dc1565b9450602086013561350c81612dc1565b935060408601356001600160401b038082111561352857600080fd5b61353489838a0161346a565b9450606088013591508082111561354a57600080fd5b61355689838a0161346a565b9350608088013591508082111561356c57600080fd5b5061357988828901612e83565b9150509295509295909350565b6000806040838503121561359957600080fd5b82356135a481612dc1565b915060208301356001600160401b038111156135bf57600080fd5b612f4a85828601612f54565b6000806000604084860312156135e057600080fd5b83356135eb81612dc1565b925060208401356001600160401b0381111561360657600080fd5b613195868287016130f9565b6020815260006106e96020830184613355565b6020808252825182820181905260009190848201906040850190845b818110156136665783516001600160a01b031683529284019291840191600101613641565b50909695505050505050565b6020815260006106e960208301846131af565b600080600080600060a0868803121561369d57600080fd5b85356136a881612dc1565b945060208601356136b881612dc1565b9350604086013592506060860135915060808601356001600160401b038111156136e157600080fd5b61357988828901612e83565b60208082526021908201527f4163636f756e743a206e6f742061646d696e206f7220456e747279506f696e746040820152601760f91b606082015260800190565b634e487b7160e01b600052603260045260246000fd5b6000808335601e1984360301811261375b57600080fd5b8301803591506001600160401b0382111561377557600080fd5b60200191503681900382131561278f57600080fd5b6001600160a01b03929092168252602082015260400190565b80356001600160801b0381168114612de157600080fd5b6000602082840312156137cc57600080fd5b6106e9826137a3565b803560ff81168114612de157600080fd5b6000602082840312156137f857600080fd5b6106e9826137d5565b634e487b7160e01b600052601160045260246000fd5b8082018082111561059457610594613801565b6000808335601e1984360301811261384157600080fd5b8301803591506001600160401b0382111561385b57600080fd5b6020019150600581901b360382131561278f57600080fd5b6000808335601e1984360301811261388a57600080fd5b83016020810192503590506001600160401b038111156138a957600080fd5b8060051b360382131561278f57600080fd5b8183526000602080850194508260005b858110156138f95781356138de81612dc1565b6001600160a01b0316875295820195908201906001016138cb565b509495945050505050565b602081526139256020820161391884612dd6565b6001600160a01b03169052565b6000613933602084016137d5565b60ff811660408401525061394a6040840184613873565b610120806060860152613962610140860183856138bb565b92506060860135608086015261397a608087016137a3565b915061398960a08601836131a2565b61399560a087016137a3565b91506139a460c08601836131a2565b6139b060c087016137a3565b91506139bf60e08601836131a2565b6139cb60e087016137a3565b91506101006139dc818701846131a2565b9590950135939094019290925250919050565b600060018201613a0157613a01613801565b5060010190565b8284823760609190911b6001600160601b0319169101908152601401919050565b600060208284031215613a3b57600080fd5b5051919050565b600181811c90821680613a5657607f821691505b602082108103612f6757634e487b7160e01b600052602260045260246000fd5b600060208284031215613a8857600080fd5b815180151581146106e957600080fd5b60008251613aaa818460208701613331565b9190910192915050565b601f821115613afc576000816000526020600020601f850160051c81016020861015613add5750805b601f850160051c820191505b81811015610a2457828155600101613ae9565b505050565b81516001600160401b03811115613b1a57613b1a612de6565b613b2e81613b288454613a42565b84613ab4565b602080601f831160018114613b635760008415613b4b5750858301515b600019600386901b1c1916600185901b178555610a24565b600085815260208120601f198616915b82811015613b9257888601518255948401946001909101908401613b73565b5085821015613bb05787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b604081526000613bd36040830185613355565b8281036020840152613be58185613355565b95945050505050565b60008184825b85811015613c25578135613c0781612dc1565b6001600160a01b031683526020928301929190910190600101613bf4565b509095945050505050565b602080825260059082015264214461746160d81b604082015260600190565b60008085851115613c5f57600080fd5b83861115613c6c57600080fd5b5050820193919092039150565b6001600160e01b03198135818116916004851015613ca15780818660040360031b1b83161692505b505092915050565b600082601f830112613cba57600080fd5b81356020613cca61348b83613447565b82815260059290921b84018101918181019086841115613ce957600080fd5b8286015b848110156134ce5780356001600160401b03811115613d0c5760008081fd5b613d1a8986838b0101612e83565b845250918301918301613ced565b600080600060608486031215613d3d57600080fd5b83356001600160401b0380821115613d5457600080fd5b818601915086601f830112613d6857600080fd5b81356020613d7861348b83613447565b82815260059290921b8401810191818101908a841115613d9757600080fd5b948201945b83861015613dbe578535613daf81612dc1565b82529482019490820190613d9c565b97505087013592505080821115613dd457600080fd5b613de08783880161346a565b93506040860135915080821115613df657600080fd5b50613e0386828701613ca9565b9150509250925092565b6001600160a01b03831681526040602082018190526000906105a390830184613355565b634e487b7160e01b600052602160045260246000fd5b8181038181111561059457610594613801565b634e487b7160e01b600052603160045260246000fdfe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a2646970667358221220cad4afb4c5b67a0f0c89d57ce9aadc583771ebb0832657a8af9d2f4d729ee64f64736f6c63430008170033"; + diff --git a/src/test/smart-wallet/utils/AABenchmarkPrepare.sol b/src/test/smart-wallet/utils/AABenchmarkPrepare.sol new file mode 100644 index 000000000..d7273abf6 --- /dev/null +++ b/src/test/smart-wallet/utils/AABenchmarkPrepare.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test utils +import { BaseTest } from "../../utils/BaseTest.sol"; + +// Account Abstraction setup for smart wallets. +import { IEntryPoint } from "contracts/prebuilts/account/utils/EntryPoint.sol"; +import { Strings } from "contracts/lib/Strings.sol"; +import { AccountFactory } from "contracts/prebuilts/account/non-upgradeable/AccountFactory.sol"; +import "forge-std/Test.sol"; + +contract AABenchmarkPrepare is BaseTest { + AccountFactory private accountFactory; + + function setUp() public override { + super.setUp(); + accountFactory = new AccountFactory( + deployer, + IEntryPoint(payable(address(0x0000000071727De22E5E9d8BAf0edAc6f37da032))) + ); + } + + function test_prepareBenchmarkFile() public { + address accountFactoryAddress = address(accountFactory); + bytes memory accountFactoryBytecode = accountFactoryAddress.code; + + address accountImplAddress = accountFactory.accountImplementation(); + bytes memory accountImplBytecode = accountImplAddress.code; + + string memory accountFactoryAddressString = string.concat( + "address constant THIRDWEB_ACCOUNT_FACTORY_ADDRESS = ", + Strings.toHexStringChecksummed(accountFactoryAddress), + ";" + ); + string memory accountFactoryBytecodeString = string.concat( + 'bytes constant THIRDWEB_ACCOUNT_FACTORY_BYTECODE = hex"', + Strings.toHexStringNoPrefix(accountFactoryBytecode), + '"', + ";" + ); + + string memory accountImplAddressString = string.concat( + "address constant THIRDWEB_ACCOUNT_IMPL_ADDRESS = ", + Strings.toHexStringChecksummed(accountImplAddress), + ";" + ); + string memory accountImplBytecodeString = string.concat( + 'bytes constant THIRDWEB_ACCOUNT_IMPL_BYTECODE = hex"', + Strings.toHexStringNoPrefix(accountImplBytecode), + '"', + ";" + ); + + string memory path = "src/test/smart-wallet/utils/AABenchmarkArtifacts.sol"; + + vm.removeFile(path); + + vm.writeLine(path, ""); + vm.writeLine(path, "pragma solidity ^0.8.0;"); + vm.writeLine(path, "interface ThirdwebAccountFactory {"); + vm.writeLine( + path, + " function createAccount(address _admin, bytes calldata _data) external returns (address);" + ); + vm.writeLine( + path, + " function getAddress(address _adminSigner, bytes calldata _data) external view returns (address);" + ); + vm.writeLine(path, "}"); + + vm.writeLine(path, "interface ThirdwebAccount {"); + vm.writeLine(path, " function execute(address _target, uint256 _value, bytes calldata _calldata) external;"); + vm.writeLine(path, "}"); + vm.writeLine(path, accountFactoryAddressString); + vm.writeLine(path, accountImplAddressString); + vm.writeLine(path, accountFactoryBytecodeString); + vm.writeLine(path, accountImplBytecodeString); + + vm.writeLine(path, ""); + } +} diff --git a/src/test/smart-wallet/utils/AABenchmarkTest.t.sol b/src/test/smart-wallet/utils/AABenchmarkTest.t.sol new file mode 100644 index 000000000..61b5ee6ed --- /dev/null +++ b/src/test/smart-wallet/utils/AABenchmarkTest.t.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "./AATestBase.sol"; +import { ThirdwebAccountFactory, ThirdwebAccount, THIRDWEB_ACCOUNT_FACTORY_ADDRESS, THIRDWEB_ACCOUNT_IMPL_ADDRESS, THIRDWEB_ACCOUNT_FACTORY_BYTECODE, THIRDWEB_ACCOUNT_IMPL_BYTECODE } from "./AABenchmarkArtifacts.sol"; + +contract ProfileThirdwebAccount is AAGasProfileBase { + ThirdwebAccountFactory factory; + + function setUp() external { + initializeTest("thirdwebAccount"); + factory = ThirdwebAccountFactory(THIRDWEB_ACCOUNT_FACTORY_ADDRESS); + vm.etch(address(factory), THIRDWEB_ACCOUNT_FACTORY_BYTECODE); + vm.etch(THIRDWEB_ACCOUNT_IMPL_ADDRESS, THIRDWEB_ACCOUNT_IMPL_BYTECODE); + setAccount(); + } + + function fillData(address _to, uint256 _value, bytes memory _data) internal view override returns (bytes memory) { + return abi.encodeWithSelector(ThirdwebAccount.execute.selector, _to, _value, _data); + } + + function getSignature(PackedUserOperation memory _op) internal view override returns (bytes memory) { + return signUserOpHash(key, _op); + } + + function createAccount(address _owner) internal override { + // if (address(account).code.length == 0) { + factory.createAccount(_owner, ""); + // } + } + + function getAccountAddr(address _owner) internal view override returns (IAccount) { + return IAccount(factory.getAddress(_owner, "")); + } + + function getInitCode(address _owner) internal view override returns (bytes memory) { + return abi.encodePacked(address(factory), abi.encodeWithSelector(factory.createAccount.selector, _owner, "")); + } + + function getDummySig(PackedUserOperation memory _op) internal pure override returns (bytes memory) { + return + hex"fffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c"; + } +} diff --git a/src/test/smart-wallet/utils/AATestArtifacts.sol b/src/test/smart-wallet/utils/AATestArtifacts.sol new file mode 100644 index 000000000..86fcb0aaf --- /dev/null +++ b/src/test/smart-wallet/utils/AATestArtifacts.sol @@ -0,0 +1,9 @@ +pragma solidity ^0.8.0; + +bytes constant ENTRYPOINT_0_7_BYTECODE = hex"60806040526004361015610024575b361561001957600080fd5b61002233612748565b005b60003560e01c806242dc5314611b0057806301ffc9a7146119ae5780630396cb60146116765780630bd28e3b146115fa5780631b2e01b814611566578063205c2878146113d157806322cdde4c1461136b57806335567e1a146112b35780635287ce12146111a557806370a0823114611140578063765e827f14610e82578063850aaf6214610dc35780639b249f6914610c74578063b760faf914610c3a578063bb9fe6bf14610a68578063c23a5cea146107c4578063dbed18e0146101a15763fc7e286d0361000e573461019c5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261019c5773ffffffffffffffffffffffffffffffffffffffff61013a61229f565b16600052600060205260a0604060002065ffffffffffff6001825492015460405192835260ff8116151560208401526dffffffffffffffffffffffffffff8160081c16604084015263ffffffff8160781c16606084015260981c166080820152f35b600080fd5b3461019c576101af36612317565b906101b86129bd565b60009160005b82811061056f57506101d08493612588565b6000805b8481106102fc5750507fbb47ee3e183a558b1a2ff0874b079f3fc5478b7454eacf2bfc5af2ff5878f972600080a16000809360005b81811061024757610240868660007f575ff3acadd5ab348fe1855e217e0f3678f8d767d7494c9f9fefbee2e17cca4d8180a2613ba7565b6001600255005b6102a261025582848a612796565b73ffffffffffffffffffffffffffffffffffffffff6102766020830161282a565b167f575ff3acadd5ab348fe1855e217e0f3678f8d767d7494c9f9fefbee2e17cca4d600080a2806127d6565b906000915b8083106102b957505050600101610209565b909194976102f36102ed6001926102e78c8b6102e0826102da8e8b8d61269d565b9261265a565b5191613597565b90612409565b99612416565b950191906102a7565b6020610309828789612796565b61031f61031682806127d6565b9390920161282a565b9160009273ffffffffffffffffffffffffffffffffffffffff8091165b8285106103505750505050506001016101d4565b909192939561037f83610378610366848c61265a565b516103728b898b61269d565b856129f6565b9290613dd7565b9116840361050a576104a5576103958491613dd7565b9116610440576103b5576103aa600191612416565b96019392919061033c565b60a487604051907f220266b6000000000000000000000000000000000000000000000000000000008252600482015260406024820152602160448201527f41413332207061796d61737465722065787069726564206f72206e6f7420647560648201527f65000000000000000000000000000000000000000000000000000000000000006084820152fd5b608488604051907f220266b6000000000000000000000000000000000000000000000000000000008252600482015260406024820152601460448201527f41413334207369676e6174757265206572726f720000000000000000000000006064820152fd5b608488604051907f220266b6000000000000000000000000000000000000000000000000000000008252600482015260406024820152601760448201527f414132322065787069726564206f72206e6f74206475650000000000000000006064820152fd5b608489604051907f220266b6000000000000000000000000000000000000000000000000000000008252600482015260406024820152601460448201527f41413234207369676e6174757265206572726f720000000000000000000000006064820152fd5b61057a818487612796565b9361058585806127d6565b919095602073ffffffffffffffffffffffffffffffffffffffff6105aa82840161282a565b1697600192838a1461076657896105da575b5050505060019293949550906105d191612409565b939291016101be565b8060406105e892019061284b565b918a3b1561019c57929391906040519485937f2dd8113300000000000000000000000000000000000000000000000000000000855288604486016040600488015252606490818601918a60051b8701019680936000915b8c83106106e657505050505050838392610684927ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc8560009803016024860152612709565b03818a5afa90816106d7575b506106c657602486604051907f86a9f7500000000000000000000000000000000000000000000000000000000082526004820152fd5b93945084936105d1600189806105bc565b6106e0906121bd565b88610690565b91939596977fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c908a9294969a0301865288357ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffee18336030181121561019c57836107538793858394016128ec565b9a0196019301909189979695949261063f565b606483604051907f08c379a00000000000000000000000000000000000000000000000000000000082526004820152601760248201527f4141393620696e76616c69642061676772656761746f720000000000000000006044820152fd5b3461019c576020807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261019c576107fc61229f565b33600052600082526001604060002001908154916dffffffffffffffffffffffffffff8360081c16928315610a0a5765ffffffffffff8160981c1680156109ac57421061094e5760009373ffffffffffffffffffffffffffffffffffffffff859485947fffffffffffffff000000000000000000000000000000000000000000000000ff86951690556040517fb7c918e0e249f999e965cafeb6c664271b3f4317d296461500e71da39f0cbda33391806108da8786836020909392919373ffffffffffffffffffffffffffffffffffffffff60408201951681520152565b0390a2165af16108e8612450565b50156108f057005b606490604051907f08c379a00000000000000000000000000000000000000000000000000000000082526004820152601860248201527f6661696c656420746f207769746864726177207374616b6500000000000000006044820152fd5b606485604051907f08c379a00000000000000000000000000000000000000000000000000000000082526004820152601b60248201527f5374616b65207769746864726177616c206973206e6f742064756500000000006044820152fd5b606486604051907f08c379a00000000000000000000000000000000000000000000000000000000082526004820152601d60248201527f6d7573742063616c6c20756e6c6f636b5374616b6528292066697273740000006044820152fd5b606485604051907f08c379a00000000000000000000000000000000000000000000000000000000082526004820152601460248201527f4e6f207374616b6520746f2077697468647261770000000000000000000000006044820152fd5b3461019c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261019c573360005260006020526001604060002001805463ffffffff8160781c16908115610bdc5760ff1615610b7e5765ffffffffffff908142160191818311610b4f5780547fffffffffffffff000000000000ffffffffffffffffffffffffffffffffffff001678ffffffffffff00000000000000000000000000000000000000609885901b161790556040519116815233907ffa9b3c14cc825c412c9ed81b3ba365a5b459439403f18829e572ed53a4180f0a90602090a2005b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601160248201527f616c726561647920756e7374616b696e670000000000000000000000000000006044820152fd5b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152600a60248201527f6e6f74207374616b6564000000000000000000000000000000000000000000006044820152fd5b60207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261019c57610022610c6f61229f565b612748565b3461019c5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261019c5760043567ffffffffffffffff811161019c576020610cc8610d1b9236906004016122c2565b919073ffffffffffffffffffffffffffffffffffffffff9260405194859283927f570e1a360000000000000000000000000000000000000000000000000000000084528560048501526024840191612709565b03816000857f000000000000000000000000efc2c1444ebcc4db75e7613d20c6a62ff67a167c165af1908115610db757602492600092610d86575b50604051917f6ca7b806000000000000000000000000000000000000000000000000000000008352166004820152fd5b610da991925060203d602011610db0575b610da181836121ed565b8101906126dd565b9083610d56565b503d610d97565b6040513d6000823e3d90fd5b3461019c5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261019c57610dfa61229f565b60243567ffffffffffffffff811161019c57600091610e1e839236906004016122c2565b90816040519283928337810184815203915af4610e39612450565b90610e7e6040519283927f99410554000000000000000000000000000000000000000000000000000000008452151560048401526040602484015260448301906123c6565b0390fd5b3461019c57610e9036612317565b610e9b9291926129bd565b610ea483612588565b60005b848110610f1c57506000927fbb47ee3e183a558b1a2ff0874b079f3fc5478b7454eacf2bfc5af2ff5878f972600080a16000915b858310610eec576102408585613ba7565b909193600190610f12610f0087898761269d565b610f0a888661265a565b519088613597565b0194019190610edb565b610f47610f40610f2e8385979561265a565b51610f3a84898761269d565b846129f6565b9190613dd7565b73ffffffffffffffffffffffffffffffffffffffff929183166110db5761107657610f7190613dd7565b911661101157610f8657600101929092610ea7565b60a490604051907f220266b6000000000000000000000000000000000000000000000000000000008252600482015260406024820152602160448201527f41413332207061796d61737465722065787069726564206f72206e6f7420647560648201527f65000000000000000000000000000000000000000000000000000000000000006084820152fd5b608482604051907f220266b6000000000000000000000000000000000000000000000000000000008252600482015260406024820152601460448201527f41413334207369676e6174757265206572726f720000000000000000000000006064820152fd5b608483604051907f220266b6000000000000000000000000000000000000000000000000000000008252600482015260406024820152601760448201527f414132322065787069726564206f72206e6f74206475650000000000000000006064820152fd5b608484604051907f220266b6000000000000000000000000000000000000000000000000000000008252600482015260406024820152601460448201527f41413234207369676e6174757265206572726f720000000000000000000000006064820152fd5b3461019c5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261019c5773ffffffffffffffffffffffffffffffffffffffff61118c61229f565b1660005260006020526020604060002054604051908152f35b3461019c5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261019c5773ffffffffffffffffffffffffffffffffffffffff6111f161229f565b6000608060405161120181612155565b828152826020820152826040820152826060820152015216600052600060205260a06040600020608060405161123681612155565b6001835493848352015490602081019060ff8316151582526dffffffffffffffffffffffffffff60408201818560081c16815263ffffffff936060840193858760781c16855265ffffffffffff978891019660981c1686526040519788525115156020880152511660408601525116606084015251166080820152f35b3461019c5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261019c5760206112ec61229f565b73ffffffffffffffffffffffffffffffffffffffff6113096122f0565b911660005260018252604060002077ffffffffffffffffffffffffffffffffffffffffffffffff821660005282526040600020547fffffffffffffffffffffffffffffffffffffffffffffffff00000000000000006040519260401b16178152f35b3461019c577ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc60208136011261019c576004359067ffffffffffffffff821161019c5761012090823603011261019c576113c9602091600401612480565b604051908152f35b3461019c5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261019c5761140861229f565b60243590336000526000602052604060002090815491828411611508576000808573ffffffffffffffffffffffffffffffffffffffff8295839561144c848a612443565b90556040805173ffffffffffffffffffffffffffffffffffffffff831681526020810185905233917fd1c19fbcd4551a5edfb66d43d2e337c04837afda3482b42bdf569a8fccdae5fb91a2165af16114a2612450565b50156114aa57005b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601260248201527f6661696c656420746f20776974686472617700000000000000000000000000006044820152fd5b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601960248201527f576974686472617720616d6f756e7420746f6f206c61726765000000000000006044820152fd5b3461019c5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261019c5761159d61229f565b73ffffffffffffffffffffffffffffffffffffffff6115ba6122f0565b9116600052600160205277ffffffffffffffffffffffffffffffffffffffffffffffff604060002091166000526020526020604060002054604051908152f35b3461019c5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261019c5760043577ffffffffffffffffffffffffffffffffffffffffffffffff811680910361019c5733600052600160205260406000209060005260205260406000206116728154612416565b9055005b6020807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261019c5760043563ffffffff9182821680920361019c5733600052600081526040600020928215611950576001840154908160781c1683106118f2576116f86dffffffffffffffffffffffffffff9182349160081c16612409565b93841561189457818511611836579065ffffffffffff61180592546040519061172082612155565b8152848101926001845260408201908816815260608201878152600160808401936000855233600052600089526040600020905181550194511515917fffffffffffffffffffffffffff0000000000000000000000000000000000000060ff72ffffffff0000000000000000000000000000006effffffffffffffffffffffffffff008954945160081b16945160781b1694169116171717835551167fffffffffffffff000000000000ffffffffffffffffffffffffffffffffffffff78ffffffffffff0000000000000000000000000000000000000083549260981b169116179055565b6040519283528201527fa5ae833d0bb1dcd632d98a8b70973e8516812898e19bf27b70071ebc8dc52c0160403392a2005b606483604051907f08c379a00000000000000000000000000000000000000000000000000000000082526004820152600e60248201527f7374616b65206f766572666c6f770000000000000000000000000000000000006044820152fd5b606483604051907f08c379a00000000000000000000000000000000000000000000000000000000082526004820152601260248201527f6e6f207374616b652073706563696669656400000000000000000000000000006044820152fd5b606482604051907f08c379a00000000000000000000000000000000000000000000000000000000082526004820152601c60248201527f63616e6e6f7420646563726561736520756e7374616b652074696d65000000006044820152fd5b606482604051907f08c379a00000000000000000000000000000000000000000000000000000000082526004820152601a60248201527f6d757374207370656369667920756e7374616b652064656c61790000000000006044820152fd5b3461019c5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261019c576004357fffffffff00000000000000000000000000000000000000000000000000000000811680910361019c57807f60fc6b6e0000000000000000000000000000000000000000000000000000000060209214908115611ad6575b8115611aac575b8115611a82575b8115611a58575b506040519015158152f35b7f01ffc9a70000000000000000000000000000000000000000000000000000000091501482611a4d565b7f3e84f0210000000000000000000000000000000000000000000000000000000081149150611a46565b7fcf28ef970000000000000000000000000000000000000000000000000000000081149150611a3f565b7f915074d80000000000000000000000000000000000000000000000000000000081149150611a38565b3461019c576102007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261019c5767ffffffffffffffff60043581811161019c573660238201121561019c57611b62903690602481600401359101612268565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc36016101c0811261019c5761014060405191611b9e83612155565b1261019c5760405192611bb0846121a0565b60243573ffffffffffffffffffffffffffffffffffffffff8116810361019c578452602093604435858201526064356040820152608435606082015260a435608082015260c43560a082015260e43560c08201526101043573ffffffffffffffffffffffffffffffffffffffff8116810361019c5760e08201526101243561010082015261014435610120820152825261016435848301526101843560408301526101a43560608301526101c43560808301526101e43590811161019c57611c7c9036906004016122c2565b905a3033036120f7578351606081015195603f5a0260061c61271060a0840151890101116120ce5760009681519182611ff0575b5050505090611cca915a9003608085015101923691612268565b925a90600094845193611cdc85613ccc565b9173ffffffffffffffffffffffffffffffffffffffff60e0870151168015600014611ea957505073ffffffffffffffffffffffffffffffffffffffff855116935b5a9003019360a06060820151910151016080860151850390818111611e95575b50508302604085015192818410600014611dce5750506003811015611da157600203611d79576113c99293508093611d7481613d65565b613cf6565b5050507fdeadaa51000000000000000000000000000000000000000000000000000000008152fd5b6024857f4e487b710000000000000000000000000000000000000000000000000000000081526021600452fd5b81611dde92979396940390613c98565b506003841015611e6857507f49628fd1471006c1482da88028e9ce4dbb080b815c9b0344d39e5a8e6ec1419f60808683015192519473ffffffffffffffffffffffffffffffffffffffff865116948873ffffffffffffffffffffffffffffffffffffffff60e0890151169701519160405192835215898301528760408301526060820152a46113c9565b807f4e487b7100000000000000000000000000000000000000000000000000000000602492526021600452fd5b6064919003600a0204909301928780611d3d565b8095918051611eba575b5050611d1d565b6003861015611fc1576002860315611eb35760a088015190823b1561019c57600091611f2491836040519586809581947f7c627b210000000000000000000000000000000000000000000000000000000083528d60048401526080602484015260848301906123c6565b8b8b0260448301528b60648301520393f19081611fad575b50611fa65787893d610800808211611f9e575b506040519282828501016040528184528284013e610e7e6040519283927fad7954bc000000000000000000000000000000000000000000000000000000008452600484015260248301906123c6565b905083611f4f565b8980611eb3565b611fb89199506121bd565b6000978a611f3c565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b91600092918380938c73ffffffffffffffffffffffffffffffffffffffff885116910192f115612023575b808080611cb0565b611cca929195503d6108008082116120c6575b5060405190888183010160405280825260008983013e805161205f575b5050600194909161201b565b7f1c4fada7374c0a9ee8841fc38afe82932dc0f8e69012e927f061a8bae611a20188870151918973ffffffffffffffffffffffffffffffffffffffff8551169401516120bc604051928392835260408d84015260408301906123c6565b0390a38680612053565b905088612036565b877fdeaddead000000000000000000000000000000000000000000000000000000006000526000fd5b606486604051907f08c379a00000000000000000000000000000000000000000000000000000000082526004820152601760248201527f4141393220696e7465726e616c2063616c6c206f6e6c790000000000000000006044820152fd5b60a0810190811067ffffffffffffffff82111761217157604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b610140810190811067ffffffffffffffff82111761217157604052565b67ffffffffffffffff811161217157604052565b6060810190811067ffffffffffffffff82111761217157604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761217157604052565b67ffffffffffffffff811161217157601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b9291926122748261222e565b9161228260405193846121ed565b82948184528183011161019c578281602093846000960137010152565b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361019c57565b9181601f8401121561019c5782359167ffffffffffffffff831161019c576020838186019501011161019c57565b6024359077ffffffffffffffffffffffffffffffffffffffffffffffff8216820361019c57565b9060407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc83011261019c5760043567ffffffffffffffff9283821161019c578060238301121561019c57816004013593841161019c5760248460051b8301011161019c57602401919060243573ffffffffffffffffffffffffffffffffffffffff8116810361019c5790565b60005b8381106123b65750506000910152565b81810151838201526020016123a6565b907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f602093612402815180928187528780880191016123a3565b0116010190565b91908201809211610b4f57565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114610b4f5760010190565b91908203918211610b4f57565b3d1561247b573d906124618261222e565b9161246f60405193846121ed565b82523d6000602084013e565b606090565b604061248e8183018361284b565b90818351918237206124a3606084018461284b565b90818451918237209260c06124bb60e083018361284b565b908186519182372091845195602087019473ffffffffffffffffffffffffffffffffffffffff833516865260208301358789015260608801526080870152608081013560a087015260a081013582870152013560e08501526101009081850152835261012083019167ffffffffffffffff918484108385111761217157838252845190206101408501908152306101608601524661018086015260608452936101a00191821183831017612171575251902090565b67ffffffffffffffff81116121715760051b60200190565b9061259282612570565b6040906125a260405191826121ed565b8381527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe06125d08295612570565b019160005b8381106125e25750505050565b60209082516125f081612155565b83516125fb816121a0565b600081526000849181838201528187820152816060818184015260809282848201528260a08201528260c08201528260e082015282610100820152826101208201528652818587015281898701528501528301528286010152016125d5565b805182101561266e5760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b919081101561266e5760051b810135907ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffee18136030182121561019c570190565b9081602091031261019c575173ffffffffffffffffffffffffffffffffffffffff8116810361019c5790565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0938186528686013760008582860101520116010190565b7f2da466a7b24304f47e87fa2e1e5a81b9831ce54fec19055ce277ca2f39ba42c4602073ffffffffffffffffffffffffffffffffffffffff61278a3485613c98565b936040519485521692a2565b919081101561266e5760051b810135907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa18136030182121561019c570190565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe18136030182121561019c570180359067ffffffffffffffff821161019c57602001918160051b3603831361019c57565b3573ffffffffffffffffffffffffffffffffffffffff8116810361019c5790565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe18136030182121561019c570180359067ffffffffffffffff821161019c5760200191813603831361019c57565b90357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe18236030181121561019c57016020813591019167ffffffffffffffff821161019c57813603831361019c57565b61012091813573ffffffffffffffffffffffffffffffffffffffff811680910361019c576129626129476129ba9561299b93855260208601356020860152612937604087018761289c565b9091806040880152860191612709565b612954606086018661289c565b908583036060870152612709565b6080840135608084015260a084013560a084015260c084013560c084015261298d60e085018561289c565b9084830360e0860152612709565b916129ac610100918281019061289c565b929091818503910152612709565b90565b60028054146129cc5760028055565b60046040517f3ee5aeb5000000000000000000000000000000000000000000000000000000008152fd5b926000905a93805194843573ffffffffffffffffffffffffffffffffffffffff811680910361019c5786526020850135602087015260808501356fffffffffffffffffffffffffffffffff90818116606089015260801c604088015260a086013560c088015260c086013590811661010088015260801c610120870152612a8060e086018661284b565b801561357b576034811061351d578060141161019c578060241161019c5760341161019c57602481013560801c60a0880152601481013560801c60808801523560601c60e08701525b612ad285612480565b60208301526040860151946effffffffffffffffffffffffffffff8660c08901511760608901511760808901511760a0890151176101008901511761012089015117116134bf57604087015160608801510160808801510160a08801510160c0880151016101008801510296835173ffffffffffffffffffffffffffffffffffffffff81511690612b66604085018561284b565b806131e4575b505060e0015173ffffffffffffffffffffffffffffffffffffffff1690600082156131ac575b6020612bd7918b828a01516000868a604051978896879586937f19822f7c00000000000000000000000000000000000000000000000000000000855260048501613db5565b0393f160009181613178575b50612c8b573d8c610800808311612c83575b50604051916020818401016040528083526000602084013e610e7e6040519283927f65c8fd4d000000000000000000000000000000000000000000000000000000008452600484015260606024840152600d60648401527f4141323320726576657274656400000000000000000000000000000000000000608484015260a0604484015260a48301906123c6565b915082612bf5565b9a92939495969798999a91156130f2575b509773ffffffffffffffffffffffffffffffffffffffff835116602084015190600052600160205260406000208160401c60005260205267ffffffffffffffff604060002091825492612cee84612416565b9055160361308d575a8503116130285773ffffffffffffffffffffffffffffffffffffffff60e0606093015116612d42575b509060a09184959697986040608096015260608601520135905a900301910152565b969550505a9683519773ffffffffffffffffffffffffffffffffffffffff60e08a01511680600052600060205260406000208054848110612fc3576080612dcd9a9b9c600093878094039055015192602089015183604051809d819582947f52b7512c0000000000000000000000000000000000000000000000000000000084528c60048501613db5565b039286f1978860009160009a612f36575b50612e86573d8b610800808311612e7e575b50604051916020818401016040528083526000602084013e610e7e6040519283927f65c8fd4d000000000000000000000000000000000000000000000000000000008452600484015260606024840152600d60648401527f4141333320726576657274656400000000000000000000000000000000000000608484015260a0604484015260a48301906123c6565b915082612df0565b9991929394959697989998925a900311612eab57509096959094939291906080612d20565b60a490604051907f220266b6000000000000000000000000000000000000000000000000000000008252600482015260406024820152602760448201527f41413336206f766572207061796d6173746572566572696669636174696f6e4760648201527f61734c696d6974000000000000000000000000000000000000000000000000006084820152fd5b915098503d90816000823e612f4b82826121ed565b604081838101031261019c5780519067ffffffffffffffff821161019c57828101601f83830101121561019c578181015191612f868361222e565b93612f9460405195866121ed565b838552820160208483850101011161019c57602092612fba9184808701918501016123a3565b01519838612dde565b60848b604051907f220266b6000000000000000000000000000000000000000000000000000000008252600482015260406024820152601e60448201527f41413331207061796d6173746572206465706f73697420746f6f206c6f7700006064820152fd5b608490604051907f220266b6000000000000000000000000000000000000000000000000000000008252600482015260406024820152601e60448201527f41413236206f76657220766572696669636174696f6e4761734c696d697400006064820152fd5b608482604051907f220266b6000000000000000000000000000000000000000000000000000000008252600482015260406024820152601a60448201527f4141323520696e76616c6964206163636f756e74206e6f6e63650000000000006064820152fd5b600052600060205260406000208054808c11613113578b9003905538612c9c565b608484604051907f220266b6000000000000000000000000000000000000000000000000000000008252600482015260406024820152601760448201527f41413231206469646e2774207061792070726566756e640000000000000000006064820152fd5b9091506020813d6020116131a4575b81613194602093836121ed565b8101031261019c57519038612be3565b3d9150613187565b508060005260006020526040600020548a81116000146131d75750612bd7602060005b915050612b92565b6020612bd7918c036131cf565b833b61345a57604088510151602060405180927f570e1a360000000000000000000000000000000000000000000000000000000082528260048301528160008161323260248201898b612709565b039273ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000efc2c1444ebcc4db75e7613d20c6a62ff67a167c1690f1908115610db75760009161343b575b5073ffffffffffffffffffffffffffffffffffffffff811680156133d6578503613371573b1561330c5760141161019c5773ffffffffffffffffffffffffffffffffffffffff9183887fd51a9c61267aa6196961883ecf5ff2da6619c37dac0fa92122513fb32c032d2d604060e0958787602086015195510151168251913560601c82526020820152a391612b6c565b60848d604051907f220266b6000000000000000000000000000000000000000000000000000000008252600482015260406024820152602060448201527f4141313520696e6974436f6465206d757374206372656174652073656e6465726064820152fd5b60848e604051907f220266b6000000000000000000000000000000000000000000000000000000008252600482015260406024820152602060448201527f4141313420696e6974436f6465206d7573742072657475726e2073656e6465726064820152fd5b60848f604051907f220266b6000000000000000000000000000000000000000000000000000000008252600482015260406024820152601b60448201527f4141313320696e6974436f6465206661696c6564206f72204f4f4700000000006064820152fd5b613454915060203d602011610db057610da181836121ed565b3861327c565b60848d604051907f220266b6000000000000000000000000000000000000000000000000000000008252600482015260406024820152601f60448201527f414131302073656e64657220616c726561647920636f6e7374727563746564006064820152fd5b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601860248201527f41413934206761732076616c756573206f766572666c6f7700000000000000006044820152fd5b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601d60248201527f4141393320696e76616c6964207061796d6173746572416e64446174610000006044820152fd5b5050600060e087015260006080870152600060a0870152612ac9565b9092915a906060810151916040928351967fffffffff00000000000000000000000000000000000000000000000000000000886135d7606084018461284b565b600060038211613b9f575b7f8dd7712f0000000000000000000000000000000000000000000000000000000094168403613a445750505061379d6000926136b292602088015161363a8a5193849360208501528b602485015260648401906128ec565b90604483015203906136727fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0928381018352826121ed565b61379189519485927e42dc5300000000000000000000000000000000000000000000000000000000602085015261020060248501526102248401906123c6565b613760604484018b60806101a091805173ffffffffffffffffffffffffffffffffffffffff808251168652602082015160208701526040820151604087015260608201516060870152838201518487015260a082015160a087015260c082015160c087015260e08201511660e0860152610100808201519086015261012080910151908501526020810151610140850152604081015161016085015260608101516101808501520151910152565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc83820301610204840152876123c6565b039081018352826121ed565b6020918183809351910182305af1600051988652156137bf575b505050505050565b909192939495965060003d8214613a3a575b7fdeaddead00000000000000000000000000000000000000000000000000000000810361385b57608487878051917f220266b600000000000000000000000000000000000000000000000000000000835260048301526024820152600f60448201527f41413935206f7574206f662067617300000000000000000000000000000000006064820152fd5b7fdeadaa510000000000000000000000000000000000000000000000000000000091929395949650146000146138c55750506138a961389e6138b8935a90612443565b608085015190612409565b9083015183611d748295613d65565b905b3880808080806137b7565b909261395290828601518651907ff62676f440ff169a3a9afdbf812e89e7f95975ee8e5c31214ffdef631c5f479273ffffffffffffffffffffffffffffffffffffffff9580878551169401516139483d610800808211613a32575b508a519084818301018c5280825260008583013e8a805194859485528401528a8301906123c6565b0390a35a90612443565b916139636080860193845190612409565b926000905a94829488519761397789613ccc565b948260e08b0151168015600014613a1857505050875116955b5a9003019560a06060820151910151019051860390818111613a04575b5050840290850151928184106000146139de57505080611e68575090816139d89293611d7481613d65565b906138ba565b6139ee9082849397950390613c98565b50611e68575090826139ff92613cf6565b6139d8565b6064919003600a02049094019338806139ad565b90919892509751613a2a575b50613990565b955038613a24565b905038613920565b8181803e516137d1565b613b97945082935090613a8c917e42dc53000000000000000000000000000000000000000000000000000000006020613b6b9501526102006024860152610224850191612709565b613b3a604484018860806101a091805173ffffffffffffffffffffffffffffffffffffffff808251168652602082015160208701526040820151604087015260608201516060870152838201518487015260a082015160a087015260c082015160c087015260e08201511660e0860152610100808201519086015261012080910151908501526020810151610140850152604081015161016085015260608101516101808501520151910152565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc83820301610204840152846123c6565b037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe081018952886121ed565b60008761379d565b5081356135e2565b73ffffffffffffffffffffffffffffffffffffffff168015613c3a57600080809381935af1613bd4612450565b5015613bdc57565b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601f60248201527f41413931206661696c65642073656e6420746f2062656e6566696369617279006044820152fd5b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601860248201527f4141393020696e76616c69642062656e656669636961727900000000000000006044820152fd5b73ffffffffffffffffffffffffffffffffffffffff166000526000602052613cc66040600020918254612409565b80915590565b610120610100820151910151808214613cf257480180821015613ced575090565b905090565b5090565b9190917f49628fd1471006c1482da88028e9ce4dbb080b815c9b0344d39e5a8e6ec1419f6080602083015192519473ffffffffffffffffffffffffffffffffffffffff946020868851169660e089015116970151916040519283526000602084015260408301526060820152a4565b60208101519051907f67b4fa9642f42120bf031f3051d1824b0fe25627945b27b8a6a65d5761d5482e60208073ffffffffffffffffffffffffffffffffffffffff855116940151604051908152a3565b613dcd604092959493956060835260608301906128ec565b9460208201520152565b8015613e6457600060408051613dec816121d1565b828152826020820152015273ffffffffffffffffffffffffffffffffffffffff811690604065ffffffffffff91828160a01c16908115613e5c575b60d01c92825191613e37836121d1565b8583528460208401521691829101524211908115613e5457509091565b905042109091565b839150613e27565b5060009060009056fea2646970667358221220b094fd69f04977ae9458e5ba422d01cd2d20dbcfca0992ff37f19aa07deec25464736f6c63430008170033"; + +bytes constant CREATOR_0_7_BYTECODE = hex"6080600436101561000f57600080fd5b6000803560e01c63570e1a361461002557600080fd5b3461018a5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261018a576004359167ffffffffffffffff9081841161018657366023850112156101865783600401358281116101825736602482870101116101825780601411610182577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffec810192808411610155577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0603f81600b8501160116830190838210908211176101555792846024819482600c60209a968b9960405286845289840196603889018837830101525193013560601c5af1908051911561014d575b5073ffffffffffffffffffffffffffffffffffffffff60405191168152f35b90503861012e565b6024857f4e487b710000000000000000000000000000000000000000000000000000000081526041600452fd5b8380fd5b8280fd5b80fdfea26469706673582212207adef8895ad3393b02fab10a111d85ea80ff35366aa43995f4ea20e67f29200664736f6c63430008170033"; + +bytes constant VERIFYINGPAYMASTER_BYTECODE = hex"6080604052600436106100b85760003560e01c80630396cb60146100bd578063205c2878146100d257806323d9ac9b146100f257806352b7512c1461013c5780635829c5f51461016a578063715018a6146101985780637c627b21146101ad5780638da5cb5b146101cd57806394d4ad60146101e2578063b0d691fe14610212578063bb9fe6bf14610246578063c23a5cea1461025b578063c399ec881461027b578063d0e30db014610290578063f2fde38b14610298575b600080fd5b6100d06100cb366004610d99565b6102b8565b005b3480156100de57600080fd5b506100d06100ed366004610ddb565b610343565b3480156100fe57600080fd5b506101267f000000000000000000000000000000000000000000000000000000000000000081565b6040516101339190610e07565b60405180910390f35b34801561014857600080fd5b5061015c610157366004610e34565b6103b5565b604051610133929190610e81565b34801561017657600080fd5b5061018a610185366004610ef1565b6103d9565b604051908152602001610133565b3480156101a457600080fd5b506100d06104e9565b3480156101b957600080fd5b506100d06101c8366004610f8f565b6104fd565b3480156101d957600080fd5b50610126610519565b3480156101ee57600080fd5b506102026101fd366004610ff9565b610528565b604051610133949392919061103a565b34801561021e57600080fd5b506101267f000000000000000000000000000000000000000000000000000000000000000081565b34801561025257600080fd5b506100d0610570565b34801561026757600080fd5b506100d0610276366004611086565b6105ed565b34801561028757600080fd5b5061018a61066f565b6100d0610704565b3480156102a457600080fd5b506100d06102b3366004611086565b61076b565b6102c06107e9565b604051621cb65b60e51b815263ffffffff821660048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031690630396cb609034906024016000604051808303818588803b15801561032757600080fd5b505af115801561033b573d6000803e3d6000fd5b505050505050565b61034b6107e9565b60405163040b850f60e31b81526001600160a01b038381166004830152602482018390527f0000000000000000000000000000000000000000000000000000000000000000169063205c287890604401600060405180830381600087803b15801561032757600080fd5b606060006103c1610848565b6103cc8585856108b8565b915091505b935093915050565b600083358060208601356103f060408801886110a3565b6040516103fe9291906110e9565b60405190819003902061041460608901896110a3565b6040516104229291906110e9565b604051908190039020608089013561043d60e08b018b6110a3565b61044c916034916014916110f9565b61045591611123565b604080516001600160a01b0390971660208801528601949094526060850192909252608084015260a08084019190915260c08084019290925287013560e0830152860135610100820152466101208201523061014082015265ffffffffffff80861661016083015284166101808201526101a001604051602081830303815290604052805190602001209150509392505050565b6104f16107e9565b6104fb6000610a6f565b565b610505610848565b6105128585858585610abf565b5050505050565b6000546001600160a01b031690565b600080368161053a85603481896110f9565b8101906105479190611141565b9094509250858561055a60346040611174565b6105659282906110f9565b949793965094505050565b6105786107e9565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663bb9fe6bf6040518163ffffffff1660e01b8152600401600060405180830381600087803b1580156105d357600080fd5b505af11580156105e7573d6000803e3d6000fd5b50505050565b6105f56107e9565b60405163611d2e7560e11b81526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063c23a5cea90610641908490600401610e07565b600060405180830381600087803b15801561065b57600080fd5b505af1158015610512573d6000803e3d6000fd5b6040516370a0823160e01b81526000906001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016906370a08231906106be903090600401610e07565b602060405180830381865afa1580156106db573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906106ff9190611195565b905090565b60405163b760faf960e01b81526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063b760faf9903490610752903090600401610e07565b6000604051808303818588803b15801561065b57600080fd5b6107736107e9565b6001600160a01b0381166107dd5760405162461bcd60e51b815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201526564647265737360d01b60648201526084015b60405180910390fd5b6107e681610a6f565b50565b336107f2610519565b6001600160a01b0316146104fb5760405162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e657260448201526064016107d4565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146104fb5760405162461bcd60e51b815260206004820152601560248201527414d95b99195c881b9bdd08115b9d1c9e541bda5b9d605a1b60448201526064016107d4565b60606000808036816108d06101fd60e08b018b6110a3565b9296509094509250905060408114806108e95750604181145b61095d576040805162461bcd60e51b81526020600482015260248101919091527f566572696679696e675061796d61737465723a20696e76616c6964207369676e60448201527f6174757265206c656e67746820696e207061796d6173746572416e644461746160648201526084016107d4565b600061099f61096d8b87876103d9565b7b0ca2ba3432b932bab69029b4b3b732b21026b2b9b9b0b3b29d05199960211b6000908152601c91909152603c902090565b90506109e18184848080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250610af792505050565b6001600160a01b03167f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031614610a4457610a2560018686610b1d565b60405180602001604052806000815250909650965050505050506103d1565b610a5060008686610b1d565b6040805160208101909152600081529b909a5098505050505050505050565b600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b60405162461bcd60e51b815260206004820152600d60248201526c6d757374206f7665727269646560981b60448201526064016107d4565b6000806000610b068585610b55565b91509150610b1381610b9a565b5090505b92915050565b600060d08265ffffffffffff16901b60a08465ffffffffffff16901b85610b45576000610b48565b60015b60ff161717949350505050565b6000808251604103610b8b5760208301516040840151606085015160001a610b7f87828585610cdf565b94509450505050610b93565b506000905060025b9250929050565b6000816004811115610bae57610bae6111ae565b03610bb65750565b6001816004811115610bca57610bca6111ae565b03610c125760405162461bcd60e51b815260206004820152601860248201527745434453413a20696e76616c6964207369676e617475726560401b60448201526064016107d4565b6002816004811115610c2657610c266111ae565b03610c735760405162461bcd60e51b815260206004820152601f60248201527f45434453413a20696e76616c6964207369676e6174757265206c656e6774680060448201526064016107d4565b6003816004811115610c8757610c876111ae565b036107e65760405162461bcd60e51b815260206004820152602260248201527f45434453413a20696e76616c6964207369676e6174757265202773272076616c604482015261756560f01b60648201526084016107d4565b6000806fa2a8918ca85bafe22016d0b997e4df60600160ff1b03831115610d0c5750600090506003610d90565b6040805160008082526020820180845289905260ff881692820192909252606081018690526080810185905260019060a0016020604051602081039080840390855afa158015610d60573d6000803e3d6000fd5b5050604051601f1901519150506001600160a01b038116610d8957600060019250925050610d90565b9150600090505b94509492505050565b600060208284031215610dab57600080fd5b813563ffffffff81168114610dbf57600080fd5b9392505050565b6001600160a01b03811681146107e657600080fd5b60008060408385031215610dee57600080fd5b8235610df981610dc6565b946020939093013593505050565b6001600160a01b0391909116815260200190565b60006101208284031215610e2e57600080fd5b50919050565b600080600060608486031215610e4957600080fd5b83356001600160401b03811115610e5f57600080fd5b610e6b86828701610e1b565b9660208601359650604090950135949350505050565b604081526000835180604084015260005b81811015610eaf5760208187018101516060868401015201610e92565b506000606082850101526060601f19601f8301168401019150508260208301529392505050565b803565ffffffffffff81168114610eec57600080fd5b919050565b600080600060608486031215610f0657600080fd5b83356001600160401b03811115610f1c57600080fd5b610f2886828701610e1b565b935050610f3760208501610ed6565b9150610f4560408501610ed6565b90509250925092565b60008083601f840112610f6057600080fd5b5081356001600160401b03811115610f7757600080fd5b602083019150836020828501011115610b9357600080fd5b600080600080600060808688031215610fa757600080fd5b853560038110610fb657600080fd5b945060208601356001600160401b03811115610fd157600080fd5b610fdd88828901610f4e565b9699909850959660408101359660609091013595509350505050565b6000806020838503121561100c57600080fd5b82356001600160401b0381111561102257600080fd5b61102e85828601610f4e565b90969095509350505050565b600065ffffffffffff808716835280861660208401525060606040830152826060830152828460808401376000608084840101526080601f19601f850116830101905095945050505050565b60006020828403121561109857600080fd5b8135610dbf81610dc6565b6000808335601e198436030181126110ba57600080fd5b8301803591506001600160401b038211156110d457600080fd5b602001915036819003821315610b9357600080fd5b8183823760009101908152919050565b6000808585111561110957600080fd5b8386111561111657600080fd5b5050820193919092039150565b80356020831015610b1757600019602084900360031b1b1692915050565b6000806040838503121561115457600080fd5b61115d83610ed6565b915061116b60208401610ed6565b90509250929050565b80820180821115610b1757634e487b7160e01b600052601160045260246000fd5b6000602082840312156111a757600080fd5b5051919050565b634e487b7160e01b600052602160045260246000fdfea2646970667358221220b39fe5fcd01271dac339cd8e4ceb50933262bdaa8ec1586bc91f0eb5eed0e91264736f6c63430008170033"; + +address constant VERIFYINGPAYMASTER_ADDRESS = 0xe1Fb85Ec54767ED89252751F6667CF566b16f1F0; diff --git a/src/test/smart-wallet/utils/AATestBase.sol b/src/test/smart-wallet/utils/AATestBase.sol new file mode 100644 index 000000000..76dd150a8 --- /dev/null +++ b/src/test/smart-wallet/utils/AATestBase.sol @@ -0,0 +1,333 @@ +pragma solidity ^0.8.0; + +import { IEntryPoint } from "contracts/prebuilts/account/interfaces/IEntryPoint.sol"; +import { EntryPoint } from "contracts/prebuilts/account/utils/EntryPoint.sol"; +import { PackedUserOperation } from "contracts/prebuilts/account/interfaces/PackedUserOperation.sol"; +import { IAccount } from "contracts/prebuilts/account/interfaces/IAccount.sol"; +import { VERIFYINGPAYMASTER_BYTECODE, VERIFYINGPAYMASTER_ADDRESS, ENTRYPOINT_0_7_BYTECODE, CREATOR_0_7_BYTECODE } from "./AATestArtifacts.sol"; +import { UserOperationLib } from "contracts/prebuilts/account/utils/UserOperationLib.sol"; +import { VerifyingPaymaster } from "./VerifyingPaymaster.sol"; + +import "contracts/external-deps/openzeppelin/utils/cryptography/ECDSA.sol"; +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import { MockERC20 } from "../../mocks/MockERC20.sol"; + +interface IVerifyingPaymaster { + function owner() external view returns (address); + + function getHash( + PackedUserOperation calldata userOp, + uint48 validUntil, + uint48 validAfter + ) external view returns (bytes32); +} + +interface VmModified { + function cool(address _target) external; + + function keyExists(string calldata, string calldata) external returns (bool); + + function parseJsonKeys(string calldata json, string calldata key) external pure returns (string[] memory keys); +} + +uint256 constant OV_FIXED = 21000; +uint256 constant OV_PER_USEROP = 18300; +uint256 constant OV_PER_WORD = 4; +uint256 constant OV_PER_ZERO_BYTE = 4; +uint256 constant OV_PER_NONZERO_BYTE = 16; + +abstract contract AAGasProfileBase is Test { + string public name; + string public scenarioName; + uint256 sum; + string jsonObj; + IEntryPoint public entryPoint; + address payable public beneficiary; + IAccount public account; + address public owner; + uint256 public key; + VerifyingPaymaster public paymaster; + address public verifier; + uint256 public verifierKey; + bool public writeGasProfile = false; + + function(PackedUserOperation memory) internal view returns (bytes memory) paymasterData; + function(PackedUserOperation memory) internal view returns (bytes memory) dummyPaymasterData; + + function initializeTest(string memory _name) internal { + writeGasProfile = vm.envOr("WRITE_GAS_PROFILE", false); + name = _name; + address _testEntrypoint = address(new EntryPoint()); + entryPoint = IEntryPoint(payable(address(0x0000000071727De22E5E9d8BAf0edAc6f37da032))); + vm.etch(address(entryPoint), ENTRYPOINT_0_7_BYTECODE); // ENTRYPOINT_0_7_BYTECODE + vm.etch(0xEFC2c1444eBCC4Db75e7613d20C6a62fF67A167C, CREATOR_0_7_BYTECODE); + beneficiary = payable(makeAddr("beneficiary")); + vm.deal(beneficiary, 1e18); + paymasterData = emptyPaymasterAndData; + dummyPaymasterData = emptyPaymasterAndData; + (verifier, verifierKey) = makeAddrAndKey("VERIFIER"); + address _testPaymaster = address(new VerifyingPaymaster(entryPoint, verifier)); + paymaster = VerifyingPaymaster(VERIFYINGPAYMASTER_ADDRESS); + vm.etch(address(paymaster), _testPaymaster.code); // VERIFYINGPAYMASTER_BYTECODE + } + + function setAccount() internal { + (owner, key) = makeAddrAndKey("Owner"); + account = getAccountAddr(owner); + vm.deal(address(account), 1e18); + } + + function packPaymasterStaticFields( + address paymaster, + uint256 validationGasLimit, + uint256 postOpGasLimit, + uint48 validUntil, + uint48 validAfter, + bytes memory signature + ) internal pure returns (bytes memory) { + // Pack the static fields using abi.encodePacked + bytes memory packed = abi.encodePacked( + paymaster, + uint128(validationGasLimit), + uint128(postOpGasLimit), + uint256(validUntil), // Padding to make it 32 bytes + uint256(validAfter) // Padding to make it 32 bytes + ); + + // Append the signature to the packed data + packed = abi.encodePacked(packed, signature); + + return packed; + } + + function fillUserOp(bytes memory _data) internal view returns (PackedUserOperation memory op) { + op.sender = address(account); + op.nonce = entryPoint.getNonce(address(account), 0); + if (address(account).code.length == 0) { + op.initCode = getInitCode(owner); + } + + uint128 verificationGasLimit = 500000; + uint128 callGasLimit = 500000; + bytes32 packedGasLimits = (bytes32(uint256(verificationGasLimit)) << 128) | bytes32(uint256(callGasLimit)); + + bytes memory paymasterData = packPaymasterStaticFields( + address(paymaster), + 100_000, + 100_000, + type(uint48).max, + 0, + hex"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ); + + op.callData = _data; + op.accountGasLimits = packedGasLimits; + op.preVerificationGas = 500000; + op.gasFees = (bytes32(uint256(1)) << 128) | bytes32(uint256(1)); + op.signature = getDummySig(op); + op.paymasterAndData = dummyPaymasterData(op); + op.preVerificationGas = calculatePreVerificationGas(op); + op.paymasterAndData = paymasterData; + + bytes32 paymasterHash = paymaster.getHash(op, type(uint48).max, 0); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(verifierKey, ECDSA.toEthSignedMessageHash(paymasterHash)); + bytes memory signature = abi.encodePacked(r, s, v); + + op.paymasterAndData = packPaymasterStaticFields( + address(paymaster), + 100_000, + 100_000, + type(uint48).max, + 0, + signature + ); + op.signature = getSignature(op); + } + + function signUserOpHash( + uint256 _key, + PackedUserOperation memory _op + ) internal view returns (bytes memory signature) { + bytes32 hash = entryPoint.getUserOpHash(_op); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_key, ECDSA.toEthSignedMessageHash(hash)); + signature = abi.encodePacked(r, s, v); + } + + function executeUserOp(PackedUserOperation memory _op, string memory _test, uint256 _value) internal { + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + ops[0] = _op; + uint256 eth_before; + if (_op.paymasterAndData.length > 0) { + eth_before = entryPoint.balanceOf(address(paymaster)); + } else { + eth_before = entryPoint.balanceOf(address(account)) + address(account).balance; + } + // vm.cool to be introduced to foundry + //VmModified(address(vm)).cool(address(entryPoint)); + //VmModified(address(vm)).cool(address(account)); + entryPoint.handleOps(ops, beneficiary); + uint256 eth_after; + if (_op.paymasterAndData.length > 0) { + eth_after = entryPoint.balanceOf(address(paymaster)); + } else { + eth_after = entryPoint.balanceOf(address(account)) + address(account).balance + _value; + } + if (!writeGasProfile) { + console.log("case - %s", _test); + console.log(" gasUsed : ", eth_before - eth_after); + console.log(" calldatacost : ", calldataCost(pack(_op))); + } + if (writeGasProfile && bytes(scenarioName).length > 0) { + uint256 gasUsed = eth_before - eth_after; + vm.serializeUint(jsonObj, _test, gasUsed); + sum += gasUsed; + } + } + + function testCreation() internal { + PackedUserOperation memory op = fillUserOp(fillData(address(0), 0, "")); + executeUserOp(op, "creation", 0); + } + + function testTransferNative(address _recipient, uint256 _amount) internal { + vm.skip(writeGasProfile); + createAccount(owner); + _amount = bound(_amount, 1, address(account).balance / 2); + PackedUserOperation memory op = fillUserOp(fillData(_recipient, _amount, "")); + executeUserOp(op, "native", _amount); + } + + function testTransferNative() internal { + createAccount(owner); + uint256 amount = 5e17; + address recipient = makeAddr("recipient"); + PackedUserOperation memory op = fillUserOp(fillData(recipient, amount, "")); + executeUserOp(op, "native", amount); + } + + function testTransferERC20() internal { + createAccount(owner); + MockERC20 mockERC20 = new MockERC20(); + mockERC20.mint(address(account), 1e18); + uint256 amount = 5e17; + address recipient = makeAddr("recipient"); + uint256 balance = mockERC20.balanceOf(recipient); + PackedUserOperation memory op = fillUserOp( + fillData(address(mockERC20), 0, abi.encodeWithSelector(mockERC20.transfer.selector, recipient, amount)) + ); + executeUserOp(op, "erc20", 0); + assertEq(mockERC20.balanceOf(recipient), balance + amount); + } + + function testBenchmark1Vanila() external { + scenarioName = "vanila"; + jsonObj = string(abi.encodePacked(scenarioName, " ", name)); + entryPoint.depositTo{ value: 1000e18 }(address(paymaster)); + testCreation(); + testTransferNative(); + testTransferERC20(); + if (writeGasProfile) { + string memory res = vm.serializeUint(jsonObj, "sum", sum); + console.log(res); + vm.writeJson(res, string.concat("./results/", scenarioName, "_", name, ".json")); + } + } + + function testBenchmark2Paymaster() external { + scenarioName = "paymaster"; + jsonObj = string(abi.encodePacked(scenarioName, " ", name)); + entryPoint.depositTo{ value: 1000e18 }(address(paymaster)); + paymasterData = validatePaymasterAndData; + dummyPaymasterData = getDummyPaymasterAndData; + + testCreation(); + testTransferNative(); + testTransferERC20(); + if (writeGasProfile) { + string memory res = vm.serializeUint(jsonObj, "sum", sum); + console.log(res); + vm.writeJson(res, string.concat("./results/", scenarioName, "_", name, ".json")); + } + } + + function testBenchmark3Deposit() external { + scenarioName = "deposit"; + jsonObj = string(abi.encodePacked(scenarioName, " ", name)); + entryPoint.depositTo{ value: 1000e18 }(address(paymaster)); + entryPoint.depositTo{ value: 1000e18 }(address(account)); + testCreation(); + testTransferNative(); + testTransferERC20(); + if (writeGasProfile) { + string memory res = vm.serializeUint(jsonObj, "sum", sum); + console.log(res); + vm.writeJson(res, string.concat("./results/", scenarioName, "_", name, ".json")); + } + } + + function emptyPaymasterAndData(PackedUserOperation memory _op) internal pure returns (bytes memory ret) {} + + function validatePaymasterAndData(PackedUserOperation memory _op) internal view returns (bytes memory ret) { + bytes32 hash = paymaster.getHash(_op, 0, 0); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(verifierKey, ECDSA.toEthSignedMessageHash(hash)); + ret = abi.encodePacked(address(paymaster), uint256(0), uint256(0), r, s, uint8(v)); + } + + function getDummyPaymasterAndData(PackedUserOperation memory _op) internal view returns (bytes memory ret) { + ret = abi.encodePacked( + address(paymaster), + uint256(0), + uint256(0), + hex"fffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c" + ); + } + + function pack(PackedUserOperation memory _op) internal pure returns (bytes memory) { + bytes memory packed = abi.encode( + _op.sender, + _op.nonce, + _op.initCode, + _op.callData, + _op.accountGasLimits, + _op.preVerificationGas, + _op.gasFees, + _op.paymasterAndData, + _op.signature + ); + return packed; + } + + function calldataCost(bytes memory packed) internal view returns (uint256) { + uint256 cost = 0; + for (uint256 i = 0; i < packed.length; i++) { + if (packed[i] == 0) { + cost += OV_PER_ZERO_BYTE; + } else { + cost += OV_PER_NONZERO_BYTE; + } + } + return cost; + } + + // NOTE: this can vary depending on the bundler, this equation is referencing eth-infinitism bundler's pvg calculation + function calculatePreVerificationGas(PackedUserOperation memory _op) internal view returns (uint256) { + bytes memory packed = pack(_op); + uint256 calculated = OV_FIXED + OV_PER_USEROP + (OV_PER_WORD * (packed.length + 31)) / 32; + calculated += calldataCost(packed); + return calculated; + } + + function createAccount(address _owner) internal virtual; + + function getSignature(PackedUserOperation memory _op) internal view virtual returns (bytes memory); + + function getDummySig(PackedUserOperation memory _op) internal pure virtual returns (bytes memory); + + function fillData(address _to, uint256 _amount, bytes memory _data) internal view virtual returns (bytes memory); + + function getAccountAddr(address _owner) internal view virtual returns (IAccount _account); + + function getInitCode(address _owner) internal view virtual returns (bytes memory); +} diff --git a/src/test/smart-wallet/utils/BasePaymaster.sol b/src/test/smart-wallet/utils/BasePaymaster.sol new file mode 100644 index 000000000..3a14aae76 --- /dev/null +++ b/src/test/smart-wallet/utils/BasePaymaster.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +/* solhint-disable reason-string */ + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import "contracts/prebuilts/account/interfaces/IPaymaster.sol"; +import "contracts/prebuilts/account/interfaces/IEntryPoint.sol"; +import "contracts/prebuilts/account/utils/UserOperationLib.sol"; +/** + * Helper class for creating a paymaster. + * provides helper methods for staking. + * Validates that the postOp is called only by the entryPoint. + */ +abstract contract BasePaymaster is IPaymaster, Ownable { + IEntryPoint public immutable entryPoint; + + uint256 internal constant PAYMASTER_VALIDATION_GAS_OFFSET = UserOperationLib.PAYMASTER_VALIDATION_GAS_OFFSET; + uint256 internal constant PAYMASTER_POSTOP_GAS_OFFSET = UserOperationLib.PAYMASTER_POSTOP_GAS_OFFSET; + uint256 internal constant PAYMASTER_DATA_OFFSET = UserOperationLib.PAYMASTER_DATA_OFFSET; + + constructor(IEntryPoint _entryPoint) { + _transferOwnership(msg.sender); + _validateEntryPointInterface(_entryPoint); + entryPoint = _entryPoint; + } + + //sanity check: make sure this EntryPoint was compiled against the same + // IEntryPoint of this paymaster + function _validateEntryPointInterface(IEntryPoint _entryPoint) internal virtual { + require( + IERC165(address(_entryPoint)).supportsInterface(type(IEntryPoint).interfaceId), + "IEntryPoint interface mismatch" + ); + } + + /// @inheritdoc IPaymaster + function validatePaymasterUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 maxCost + ) external override returns (bytes memory context, uint256 validationData) { + _requireFromEntryPoint(); + return _validatePaymasterUserOp(userOp, userOpHash, maxCost); + } + + /** + * Validate a user operation. + * @param userOp - The user operation. + * @param userOpHash - The hash of the user operation. + * @param maxCost - The maximum cost of the user operation. + */ + function _validatePaymasterUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 maxCost + ) internal virtual returns (bytes memory context, uint256 validationData); + + /// @inheritdoc IPaymaster + function postOp( + PostOpMode mode, + bytes calldata context, + uint256 actualGasCost, + uint256 actualUserOpFeePerGas + ) external override { + _requireFromEntryPoint(); + _postOp(mode, context, actualGasCost, actualUserOpFeePerGas); + } + + /** + * Post-operation handler. + * (verified to be called only through the entryPoint) + * @dev If subclass returns a non-empty context from validatePaymasterUserOp, + * it must also implement this method. + * @param mode - Enum with the following options: + * opSucceeded - User operation succeeded. + * opReverted - User op reverted. The paymaster still has to pay for gas. + * postOpReverted - never passed in a call to postOp(). + * @param context - The context value returned by validatePaymasterUserOp + * @param actualGasCost - Actual gas used so far (without this postOp call). + * @param actualUserOpFeePerGas - the gas price this UserOp pays. This value is based on the UserOp's maxFeePerGas + * and maxPriorityFee (and basefee) + * It is not the same as tx.gasprice, which is what the bundler pays. + */ + function _postOp( + PostOpMode mode, + bytes calldata context, + uint256 actualGasCost, + uint256 actualUserOpFeePerGas + ) internal virtual { + (mode, context, actualGasCost, actualUserOpFeePerGas); // unused params + // subclass must override this method if validatePaymasterUserOp returns a context + revert("must override"); + } + + /** + * Add a deposit for this paymaster, used for paying for transaction fees. + */ + function deposit() public payable { + entryPoint.depositTo{ value: msg.value }(address(this)); + } + + /** + * Withdraw value from the deposit. + * @param withdrawAddress - Target to send to. + * @param amount - Amount to withdraw. + */ + function withdrawTo(address payable withdrawAddress, uint256 amount) public onlyOwner { + entryPoint.withdrawTo(withdrawAddress, amount); + } + + /** + * Add stake for this paymaster. + * This method can also carry eth value to add to the current stake. + * @param unstakeDelaySec - The unstake delay for this paymaster. Can only be increased. + */ + function addStake(uint32 unstakeDelaySec) external payable onlyOwner { + entryPoint.addStake{ value: msg.value }(unstakeDelaySec); + } + + /** + * Return current paymaster's deposit on the entryPoint. + */ + function getDeposit() public view returns (uint256) { + return entryPoint.balanceOf(address(this)); + } + + /** + * Unlock the stake, in order to withdraw it. + * The paymaster can't serve requests once unlocked, until it calls addStake again + */ + function unlockStake() external onlyOwner { + entryPoint.unlockStake(); + } + + /** + * Withdraw the entire paymaster's stake. + * stake must be unlocked first (and then wait for the unstakeDelay to be over) + * @param withdrawAddress - The address to send withdrawn value. + */ + function withdrawStake(address payable withdrawAddress) external onlyOwner { + entryPoint.withdrawStake(withdrawAddress); + } + + /** + * Validate the call is made from a valid entrypoint + */ + function _requireFromEntryPoint() internal virtual { + require(msg.sender == address(entryPoint), "Sender not EntryPoint"); + } +} diff --git a/src/test/smart-wallet/utils/MessageHashUtils.sol b/src/test/smart-wallet/utils/MessageHashUtils.sol new file mode 100644 index 000000000..06c61b8f7 --- /dev/null +++ b/src/test/smart-wallet/utils/MessageHashUtils.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/MessageHashUtils.sol) + +pragma solidity ^0.8.20; + +import { Strings } from "contracts/lib/Strings.sol"; + +/** + * @dev Signature message hash utilities for producing digests to be consumed by {ECDSA} recovery or signing. + * + * The library provides methods for generating a hash of a message that conforms to the + * https://eips.ethereum.org/EIPS/eip-191[ERC-191] and https://eips.ethereum.org/EIPS/eip-712[EIP 712] + * specifications. + */ +library MessageHashUtils { + /** + * @dev Returns the keccak256 digest of an ERC-191 signed data with version + * `0x45` (`personal_sign` messages). + * + * The digest is calculated by prefixing a bytes32 `messageHash` with + * `"\x19Ethereum Signed Message:\n32"` and hashing the result. It corresponds with the + * hash signed when using the https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] JSON-RPC method. + * + * NOTE: The `messageHash` parameter is intended to be the result of hashing a raw message with + * keccak256, although any bytes32 value can be safely used because the final digest will + * be re-hashed. + * + * See {ECDSA-recover}. + */ + function toEthSignedMessageHash(bytes32 messageHash) internal pure returns (bytes32 digest) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, "\x19Ethereum Signed Message:\n32") // 32 is the bytes-length of messageHash + mstore(0x1c, messageHash) // 0x1c (28) is the length of the prefix + digest := keccak256(0x00, 0x3c) // 0x3c is the length of the prefix (0x1c) + messageHash (0x20) + } + } + + /** + * @dev Returns the keccak256 digest of an ERC-191 signed data with version + * `0x45` (`personal_sign` messages). + * + * The digest is calculated by prefixing an arbitrary `message` with + * `"\x19Ethereum Signed Message:\n" + len(message)` and hashing the result. It corresponds with the + * hash signed when using the https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] JSON-RPC method. + * + * See {ECDSA-recover}. + */ + function toEthSignedMessageHash(bytes memory message) internal pure returns (bytes32) { + return + keccak256(bytes.concat("\x19Ethereum Signed Message:\n", bytes(Strings.toString(message.length)), message)); + } + + /** + * @dev Returns the keccak256 digest of an ERC-191 signed data with version + * `0x00` (data with intended validator). + * + * The digest is calculated by prefixing an arbitrary `data` with `"\x19\x00"` and the intended + * `validator` address. Then hashing the result. + * + * See {ECDSA-recover}. + */ + function toDataWithIntendedValidatorHash(address validator, bytes memory data) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(hex"19_00", validator, data)); + } + + /** + * @dev Returns the keccak256 digest of an EIP-712 typed data (ERC-191 version `0x01`). + * + * The digest is calculated from a `domainSeparator` and a `structHash`, by prefixing them with + * `\x19\x01` and hashing the result. It corresponds to the hash signed by the + * https://eips.ethereum.org/EIPS/eip-712[`eth_signTypedData`] JSON-RPC method as part of EIP-712. + * + * See {ECDSA-recover}. + */ + function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32 digest) { + /// @solidity memory-safe-assembly + assembly { + let ptr := mload(0x40) + mstore(ptr, hex"19_01") + mstore(add(ptr, 0x02), domainSeparator) + mstore(add(ptr, 0x22), structHash) + digest := keccak256(ptr, 0x42) + } + } +} diff --git a/src/test/smart-wallet/utils/VerifyingPaymaster.sol b/src/test/smart-wallet/utils/VerifyingPaymaster.sol new file mode 100644 index 000000000..82da19733 --- /dev/null +++ b/src/test/smart-wallet/utils/VerifyingPaymaster.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +/* solhint-disable reason-string */ +/* solhint-disable no-inline-assembly */ + +import "./BasePaymaster.sol"; +import "contracts/prebuilts/account/utils/UserOperationLib.sol"; +import "contracts/prebuilts/account/utils/Helpers.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { MessageHashUtils } from "./MessageHashUtils.sol"; + +/** + * A sample paymaster that uses external service to decide whether to pay for the UserOp. + * The paymaster trusts an external signer to sign the transaction. + * The calling user must pass the UserOp to that external signer first, which performs + * whatever off-chain verification before signing the UserOp. + * Note that this signature is NOT a replacement for the account-specific signature: + * - the paymaster checks a signature to agree to PAY for GAS. + * - the account checks a signature to prove identity and account ownership. + */ +contract VerifyingPaymaster is BasePaymaster { + using UserOperationLib for PackedUserOperation; + + address public immutable verifyingSigner; + + uint256 private constant VALID_TIMESTAMP_OFFSET = PAYMASTER_DATA_OFFSET; + + uint256 private constant SIGNATURE_OFFSET = VALID_TIMESTAMP_OFFSET + 64; + + constructor(IEntryPoint _entryPoint, address _verifyingSigner) BasePaymaster(_entryPoint) { + verifyingSigner = _verifyingSigner; + } + + /** + * return the hash we're going to sign off-chain (and validate on-chain) + * this method is called by the off-chain service, to sign the request. + * it is called on-chain from the validatePaymasterUserOp, to validate the signature. + * note that this signature covers all fields of the UserOperation, except the "paymasterAndData", + * which will carry the signature itself. + */ + function getHash( + PackedUserOperation calldata userOp, + uint48 validUntil, + uint48 validAfter + ) public view returns (bytes32) { + //can't use userOp.hash(), since it contains also the paymasterAndData itself. + address sender = userOp.getSender(); + return + keccak256( + abi.encode( + sender, + userOp.nonce, + keccak256(userOp.initCode), + keccak256(userOp.callData), + userOp.accountGasLimits, + uint256(bytes32(userOp.paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET:PAYMASTER_DATA_OFFSET])), + userOp.preVerificationGas, + userOp.gasFees, + block.chainid, + address(this), + validUntil, + validAfter + ) + ); + } + + /** + * verify our external signer signed this request. + * the "paymasterAndData" is expected to be the paymaster and a signature over the entire request params + * paymasterAndData[:20] : address(this) + * paymasterAndData[20:84] : abi.encode(validUntil, validAfter) + * paymasterAndData[84:] : signature + */ + function _validatePaymasterUserOp( + PackedUserOperation calldata userOp, + bytes32 /*userOpHash*/, + uint256 requiredPreFund + ) internal view override returns (bytes memory context, uint256 validationData) { + (requiredPreFund); + + (uint48 validUntil, uint48 validAfter, bytes calldata signature) = parsePaymasterAndData( + userOp.paymasterAndData + ); + + //ECDSA library supports both 64 and 65-byte long signatures. + // we only "require" it here so that the revert reason on invalid signature will be of "VerifyingPaymaster", and not "ECDSA" + require( + signature.length == 64 || signature.length == 65, + "VerifyingPaymaster: invalid signature length in paymasterAndData" + ); + + bytes32 hash = MessageHashUtils.toEthSignedMessageHash(getHash(userOp, validUntil, validAfter)); + + //don't revert on signature failure: return SIG_VALIDATION_FAILED + if (verifyingSigner != ECDSA.recover(hash, signature)) { + return ("", _packValidationData(true, validUntil, validAfter)); + } + + //no need for other on-chain validation: entire UserOp should have been checked + // by the external service prior to signing it. + return ("", _packValidationData(false, validUntil, validAfter)); + } + + function parsePaymasterAndData( + bytes calldata paymasterAndData + ) public view returns (uint48 validUntil, uint48 validAfter, bytes calldata signature) { + (validUntil, validAfter) = abi.decode(paymasterAndData[VALID_TIMESTAMP_OFFSET:], (uint48, uint48)); + signature = paymasterAndData[SIGNATURE_OFFSET:]; + } +} diff --git a/src/test/split-BTT/distribute-erc20/distribute.t.sol b/src/test/split-BTT/distribute-erc20/distribute.t.sol new file mode 100644 index 000000000..4c2d9c139 --- /dev/null +++ b/src/test/split-BTT/distribute-erc20/distribute.t.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MySplit is Split {} + +contract SplitTest_DistributeERC20 is BaseTest { + address payable public implementation; + address payable public proxy; + + address[] public payees; + uint256[] public shares; + + address internal caller; + string internal _contractURI; + + MySplit internal splitContract; + + event PaymentReleased(address to, uint256 amount); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = payable(address(new MySplit())); + + // create 5 payees and shares + for (uint160 i = 0; i < 5; i++) { + payees.push(getActor(i + 100)); + shares.push(i + 100); + } + + caller = getActor(1); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = payable( + address( + new TWProxy( + implementation, + abi.encodeCall(Split.initialize, (deployer, CONTRACT_URI, forwarders(), payees, shares)) + ) + ) + ); + + splitContract = MySplit(proxy); + _contractURI = "ipfs://contracturi"; + + erc20.mint(address(splitContract), 100 ether); + } + + function test_distribute() public { + uint256[] memory pendingAmounts = new uint256[](payees.length); + + // get pending payments + for (uint256 i = 0; i < 5; i++) { + pendingAmounts[i] = splitContract.releasable(IERC20Upgradeable(address(erc20)), payees[i]); + } + + // distribute + splitContract.distribute(IERC20Upgradeable(address(erc20))); + + uint256 totalPaid; + for (uint256 i = 0; i < 5; i++) { + totalPaid += pendingAmounts[i]; + + assertEq(splitContract.released(IERC20Upgradeable(address(erc20)), payees[i]), pendingAmounts[i]); + assertEq(erc20.balanceOf(payees[i]), pendingAmounts[i]); + } + assertEq(splitContract.totalReleased(IERC20Upgradeable(address(erc20))), totalPaid); + + assertEq(erc20.balanceOf(address(splitContract)), 100 ether - totalPaid); + } +} diff --git a/src/test/split-BTT/distribute-erc20/distribute.tree b/src/test/split-BTT/distribute-erc20/distribute.tree new file mode 100644 index 000000000..320921cca --- /dev/null +++ b/src/test/split-BTT/distribute-erc20/distribute.tree @@ -0,0 +1,6 @@ +distribute() +├── it should update released mapping for all payees account by respective pending payments ✅ +├── it should update total released by total pending payments ✅ +├── it should send correct pending payment amounts of erc20 tokens to each account ✅ +├── it should reduce balance of contract by total paid in this call ✅ + \ No newline at end of file diff --git a/src/test/split-BTT/distribute-native-token/distribute.t.sol b/src/test/split-BTT/distribute-native-token/distribute.t.sol new file mode 100644 index 000000000..fd524c40d --- /dev/null +++ b/src/test/split-BTT/distribute-native-token/distribute.t.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MySplit is Split {} + +contract SplitTest_DistributeNativeToken is BaseTest { + address payable public implementation; + address payable public proxy; + + address[] public payees; + uint256[] public shares; + + address internal caller; + string internal _contractURI; + + MySplit internal splitContract; + + event PaymentReleased(address to, uint256 amount); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = payable(address(new MySplit())); + + // create 5 payees and shares + for (uint160 i = 0; i < 5; i++) { + payees.push(getActor(i + 100)); + shares.push(i + 100); + } + + caller = getActor(1); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = payable( + address( + new TWProxy( + implementation, + abi.encodeCall(Split.initialize, (deployer, CONTRACT_URI, forwarders(), payees, shares)) + ) + ) + ); + + splitContract = MySplit(proxy); + _contractURI = "ipfs://contracturi"; + + vm.deal(address(splitContract), 100 ether); + } + + function test_distribute() public { + uint256[] memory pendingAmounts = new uint256[](payees.length); + + // get pending payments + for (uint256 i = 0; i < 5; i++) { + pendingAmounts[i] = splitContract.releasable(payees[i]); + } + + // distribute + splitContract.distribute(); + + uint256 totalPaid; + for (uint256 i = 0; i < 5; i++) { + totalPaid += pendingAmounts[i]; + + assertEq(splitContract.released(payees[i]), pendingAmounts[i]); + assertEq(payees[i].balance, pendingAmounts[i]); + } + assertEq(splitContract.totalReleased(), totalPaid); + + assertEq(address(splitContract).balance, 100 ether - totalPaid); + } +} diff --git a/src/test/split-BTT/distribute-native-token/distribute.tree b/src/test/split-BTT/distribute-native-token/distribute.tree new file mode 100644 index 000000000..d75f4cc53 --- /dev/null +++ b/src/test/split-BTT/distribute-native-token/distribute.tree @@ -0,0 +1,6 @@ +distribute() +├── it should update released mapping for all payees account by respective pending payments ✅ +├── it should update total released by total pending payments ✅ +├── it should send correct pending payment amounts of native tokens to each account ✅ +├── it should reduce balance of contract by total paid in this call ✅ + \ No newline at end of file diff --git a/src/test/split-BTT/initialize/initialize.t.sol b/src/test/split-BTT/initialize/initialize.t.sol new file mode 100644 index 000000000..bc75ae266 --- /dev/null +++ b/src/test/split-BTT/initialize/initialize.t.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MySplit is Split {} + +contract SplitTest_Initialize is BaseTest { + address payable public implementation; + address payable public proxy; + + address[] public payees; + uint256[] public shares; + + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + + function setUp() public override { + super.setUp(); + + // create 5 payees and shares + for (uint160 i = 0; i < 5; i++) { + payees.push(getActor(i + 100)); + shares.push(i + 100); + } + + // Deploy implementation. + implementation = payable(address(new MySplit())); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = payable( + address( + new TWProxy( + implementation, + abi.encodeCall(Split.initialize, (deployer, CONTRACT_URI, forwarders(), payees, shares)) + ) + ) + ); + } + + function test_initialize_initializingImplementation() public { + vm.expectRevert("Initializable: contract is already initialized"); + Split(implementation).initialize(deployer, CONTRACT_URI, forwarders(), payees, shares); + } + + modifier whenNotImplementation() { + _; + } + + function test_initialize_proxyAlreadyInitialized() public whenNotImplementation { + vm.expectRevert("Initializable: contract is already initialized"); + MySplit(proxy).initialize(deployer, CONTRACT_URI, forwarders(), payees, shares); + } + + modifier whenProxyNotInitialized() { + proxy = payable(address(new TWProxy(implementation, ""))); + _; + } + + function test_initialize_payeeLengthZero() public whenNotImplementation whenProxyNotInitialized { + address[] memory _payees; + uint256[] memory _shares; + vm.expectRevert("PaymentSplitter: no payees"); + MySplit(proxy).initialize(deployer, CONTRACT_URI, forwarders(), _payees, _shares); + } + + modifier whenPayeeLengthNotZero() { + _; + } + + function test_initialize_payeesSharesUnequalLength() + public + whenNotImplementation + whenProxyNotInitialized + whenPayeeLengthNotZero + { + uint256[] memory _shares; + vm.expectRevert("PaymentSplitter: payees and shares length mismatch"); + MySplit(proxy).initialize(deployer, CONTRACT_URI, forwarders(), payees, _shares); + } + + modifier whenEqualLengths() { + _; + } + + function test_initialize() + public + whenNotImplementation + whenProxyNotInitialized + whenPayeeLengthNotZero + whenEqualLengths + { + MySplit(proxy).initialize(deployer, CONTRACT_URI, forwarders(), payees, shares); + + // check state + MySplit splitContract = MySplit(proxy); + + address[] memory _trustedForwarders = forwarders(); + for (uint256 i = 0; i < _trustedForwarders.length; i++) { + assertTrue(splitContract.isTrustedForwarder(_trustedForwarders[i])); + } + + uint256 totalShares; + for (uint160 i = 0; i < 5; i++) { + uint256 _shares = splitContract.shares(payees[i]); + assertEq(_shares, shares[i]); + + totalShares += _shares; + } + assertEq(totalShares, splitContract.totalShares()); + assertEq(splitContract.payeeCount(), payees.length); + assertEq(splitContract.contractURI(), CONTRACT_URI); + assertTrue(splitContract.hasRole(bytes32(0x00), deployer)); + } + + function test_initialize_event_RoleGranted_DefaultAdmin() + public + whenNotImplementation + whenProxyNotInitialized + whenPayeeLengthNotZero + whenEqualLengths + { + bytes32 _defaultAdminRole = bytes32(0x00); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_defaultAdminRole, deployer, deployer); + MySplit(proxy).initialize(deployer, CONTRACT_URI, forwarders(), payees, shares); + } +} diff --git a/src/test/split-BTT/initialize/initialize.tree b/src/test/split-BTT/initialize/initialize.tree new file mode 100644 index 000000000..4647d7f70 --- /dev/null +++ b/src/test/split-BTT/initialize/initialize.tree @@ -0,0 +1,25 @@ +initialize( + address _defaultAdmin, + string memory _contractURI, + address[] memory _trustedForwarders, + address[] memory _payees, + uint256[] memory _shares +) +├── when initializing the implementation contract (not proxy) +│ └── it should revert ✅ +└── when it is a proxy to the implementation + └── when it is already initialized + │ └── it should revert ✅ + └── when it is not initialized + └── when `_payees` length is zero + │ └── it should revert ✅ + └── `_payees` length is not zero + └── when `_payees` length not equal to `_shares` length + │ └── it should revert ✅ + └── when `_payees` length equal to `_shares` length + └── it should set trustedForwarder mapping to true for all addresses in `_trustedForwarders` ✅ + └── it should correctly save `_payees` and `_shares` in state ✅ + └── it should set contractURI to `_contractURI` param value ✅ + └── it should grant 0x00 (DEFAULT_ADMIN_ROLE) to `_defaultAdmin` address ✅ + └── it should emit RoleGranted event ✅ + diff --git a/src/test/split-BTT/other-functions/other.t.sol b/src/test/split-BTT/other-functions/other.t.sol new file mode 100644 index 000000000..2bf87b871 --- /dev/null +++ b/src/test/split-BTT/other-functions/other.t.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MySplit is Split {} + +contract SplitTest_OtherFunctions is BaseTest { + address payable public implementation; + address payable public proxy; + + address[] public payees; + uint256[] public shares; + + address internal caller; + string internal _contractURI; + + MySplit internal splitContract; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = payable(address(new MySplit())); + + // create 5 payees and shares + for (uint160 i = 0; i < 5; i++) { + payees.push(getActor(i + 100)); + shares.push(i + 100); + } + + caller = getActor(1); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = payable( + address( + new TWProxy( + implementation, + abi.encodeCall(Split.initialize, (deployer, CONTRACT_URI, forwarders(), payees, shares)) + ) + ) + ); + + splitContract = MySplit(proxy); + _contractURI = "ipfs://contracturi"; + } + + function test_contractType() public { + assertEq(splitContract.contractType(), bytes32("Split")); + } + + function test_contractVersion() public { + assertEq(splitContract.contractVersion(), uint8(1)); + } +} diff --git a/src/test/split-BTT/other-functions/other.tree b/src/test/split-BTT/other-functions/other.tree new file mode 100644 index 000000000..dd1798caa --- /dev/null +++ b/src/test/split-BTT/other-functions/other.tree @@ -0,0 +1,5 @@ +contractType() +├── it should return bytes32("TokenERC721") ✅ + +contractVersion() +├── it should return uint8(1) ✅ diff --git a/src/test/split-BTT/release-erc20/release.t.sol b/src/test/split-BTT/release-erc20/release.t.sol new file mode 100644 index 000000000..c5499f664 --- /dev/null +++ b/src/test/split-BTT/release-erc20/release.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MySplit is Split {} + +contract SplitTest_ReleaseERC20 is BaseTest { + address payable public implementation; + address payable public proxy; + + address[] public payees; + uint256[] public shares; + + address internal caller; + string internal _contractURI; + + MySplit internal splitContract; + + event ERC20PaymentReleased(IERC20Upgradeable indexed token, address to, uint256 amount); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = payable(address(new MySplit())); + + // create 5 payees and shares + for (uint160 i = 0; i < 5; i++) { + payees.push(getActor(i + 100)); + shares.push(i + 100); + } + + caller = getActor(1); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = payable( + address( + new TWProxy( + implementation, + abi.encodeCall(Split.initialize, (deployer, CONTRACT_URI, forwarders(), payees, shares)) + ) + ) + ); + + splitContract = MySplit(proxy); + _contractURI = "ipfs://contracturi"; + } + + function test_release_zeroShares() public { + vm.expectRevert("PaymentSplitter: account has no shares"); + splitContract.release(IERC20Upgradeable(address(erc20)), payable(address(0x123))); // arbitrary address + } + + modifier whenNonZeroShares() { + _; + } + + function test_release_pendingPaymentZero() public { + vm.expectRevert("PaymentSplitter: account is not due payment"); + splitContract.release(IERC20Upgradeable(address(erc20)), payable(payees[1])); + } + + modifier whenPendingPaymentNonZero() { + erc20.mint(address(splitContract), 100 ether); + _; + } + + function test_release() public whenPendingPaymentNonZero { + address _payeeOne = payees[1]; // select a payee from the array + uint256 pendingPayment = splitContract.releasable(IERC20Upgradeable(address(erc20)), _payeeOne); + + splitContract.release(IERC20Upgradeable(address(erc20)), payable(_payeeOne)); + + uint256 totalReleased = splitContract.totalReleased(IERC20Upgradeable(address(erc20))); + assertEq(splitContract.released(IERC20Upgradeable(address(erc20)), _payeeOne), pendingPayment); + assertEq(totalReleased, pendingPayment); + assertEq(erc20.balanceOf(_payeeOne), pendingPayment); + + // check for another payee + address _payeeThree = payees[3]; + pendingPayment = splitContract.releasable(IERC20Upgradeable(address(erc20)), _payeeThree); + + splitContract.release(IERC20Upgradeable(address(erc20)), payable(_payeeThree)); + + assertEq(splitContract.released(IERC20Upgradeable(address(erc20)), _payeeThree), pendingPayment); + assertEq(splitContract.totalReleased(IERC20Upgradeable(address(erc20))), totalReleased + pendingPayment); + assertEq(erc20.balanceOf(_payeeThree), pendingPayment); + + assertEq( + erc20.balanceOf(address(splitContract)), + 100 ether - erc20.balanceOf(_payeeOne) - erc20.balanceOf(_payeeThree) + ); + } + + function test_release_event_PaymentReleased() public whenPendingPaymentNonZero { + address _payeeOne = payees[1]; // select a payee from the array + uint256 pendingPayment = splitContract.releasable(IERC20Upgradeable(address(erc20)), _payeeOne); + + vm.expectEmit(true, false, false, true); + emit ERC20PaymentReleased(IERC20Upgradeable(address(erc20)), _payeeOne, pendingPayment); + splitContract.release(IERC20Upgradeable(address(erc20)), payable(_payeeOne)); + } +} diff --git a/src/test/split-BTT/release-erc20/release.tree b/src/test/split-BTT/release-erc20/release.tree new file mode 100644 index 000000000..217ea3b6c --- /dev/null +++ b/src/test/split-BTT/release-erc20/release.tree @@ -0,0 +1,12 @@ +release(address payable account) +├── when account has zero shares + │ └── it should revert ✅ + └── when account has non-zero shares + └── when pending payment is zero + │ └── it should revert ✅ + └── when pending payment is not zero + └── it should update released mapping for the account by pending payment ✅ + └── it should update total released by pending payment ✅ + └── it should send pending payment amount of erc20 token to account ✅ + └── it should emit ERC20PaymentReleased event ✅ + \ No newline at end of file diff --git a/src/test/split-BTT/release-native-token/release.t.sol b/src/test/split-BTT/release-native-token/release.t.sol new file mode 100644 index 000000000..18210e0da --- /dev/null +++ b/src/test/split-BTT/release-native-token/release.t.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MySplit is Split {} + +contract SplitTest_ReleaseNativeToken is BaseTest { + address payable public implementation; + address payable public proxy; + + address[] public payees; + uint256[] public shares; + + address internal caller; + string internal _contractURI; + + MySplit internal splitContract; + + event PaymentReleased(address to, uint256 amount); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = payable(address(new MySplit())); + + // create 5 payees and shares + for (uint160 i = 0; i < 5; i++) { + payees.push(getActor(i + 100)); + shares.push(i + 100); + } + + caller = getActor(1); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = payable( + address( + new TWProxy( + implementation, + abi.encodeCall(Split.initialize, (deployer, CONTRACT_URI, forwarders(), payees, shares)) + ) + ) + ); + + splitContract = MySplit(proxy); + _contractURI = "ipfs://contracturi"; + } + + function test_release_zeroShares() public { + vm.expectRevert("PaymentSplitter: account has no shares"); + splitContract.release(payable(address(0x123))); // arbitrary address + } + + modifier whenNonZeroShares() { + _; + } + + function test_release_pendingPaymentZero() public { + vm.expectRevert("PaymentSplitter: account is not due payment"); + splitContract.release(payable(payees[1])); + } + + modifier whenPendingPaymentNonZero() { + vm.deal(address(splitContract), 100 ether); + _; + } + + function test_release() public whenPendingPaymentNonZero { + address _payeeOne = payees[1]; // select a payee from the array + uint256 pendingPayment = splitContract.releasable(_payeeOne); + + splitContract.release(payable(_payeeOne)); + + uint256 totalReleased = splitContract.totalReleased(); + assertEq(splitContract.released(_payeeOne), pendingPayment); + assertEq(totalReleased, pendingPayment); + assertEq(_payeeOne.balance, pendingPayment); + + // check for another payee + address _payeeThree = payees[3]; + pendingPayment = splitContract.releasable(_payeeThree); + + splitContract.release(payable(_payeeThree)); + + assertEq(splitContract.released(_payeeThree), pendingPayment); + assertEq(splitContract.totalReleased(), totalReleased + pendingPayment); + assertEq(_payeeThree.balance, pendingPayment); + + assertEq(address(splitContract).balance, 100 ether - _payeeOne.balance - _payeeThree.balance); + } + + function test_release_event_PaymentReleased() public whenPendingPaymentNonZero { + address _payeeOne = payees[1]; // select a payee from the array + uint256 pendingPayment = splitContract.releasable(_payeeOne); + + vm.expectEmit(false, false, false, true); + emit PaymentReleased(_payeeOne, pendingPayment); + splitContract.release(payable(_payeeOne)); + } +} diff --git a/src/test/split-BTT/release-native-token/release.tree b/src/test/split-BTT/release-native-token/release.tree new file mode 100644 index 000000000..afa44e86d --- /dev/null +++ b/src/test/split-BTT/release-native-token/release.tree @@ -0,0 +1,12 @@ +release(address payable account) +├── when account has zero shares + │ └── it should revert ✅ + └── when account has non-zero shares + └── when pending payment is zero + │ └── it should revert ✅ + └── when pending payment is not zero + └── it should update released mapping for the account by pending payment ✅ + └── it should update total released by pending payment ✅ + └── it should send pending payment amount of native tokens to account ✅ + └── it should emit PaymentReleased event ✅ + \ No newline at end of file diff --git a/src/test/split-BTT/set-contract-uri/setContractURI.t.sol b/src/test/split-BTT/set-contract-uri/setContractURI.t.sol new file mode 100644 index 000000000..af3cc85d1 --- /dev/null +++ b/src/test/split-BTT/set-contract-uri/setContractURI.t.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MySplit is Split {} + +contract SplitTest_SetContractURI is BaseTest { + address payable public implementation; + address payable public proxy; + + address[] public payees; + uint256[] public shares; + + address internal caller; + string internal _contractURI; + + MySplit internal splitContract; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = payable(address(new MySplit())); + + // create 5 payees and shares + for (uint160 i = 0; i < 5; i++) { + payees.push(getActor(i + 100)); + shares.push(i + 100); + } + + caller = getActor(1); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = payable( + address( + new TWProxy( + implementation, + abi.encodeCall(Split.initialize, (deployer, CONTRACT_URI, forwarders(), payees, shares)) + ) + ) + ); + + splitContract = MySplit(proxy); + _contractURI = "ipfs://contracturi"; + } + + function test_setContractURI_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(caller), 20), + " is missing role ", + Strings.toHexString(uint256(0), 32) + ) + ); + splitContract.setContractURI(_contractURI); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + splitContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setContractURI_empty() public whenCallerAuthorized { + vm.prank(address(caller)); + splitContract.setContractURI(""); + + // get contract uri + assertEq(splitContract.contractURI(), ""); + } + + function test_setContractURI_notEmpty() public whenCallerAuthorized { + vm.prank(address(caller)); + splitContract.setContractURI(_contractURI); + + // get contract uri + assertEq(splitContract.contractURI(), _contractURI); + } +} diff --git a/src/test/split-BTT/set-contract-uri/setContractURI.tree b/src/test/split-BTT/set-contract-uri/setContractURI.tree new file mode 100644 index 000000000..8fc480b19 --- /dev/null +++ b/src/test/split-BTT/set-contract-uri/setContractURI.tree @@ -0,0 +1,8 @@ +setContractURI(string calldata _uri) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── when `uri` is empty + │ └── it should update contract URI to empty string ✅ + └── when `uri` is not empty + └── it should update contract URI to `_uri` ✅ \ No newline at end of file diff --git a/src/test/staking/EditionStake.t.sol b/src/test/staking/EditionStake.t.sol new file mode 100644 index 000000000..0df3b9e91 --- /dev/null +++ b/src/test/staking/EditionStake.t.sol @@ -0,0 +1,1153 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { EditionStake } from "contracts/prebuilts/staking/EditionStake.sol"; + +// Test imports + +import "../utils/BaseTest.sol"; + +contract EditionStakeTest is BaseTest { + EditionStake internal stakeContract; + + address internal stakerOne; + address internal stakerTwo; + + uint256 internal defaultTimeUnit; + uint256 internal defaultRewardsPerUnitTime; + + function setUp() public override { + super.setUp(); + + defaultTimeUnit = 60; + defaultRewardsPerUnitTime = 1; + + stakerOne = address(0x345); + stakerTwo = address(0x567); + + erc1155.mint(stakerOne, 0, 100); // mint 100 tokens with id 0 to stakerOne + erc1155.mint(stakerOne, 1, 100); // mint 100 tokens with id 1 to stakerOne + + erc1155.mint(stakerTwo, 0, 100); // mint 100 tokens with id 0 to stakerTwo + erc1155.mint(stakerTwo, 1, 100); // mint 100 tokens with id 1 to stakerTwo + + erc20.mint(deployer, 1000 ether); // mint reward tokens to contract admin + + stakeContract = EditionStake(payable(getContract("EditionStake"))); + + // set approvals + vm.prank(stakerOne); + erc1155.setApprovalForAll(address(stakeContract), true); + + vm.prank(stakerTwo); + erc1155.setApprovalForAll(address(stakeContract), true); + + vm.startPrank(deployer); + erc20.approve(address(stakeContract), type(uint256).max); + stakeContract.depositRewardTokens(100 ether); + // erc20.transfer(address(stakeContract), 100 ether); + vm.stopPrank(); + assertEq(stakeContract.getRewardTokenBalance(), 100 ether); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: Stake + - with default time-unit and rewards + - different token-ids staked by stakers + //////////////////////////////////////////////////////////////*/ + + function test_state_stake_defaults_differentTokens() public { + //================ first staker ====================== + vm.warp(1); + + // stake 50 tokens with token-id 0 + vm.prank(stakerOne); + stakeContract.stake(0, 50); + uint256 timeOfLastUpdate_one = block.timestamp; + + // check balances/ownership of staked tokens + assertEq(erc1155.balanceOf(address(stakeContract), 0), 50); + assertEq(erc1155.balanceOf(address(stakerOne), 0), 50); + + // check available rewards right after staking + (uint256 _amountStaked, uint256 _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq(_amountStaked, 50); + assertEq(_availableRewards, 0); + + //=================== warp timestamp to calculate rewards + vm.roll(100); + vm.warp(1000); + + // check available rewards after warp + (, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate_one) * 50) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + //================ second staker ====================== + vm.roll(200); + vm.warp(2000); + + // stake 20 tokens with token-id 1 + vm.prank(stakerTwo); + stakeContract.stake(1, 20); + uint256 timeOfLastUpdate_two = block.timestamp; + + // check balances/ownership of staked tokens + assertEq(erc1155.balanceOf(address(stakeContract), 1), 20); + assertEq(erc1155.balanceOf(address(stakerTwo), 1), 80); + + // check available rewards right after staking + (_amountStaked, _availableRewards) = stakeContract.getStakeInfoForToken(1, stakerTwo); + + assertEq(_amountStaked, 20); + assertEq(_availableRewards, 0); + + //=================== warp timestamp to calculate rewards + vm.roll(300); + vm.warp(3000); + + // check available rewards for stakerOne + (, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate_one) * 50) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + // check available rewards for stakerTwo + (, _availableRewards) = stakeContract.getStakeInfoForToken(1, stakerTwo); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate_two) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + } + + function test_revert_stake_stakingZeroTokens() public { + // stake 0 tokens + + vm.prank(stakerOne); + vm.expectRevert("Staking 0 tokens"); + stakeContract.stake(0, 0); + } + + function test_revert_stake_notBalanceOrApproved() public { + // stake unowned tokens + vm.prank(stakerOne); + vm.expectRevert("ERC1155: insufficient balance for transfer"); + stakeContract.stake(2, 10); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: Stake + - with default time-unit and rewards + - same token-id staked by stakers + //////////////////////////////////////////////////////////////*/ + + function test_state_stake_defaults_sameToken() public { + //================ first staker ====================== + vm.warp(1); + + // stake 50 tokens with token-id 0 + vm.prank(stakerOne); + stakeContract.stake(0, 50); + uint256 timeOfLastUpdate_one = block.timestamp; + + // check balances/ownership of staked tokens + assertEq(erc1155.balanceOf(address(stakeContract), 0), 50); + assertEq(erc1155.balanceOf(address(stakerOne), 0), 50); + + // check available rewards right after staking + (uint256 _amountStaked, uint256 _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq(_amountStaked, 50); + assertEq(_availableRewards, 0); + + //=================== warp timestamp to calculate rewards + vm.roll(100); + vm.warp(1000); + + // check available rewards after warp + (, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate_one) * 50) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + //================ second staker ====================== + vm.roll(200); + vm.warp(2000); + + // stake 20 tokens with token-id 0 + vm.prank(stakerTwo); + stakeContract.stake(0, 20); + uint256 timeOfLastUpdate_two = block.timestamp; + + // check balances/ownership of staked tokens + assertEq(erc1155.balanceOf(address(stakeContract), 0), 20 + 50); // sum of staked tokens by both stakers + assertEq(erc1155.balanceOf(address(stakerTwo), 0), 80); + + // check available rewards right after staking + (_amountStaked, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerTwo); + + assertEq(_amountStaked, 20); + assertEq(_availableRewards, 0); + + //=================== warp timestamp to calculate rewards + vm.roll(300); + vm.warp(3000); + + // check available rewards for stakerOne + (, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate_one) * 50) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + // check available rewards for stakerTwo + (, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerTwo); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate_two) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: claimRewards + - default timeUnit and rewards + - different token-ids staked by stakers + //////////////////////////////////////////////////////////////*/ + + function test_state_claimRewards_defaults_differentTokens() public { + //================ setup stakerOne ====================== + vm.warp(1); + + // stake 50 tokens with token-id 0 + vm.prank(stakerOne); + stakeContract.stake(0, 50); + uint256 timeOfLastUpdate_one = block.timestamp; + + //=================== warp timestamp to claim rewards + vm.roll(100); + vm.warp(1000); + + uint256 rewardBalanceBefore = stakeContract.getRewardTokenBalance(); + vm.prank(stakerOne); + stakeContract.claimRewards(0); + uint256 rewardBalanceAfter = stakeContract.getRewardTokenBalance(); + + // check reward balances + assertEq( + erc20.balanceOf(stakerOne), + ((((block.timestamp - timeOfLastUpdate_one) * 50) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + assertEq( + rewardBalanceAfter, + rewardBalanceBefore - + ((((block.timestamp - timeOfLastUpdate_one) * 50) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + // check available rewards after claiming + (uint256 _amountStaked, uint256 _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq(_amountStaked, 50); + assertEq(_availableRewards, 0); + + //================ setup stakerTwo ====================== + + // stake 20 tokens with token-id 1 + vm.prank(stakerTwo); + stakeContract.stake(1, 20); + uint256 timeOfLastUpdate_two = block.timestamp; + + //=================== warp timestamp to claim rewards + vm.roll(200); + vm.warp(2000); + + rewardBalanceBefore = stakeContract.getRewardTokenBalance(); + vm.prank(stakerTwo); + stakeContract.claimRewards(1); + rewardBalanceAfter = stakeContract.getRewardTokenBalance(); + + // check reward balances + assertEq( + erc20.balanceOf(stakerTwo), + ((((block.timestamp - timeOfLastUpdate_two) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + assertEq( + rewardBalanceAfter, + rewardBalanceBefore - + ((((block.timestamp - timeOfLastUpdate_two) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + // check available rewards after claiming -- stakerTwo + (_amountStaked, _availableRewards) = stakeContract.getStakeInfoForToken(1, stakerTwo); + assertEq(_amountStaked, 20); + assertEq(_availableRewards, 0); + + // check available rewards -- stakerOne + (_amountStaked, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + assertEq(_amountStaked, 50); + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate_two) * 50) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + } + + function test_revert_claimRewards_noRewards() public { + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(0, 50); + + //=================== try to claim rewards for a different token + + vm.prank(stakerOne); + vm.expectRevert("No rewards"); + stakeContract.claimRewards(1); + + //=================== try to claim rewards in same block + + vm.prank(stakerOne); + vm.expectRevert("No rewards"); + stakeContract.claimRewards(0); + + //======= withdraw tokens and claim rewards + vm.roll(100); + vm.warp(1000); + + vm.prank(stakerOne); + stakeContract.withdraw(0, 50); + vm.prank(stakerOne); + stakeContract.claimRewards(0); + + //===== try to claim rewards again + vm.roll(200); + vm.warp(2000); + vm.prank(stakerOne); + vm.expectRevert("No rewards"); + stakeContract.claimRewards(0); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: claimRewards + - default timeUnit and rewards + - same token-ids staked by stakers + //////////////////////////////////////////////////////////////*/ + + function test_state_claimRewards_defaults_sameToken() public { + //================ setup stakerOne ====================== + vm.warp(1); + + // stake 50 tokens with token-id 0 + vm.prank(stakerOne); + stakeContract.stake(0, 50); + uint256 timeOfLastUpdate_one = block.timestamp; + + //=================== warp timestamp to claim rewards + vm.roll(100); + vm.warp(1000); + + uint256 rewardBalanceBefore = stakeContract.getRewardTokenBalance(); + vm.prank(stakerOne); + stakeContract.claimRewards(0); + uint256 rewardBalanceAfter = stakeContract.getRewardTokenBalance(); + + // check reward balances + assertEq( + erc20.balanceOf(stakerOne), + ((((block.timestamp - timeOfLastUpdate_one) * 50) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + assertEq( + rewardBalanceAfter, + rewardBalanceBefore - + ((((block.timestamp - timeOfLastUpdate_one) * 50) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + // check available rewards after claiming + (uint256 _amountStaked, uint256 _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq(_amountStaked, 50); + assertEq(_availableRewards, 0); + + //================ setup stakerTwo ====================== + + // stake 20 tokens with token-id 1 + vm.prank(stakerTwo); + stakeContract.stake(0, 20); + uint256 timeOfLastUpdate_two = block.timestamp; + + //=================== warp timestamp to claim rewards + vm.roll(200); + vm.warp(2000); + + rewardBalanceBefore = stakeContract.getRewardTokenBalance(); + vm.prank(stakerTwo); + stakeContract.claimRewards(0); + rewardBalanceAfter = stakeContract.getRewardTokenBalance(); + + // check reward balances + assertEq( + erc20.balanceOf(stakerTwo), + ((((block.timestamp - timeOfLastUpdate_two) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + assertEq( + rewardBalanceAfter, + rewardBalanceBefore - + ((((block.timestamp - timeOfLastUpdate_two) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + // check available rewards after claiming -- stakerTwo + (_amountStaked, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerTwo); + assertEq(_amountStaked, 20); + assertEq(_availableRewards, 0); + + // check available rewards -- stakerOne + (_amountStaked, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + assertEq(_amountStaked, 50); + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate_two) * 50) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: stake conditions + - set rewards for token0 + - default time unit + //////////////////////////////////////////////////////////////*/ + + function test_state_setRewardsPerUnitTime_token0() public { + // set value and check + uint256 rewardsPerUnitTime = 50; + vm.prank(deployer); + stakeContract.setRewardsPerUnitTime(0, rewardsPerUnitTime); + assertEq(rewardsPerUnitTime, stakeContract.getRewardsPerUnitTime(0)); + + //================ stake tokens + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(0, 50); + uint256 timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set rewardsPerUnitTime + vm.roll(100); + vm.warp(1000); + + vm.prank(deployer); + stakeContract.setRewardsPerUnitTime(0, 200); + assertEq(200, stakeContract.getRewardsPerUnitTime(0)); + uint256 newTimeOfLastUpdate = block.timestamp; + + // check available rewards -- should use previous value for rewardsPerUnitTime for calculation + (, uint256 _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * 50) * rewardsPerUnitTime) / defaultTimeUnit) + ); + + //====== check rewards after some time + vm.roll(300); + vm.warp(3000); + + (, uint256 _newRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _newRewards, + _availableRewards + ((((block.timestamp - newTimeOfLastUpdate) * 50) * 200) / defaultTimeUnit) + ); + + // =========== token 1 + //================ stake tokens + + vm.prank(stakerOne); + stakeContract.stake(1, 20); + timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set rewardsPerUnitTime for token-0 + vm.roll(400); + vm.warp(4000); + + vm.prank(deployer); + stakeContract.setRewardsPerUnitTime(0, 300); + assertEq(300, stakeContract.getRewardsPerUnitTime(0)); + newTimeOfLastUpdate = block.timestamp; + + // check available rewards for token-1 -- should use defaultRewardsPerUnitTime for calculation + (, _availableRewards) = stakeContract.getStakeInfoForToken(1, stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + //====== check rewards after some time + vm.roll(500); + vm.warp(5000); + + (, _newRewards) = stakeContract.getStakeInfoForToken(1, stakerOne); + + assertEq( + _newRewards, + ((((block.timestamp - timeOfLastUpdate) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: stake conditions + - set rewards for both tokens + - default time unit + //////////////////////////////////////////////////////////////*/ + + function test_state_setRewardsPerUnitTime_bothTokens() public { + // set value and check + uint256 rewardsPerUnitTime = 50; + vm.prank(deployer); + stakeContract.setRewardsPerUnitTime(0, rewardsPerUnitTime); + assertEq(rewardsPerUnitTime, stakeContract.getRewardsPerUnitTime(0)); + + //================ stake tokens + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(0, 50); + uint256 timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set rewardsPerUnitTime + vm.roll(100); + vm.warp(1000); + + vm.prank(deployer); + stakeContract.setRewardsPerUnitTime(0, 200); + assertEq(200, stakeContract.getRewardsPerUnitTime(0)); + uint256 newTimeOfLastUpdate = block.timestamp; + + // check available rewards -- should use previous value for rewardsPerUnitTime for calculation + (, uint256 _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * 50) * rewardsPerUnitTime) / defaultTimeUnit) + ); + + //====== check rewards after some time + vm.roll(300); + vm.warp(3000); + + (, uint256 _newRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _newRewards, + _availableRewards + ((((block.timestamp - newTimeOfLastUpdate) * 50) * 200) / defaultTimeUnit) + ); + + // =========== token 1 + //================ stake tokens + + vm.prank(stakerOne); + stakeContract.stake(1, 20); + timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and set rewardsPerUnitTime for token-1 + vm.roll(400); + vm.warp(4000); + + vm.prank(deployer); + stakeContract.setRewardsPerUnitTime(1, 300); + assertEq(300, stakeContract.getRewardsPerUnitTime(1)); + newTimeOfLastUpdate = block.timestamp; + + // check available rewards for token-1 -- should use defaultRewardsPerUnitTime for calculation + (, _availableRewards) = stakeContract.getStakeInfoForToken(1, stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + //====== check rewards after some time + vm.roll(500); + vm.warp(5000); + + (, _newRewards) = stakeContract.getStakeInfoForToken(1, stakerOne); + + // should calculate based on newTimeOfLastUpdate and rewardsPerUnitTime (not default) + assertEq( + _newRewards, + _availableRewards + ((((block.timestamp - newTimeOfLastUpdate) * 20) * 300) / defaultTimeUnit) + ); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: stake conditions + - default rewards + - set time unit for token0 + //////////////////////////////////////////////////////////////*/ + + function test_state_setTimeUnit_token0() public { + // set value and check + uint80 timeUnit = 100; + vm.prank(deployer); + stakeContract.setTimeUnit(0, timeUnit); + assertEq(timeUnit, stakeContract.getTimeUnit(0)); + + //================ stake tokens + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(0, 50); + uint256 timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set timeUnit + vm.roll(100); + vm.warp(1000); + + vm.prank(deployer); + stakeContract.setTimeUnit(0, 200); + assertEq(200, stakeContract.getTimeUnit(0)); + uint256 newTimeOfLastUpdate = block.timestamp; + + // check available rewards -- should use previous value for timeUnit for calculation + (, uint256 _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * 50) * defaultRewardsPerUnitTime) / timeUnit) + ); + + //====== check rewards after some time + vm.roll(300); + vm.warp(3000); + + (, uint256 _newRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _newRewards, + _availableRewards + ((((block.timestamp - newTimeOfLastUpdate) * 50) * defaultRewardsPerUnitTime) / 200) + ); + + // =========== token 1 + //================ stake tokens + + vm.prank(stakerOne); + stakeContract.stake(1, 20); + timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set rewardsPerUnitTime for token-0 + vm.roll(400); + vm.warp(4000); + + vm.prank(deployer); + stakeContract.setTimeUnit(0, 10); + assertEq(10, stakeContract.getTimeUnit(0)); + newTimeOfLastUpdate = block.timestamp; + + // check available rewards for token-1 -- should use defaultTimeUnit for calculation + (, _availableRewards) = stakeContract.getStakeInfoForToken(1, stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + //====== check rewards after some time + vm.roll(500); + vm.warp(5000); + + (, _newRewards) = stakeContract.getStakeInfoForToken(1, stakerOne); + + assertEq( + _newRewards, + ((((block.timestamp - timeOfLastUpdate) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: stake conditions + - default rewards + - set time unit for both tokens + //////////////////////////////////////////////////////////////*/ + + function test_state_setTimeUnit_bothTokens() public { + // set value and check + uint80 timeUnit = 100; + vm.prank(deployer); + stakeContract.setTimeUnit(0, timeUnit); + assertEq(timeUnit, stakeContract.getTimeUnit(0)); + + //================ stake tokens + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(0, 50); + uint256 timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set rewardsPerUnitTime + vm.roll(100); + vm.warp(1000); + + vm.prank(deployer); + stakeContract.setTimeUnit(0, 200); + assertEq(200, stakeContract.getTimeUnit(0)); + uint256 newTimeOfLastUpdate = block.timestamp; + + // check available rewards -- should use previous value for timeUnit for calculation + (, uint256 _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * 50) * defaultRewardsPerUnitTime) / timeUnit) + ); + + //====== check rewards after some time + vm.roll(300); + vm.warp(3000); + + (, uint256 _newRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _newRewards, + _availableRewards + ((((block.timestamp - newTimeOfLastUpdate) * 50) * defaultRewardsPerUnitTime) / 200) + ); + + // =========== token 1 + //================ stake tokens + + vm.prank(stakerOne); + stakeContract.stake(1, 20); + timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and set timeUnit for token-1 + vm.roll(400); + vm.warp(4000); + + vm.prank(deployer); + stakeContract.setTimeUnit(1, 300); + assertEq(300, stakeContract.getTimeUnit(1)); + newTimeOfLastUpdate = block.timestamp; + + // check available rewards for token-1 -- should use defaultTimeUnit for calculation + (, _availableRewards) = stakeContract.getStakeInfoForToken(1, stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + //====== check rewards after some time + vm.roll(500); + vm.warp(5000); + + (, _newRewards) = stakeContract.getStakeInfoForToken(1, stakerOne); + + // should calculate based on newTimeOfLastUpdate and new time unit (not default) + assertEq( + _newRewards, + _availableRewards + ((((block.timestamp - newTimeOfLastUpdate) * 20) * defaultRewardsPerUnitTime) / 300) + ); + } + + function test_revert_setRewardsPerUnitTime_notAuthorized() public { + vm.expectRevert("Not authorized"); + stakeContract.setRewardsPerUnitTime(0, 1); + } + + function test_revert_setTimeUnit_notAuthorized() public { + vm.expectRevert("Not authorized"); + stakeContract.setTimeUnit(0, 1); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: withdraw + - different token-ids staked by stakers + //////////////////////////////////////////////////////////////*/ + + function test_state_withdraw_differentTokens() public { + //================ stake different tokens ====================== + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(0, 50); + + vm.prank(stakerTwo); + stakeContract.stake(1, 20); + + uint256 timeOfLastUpdate = block.timestamp; + + //========== warp timestamp before withdraw + vm.roll(100); + vm.warp(1000); + + // withdraw partially for stakerOne + vm.prank(stakerOne); + stakeContract.withdraw(0, 40); + uint256 timeOfLastUpdateLatest = block.timestamp; + + // check balances/ownership after withdraw + assertEq(erc1155.balanceOf(stakerOne, 0), 90); + assertEq(erc1155.balanceOf(address(stakeContract), 0), 10); + assertEq(erc1155.balanceOf(stakerTwo, 1), 80); + assertEq(erc1155.balanceOf(address(stakeContract), 1), 20); + + // check available rewards after withdraw + (, uint256 _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * 50) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + (, _availableRewards) = stakeContract.getStakeInfoForToken(1, stakerTwo); + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + // check available rewards some time after withdraw + vm.roll(200); + vm.warp(2000); + + // check rewards for stakerOne + (, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _availableRewards, + (((((timeOfLastUpdateLatest - timeOfLastUpdate) * 50)) * defaultRewardsPerUnitTime) / defaultTimeUnit) + + (((((block.timestamp - timeOfLastUpdateLatest) * 10)) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + // withdraw partially for stakerTwo + vm.prank(stakerTwo); + stakeContract.withdraw(1, 10); + timeOfLastUpdateLatest = block.timestamp; + + // check balances/ownership after withdraw + assertEq(erc1155.balanceOf(stakerOne, 0), 90); + assertEq(erc1155.balanceOf(address(stakeContract), 0), 10); + assertEq(erc1155.balanceOf(stakerTwo, 1), 90); + assertEq(erc1155.balanceOf(address(stakeContract), 1), 10); + + // check rewards for stakerTwo + (, _availableRewards) = stakeContract.getStakeInfoForToken(1, stakerTwo); + + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate) * 20)) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + // check rewards for stakerTwo after some time + vm.roll(300); + vm.warp(3000); + (, _availableRewards) = stakeContract.getStakeInfoForToken(1, stakerTwo); + + assertEq( + _availableRewards, + (((((timeOfLastUpdateLatest - timeOfLastUpdate) * 20)) * defaultRewardsPerUnitTime) / defaultTimeUnit) + + (((((block.timestamp - timeOfLastUpdateLatest) * 10)) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: withdraw + - same token-ids staked by stakers + //////////////////////////////////////////////////////////////*/ + + function test_state_withdraw_sameToken() public { + //================ stake different tokens ====================== + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(0, 50); + + vm.prank(stakerTwo); + stakeContract.stake(0, 20); + + uint256 timeOfLastUpdate = block.timestamp; + + //========== warp timestamp before withdraw + vm.roll(100); + vm.warp(1000); + + // withdraw partially for stakerOne + vm.prank(stakerOne); + stakeContract.withdraw(0, 40); + uint256 timeOfLastUpdateLatest = block.timestamp; + + // check balances/ownership after withdraw + assertEq(erc1155.balanceOf(stakerOne, 0), 90); + assertEq(erc1155.balanceOf(stakerTwo, 0), 80); + assertEq(erc1155.balanceOf(address(stakeContract), 0), 10 + 20); + + // check available rewards after withdraw + (, uint256 _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * 50) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + (, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerTwo); + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + // check available rewards some time after withdraw + vm.roll(200); + vm.warp(2000); + + // check rewards for stakerOne + (, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _availableRewards, + (((((timeOfLastUpdateLatest - timeOfLastUpdate) * 50)) * defaultRewardsPerUnitTime) / defaultTimeUnit) + + (((((block.timestamp - timeOfLastUpdateLatest) * 10)) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + // withdraw partially for stakerTwo + vm.prank(stakerTwo); + stakeContract.withdraw(0, 10); + timeOfLastUpdateLatest = block.timestamp; + + // check balances/ownership after withdraw + assertEq(erc1155.balanceOf(stakerOne, 0), 90); + assertEq(erc1155.balanceOf(stakerTwo, 0), 90); + assertEq(erc1155.balanceOf(address(stakeContract), 0), 10 + 10); + + // check rewards for stakerTwo + (, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerTwo); + + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate) * 20)) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + // check rewards for stakerTwo after some time + vm.roll(300); + vm.warp(3000); + (, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerTwo); + + assertEq( + _availableRewards, + (((((timeOfLastUpdateLatest - timeOfLastUpdate) * 20)) * defaultRewardsPerUnitTime) / defaultTimeUnit) + + (((((block.timestamp - timeOfLastUpdateLatest) * 10)) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + } + + function test_revert_withdraw_withdrawingZeroTokens() public { + vm.expectRevert("Withdrawing 0 tokens"); + stakeContract.withdraw(0, 0); + } + + function test_revert_withdraw_withdrawingMoreThanStaked() public { + // stake tokens + vm.prank(stakerOne); + stakeContract.stake(0, 50); + + vm.prank(stakerTwo); + stakeContract.stake(1, 20); + + vm.prank(stakerTwo); + stakeContract.stake(0, 20); + + // view staked tokens + vm.roll(200); + vm.warp(2000); + (uint256[] memory _tokensStaked, uint256[] memory _tokenAmounts, uint256 _totalRewards) = stakeContract + .getStakeInfo(stakerOne); + + console.log("==== staker one ===="); + for (uint256 i = 0; i < _tokensStaked.length; i++) { + console.log(_tokensStaked[i], _tokenAmounts[i]); + } + + (_tokensStaked, _tokenAmounts, _totalRewards) = stakeContract.getStakeInfo(stakerTwo); + + console.log("==== staker two ===="); + for (uint256 i = 0; i < _tokensStaked.length; i++) { + console.log(_tokensStaked[i], _tokenAmounts[i]); + } + + // trying to withdraw more than staked + vm.prank(stakerOne); + vm.expectRevert("Withdrawing more than staked"); + stakeContract.withdraw(0, 60); + + // withdraw partially + vm.prank(stakerOne); + stakeContract.withdraw(0, 30); + + // trying to withdraw more than staked + vm.prank(stakerOne); + vm.expectRevert("Withdrawing more than staked"); + stakeContract.withdraw(0, 60); + + // re-stake + vm.prank(stakerOne); + stakeContract.stake(0, 30); + + // trying to withdraw more than staked + vm.prank(stakerOne); + vm.expectRevert("Withdrawing more than staked"); + stakeContract.withdraw(0, 60); + + // trying to withdraw different tokens + vm.prank(stakerOne); + vm.expectRevert("Withdrawing more than staked"); + stakeContract.withdraw(1, 20); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function test_revert_zeroTimeUnit_adminLockTokens() public { + //================ stake tokens + vm.warp(1); + + // User stakes tokens + vm.prank(stakerOne); + stakeContract.stake(0, 50); + + // set default timeUnit to zero + uint80 newTimeUnit = 0; + vm.prank(deployer); + vm.expectRevert("time-unit can't be 0"); + stakeContract.setDefaultTimeUnit(newTimeUnit); + + // set timeUnit to zero + vm.prank(deployer); + vm.expectRevert("time-unit can't be 0"); + stakeContract.setTimeUnit(0, newTimeUnit); + + // stakerOne and stakerTwo can withdraw their tokens + // vm.expectRevert(stdError.divisionError); + vm.prank(stakerOne); + stakeContract.withdraw(0, 50); + } + + function test_Macro_EditionDirectSafeTransferLocksToken() public { + uint256 tokenId = 0; + + // stakerOne mistakenly safe-transfers direct to the staking contract + vm.prank(stakerOne); + vm.expectRevert("Direct transfer"); + erc1155.safeTransferFrom(stakerOne, address(stakeContract), tokenId, 100, ""); + + // show that the transferred tokens were not properly staked + // (uint256 tokensStaked, uint256 rewards) = stakeContract.getStakeInfoForToken(tokenId, stakerOne); + // assertEq(0, tokensStaked); + + // // show that stakerOne cannot recover the tokens + // vm.expectRevert(); + // vm.prank(stakerOne); + // stakeContract.withdraw(tokenId, 100); + } +} + +contract Macro_EditionStakeTest is BaseTest { + EditionStake internal stakeContract; + + uint256 internal defaultTimeUnit; + uint256 internal defaultRewardsPerUnitTime; + uint64 internal tokenAmount = 100; + address internal stakerOne = address(0x345); + address internal stakerTwo = address(0x567); + + function setUp() public override { + super.setUp(); + + defaultTimeUnit = 60; + defaultRewardsPerUnitTime = 1; + + // mint erc1155 tokens to stakers + erc1155.mint(stakerOne, 1, tokenAmount); + erc1155.mint(stakerTwo, 2, tokenAmount); + + // mint reward tokens to contract admin + erc20.mint(deployer, 1000 ether); + + stakeContract = EditionStake(payable(getContract("EditionStake"))); + + // set approval + vm.prank(stakerOne); + erc1155.setApprovalForAll(address(stakeContract), true); + vm.prank(stakerTwo); + erc1155.setApprovalForAll(address(stakeContract), true); + } + + // Demostrate setting unitTime to 0 locks the tokens irreversibly + function testEdition_adminLockTokens() public { + //================ stake tokens + vm.warp(1); + + // Two users stake 1 tokens each + vm.prank(stakerOne); + stakeContract.stake(1, tokenAmount); + vm.prank(stakerTwo); + stakeContract.stake(2, tokenAmount); + + // set timeUnit to zero + uint80 newTimeUnit = 0; + vm.prank(deployer); + vm.expectRevert("time-unit can't be 0"); + stakeContract.setDefaultTimeUnit(newTimeUnit); + + // stakerOne and stakerTwo can't withdraw their tokens + // vm.expectRevert(stdError.divisionError); + vm.prank(stakerOne); + stakeContract.withdraw(1, tokenAmount); + + // vm.expectRevert(stdError.divisionError); + vm.prank(stakerTwo); + stakeContract.withdraw(2, tokenAmount); + + // timeUnit can't be changed back to a nonzero value + newTimeUnit = 40; + // vm.expectRevert(stdError.divisionError); + vm.prank(deployer); + stakeContract.setDefaultTimeUnit(newTimeUnit); + } + + // Demostrate setting rewardsPerTimeUnit to a high value locks the tokens irreversibly + function testEdition_demostrate_adminRewardsLock() public { + //================ stake tokens + vm.warp(1); + + // Two users stake 1 tokens each + vm.prank(stakerOne); + stakeContract.stake(1, tokenAmount); + vm.prank(stakerTwo); + stakeContract.stake(2, tokenAmount); + + // set rewardsPerTimeUnit to max value + uint256 rewardsPerTimeUnit = type(uint256).max; + vm.prank(deployer); + stakeContract.setDefaultRewardsPerUnitTime(rewardsPerTimeUnit); + + vm.warp(1 days); + + // stakerOne and stakerTwo can't withdraw their tokens + // vm.expectRevert(stdError.arithmeticError); + vm.prank(stakerOne); + stakeContract.withdraw(1, tokenAmount); + + // vm.expectRevert(stdError.arithmeticError); + vm.prank(stakerTwo); + stakeContract.withdraw(2, tokenAmount); + + // timeUnit can't be changed back + rewardsPerTimeUnit = 60; + // vm.expectRevert(stdError.arithmeticError); + vm.prank(deployer); + stakeContract.setDefaultRewardsPerUnitTime(rewardsPerTimeUnit); + } +} diff --git a/src/test/staking/EditionStake_EthReward.t.sol b/src/test/staking/EditionStake_EthReward.t.sol new file mode 100644 index 000000000..2f829cdb9 --- /dev/null +++ b/src/test/staking/EditionStake_EthReward.t.sol @@ -0,0 +1,1063 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { EditionStake } from "contracts/prebuilts/staking/EditionStake.sol"; + +// Test imports + +import "../utils/BaseTest.sol"; + +contract EditionStakeEthRewardTest is BaseTest { + EditionStake internal stakeContract; + + address internal stakerOne; + address internal stakerTwo; + + uint256 internal defaultTimeUnit; + uint256 internal defaultRewardsPerUnitTime; + + function setUp() public override { + super.setUp(); + + defaultTimeUnit = 60; + defaultRewardsPerUnitTime = 1; + + stakerOne = address(0x345); + stakerTwo = address(0x567); + + erc1155.mint(stakerOne, 0, 100); // mint 100 tokens with id 0 to stakerOne + erc1155.mint(stakerOne, 1, 100); // mint 100 tokens with id 1 to stakerOne + + erc1155.mint(stakerTwo, 0, 100); // mint 100 tokens with id 0 to stakerTwo + erc1155.mint(stakerTwo, 1, 100); // mint 100 tokens with id 1 to stakerTwo + + vm.deal(deployer, 1000 ether); // mint reward tokens (Eth) to contract admin + + stakeContract = EditionStake( + payable( + deployContractProxy( + "EditionStake", + abi.encodeCall( + EditionStake.initialize, + (deployer, CONTRACT_URI, forwarders(), NATIVE_TOKEN, address(erc1155), 60, 1) + ) + ) + ) + ); + + // set approvals + vm.prank(stakerOne); + erc1155.setApprovalForAll(address(stakeContract), true); + + vm.prank(stakerTwo); + erc1155.setApprovalForAll(address(stakeContract), true); + + vm.startPrank(deployer); + stakeContract.depositRewardTokens{ value: 100 ether }(100 ether); + // erc20.transfer(address(stakeContract), 100 ether); + vm.stopPrank(); + assertEq(stakeContract.getRewardTokenBalance(), 100 ether); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: Stake + - with default time-unit and rewards + - different token-ids staked by stakers + //////////////////////////////////////////////////////////////*/ + + function test_state_stake_defaults_differentTokens() public { + //================ first staker ====================== + vm.warp(1); + + // stake 50 tokens with token-id 0 + vm.prank(stakerOne); + stakeContract.stake(0, 50); + uint256 timeOfLastUpdate_one = block.timestamp; + + // check balances/ownership of staked tokens + assertEq(erc1155.balanceOf(address(stakeContract), 0), 50); + assertEq(erc1155.balanceOf(address(stakerOne), 0), 50); + + // check available rewards right after staking + (uint256 _amountStaked, uint256 _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq(_amountStaked, 50); + assertEq(_availableRewards, 0); + + //=================== warp timestamp to calculate rewards + vm.roll(100); + vm.warp(1000); + + // check available rewards after warp + (, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate_one) * 50) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + //================ second staker ====================== + vm.roll(200); + vm.warp(2000); + + // stake 20 tokens with token-id 1 + vm.prank(stakerTwo); + stakeContract.stake(1, 20); + uint256 timeOfLastUpdate_two = block.timestamp; + + // check balances/ownership of staked tokens + assertEq(erc1155.balanceOf(address(stakeContract), 1), 20); + assertEq(erc1155.balanceOf(address(stakerTwo), 1), 80); + + // check available rewards right after staking + (_amountStaked, _availableRewards) = stakeContract.getStakeInfoForToken(1, stakerTwo); + + assertEq(_amountStaked, 20); + assertEq(_availableRewards, 0); + + //=================== warp timestamp to calculate rewards + vm.roll(300); + vm.warp(3000); + + // check available rewards for stakerOne + (, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate_one) * 50) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + // check available rewards for stakerTwo + (, _availableRewards) = stakeContract.getStakeInfoForToken(1, stakerTwo); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate_two) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + } + + function test_revert_stake_stakingZeroTokens() public { + // stake 0 tokens + + vm.prank(stakerOne); + vm.expectRevert("Staking 0 tokens"); + stakeContract.stake(0, 0); + } + + function test_revert_stake_notBalanceOrApproved() public { + // stake unowned tokens + vm.prank(stakerOne); + vm.expectRevert("ERC1155: insufficient balance for transfer"); + stakeContract.stake(2, 10); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: Stake + - with default time-unit and rewards + - same token-id staked by stakers + //////////////////////////////////////////////////////////////*/ + + function test_state_stake_defaults_sameToken() public { + //================ first staker ====================== + vm.warp(1); + + // stake 50 tokens with token-id 0 + vm.prank(stakerOne); + stakeContract.stake(0, 50); + uint256 timeOfLastUpdate_one = block.timestamp; + + // check balances/ownership of staked tokens + assertEq(erc1155.balanceOf(address(stakeContract), 0), 50); + assertEq(erc1155.balanceOf(address(stakerOne), 0), 50); + + // check available rewards right after staking + (uint256 _amountStaked, uint256 _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq(_amountStaked, 50); + assertEq(_availableRewards, 0); + + //=================== warp timestamp to calculate rewards + vm.roll(100); + vm.warp(1000); + + // check available rewards after warp + (, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate_one) * 50) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + //================ second staker ====================== + vm.roll(200); + vm.warp(2000); + + // stake 20 tokens with token-id 0 + vm.prank(stakerTwo); + stakeContract.stake(0, 20); + uint256 timeOfLastUpdate_two = block.timestamp; + + // check balances/ownership of staked tokens + assertEq(erc1155.balanceOf(address(stakeContract), 0), 20 + 50); // sum of staked tokens by both stakers + assertEq(erc1155.balanceOf(address(stakerTwo), 0), 80); + + // check available rewards right after staking + (_amountStaked, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerTwo); + + assertEq(_amountStaked, 20); + assertEq(_availableRewards, 0); + + //=================== warp timestamp to calculate rewards + vm.roll(300); + vm.warp(3000); + + // check available rewards for stakerOne + (, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate_one) * 50) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + // check available rewards for stakerTwo + (, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerTwo); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate_two) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: claimRewards + - default timeUnit and rewards + - different token-ids staked by stakers + //////////////////////////////////////////////////////////////*/ + + function test_state_claimRewards_defaults_differentTokens() public { + //================ setup stakerOne ====================== + vm.warp(1); + + // stake 50 tokens with token-id 0 + vm.prank(stakerOne); + stakeContract.stake(0, 50); + uint256 timeOfLastUpdate_one = block.timestamp; + + //=================== warp timestamp to claim rewards + vm.roll(100); + vm.warp(1000); + + uint256 rewardBalanceBefore = stakeContract.getRewardTokenBalance(); + vm.prank(stakerOne); + stakeContract.claimRewards(0); + uint256 rewardBalanceAfter = stakeContract.getRewardTokenBalance(); + + // check reward balances + assertEq( + stakerOne.balance, + ((((block.timestamp - timeOfLastUpdate_one) * 50) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + assertEq( + rewardBalanceAfter, + rewardBalanceBefore - + ((((block.timestamp - timeOfLastUpdate_one) * 50) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + // check available rewards after claiming + (uint256 _amountStaked, uint256 _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq(_amountStaked, 50); + assertEq(_availableRewards, 0); + + //================ setup stakerTwo ====================== + + // stake 20 tokens with token-id 1 + vm.prank(stakerTwo); + stakeContract.stake(1, 20); + uint256 timeOfLastUpdate_two = block.timestamp; + + //=================== warp timestamp to claim rewards + vm.roll(200); + vm.warp(2000); + + rewardBalanceBefore = stakeContract.getRewardTokenBalance(); + vm.prank(stakerTwo); + stakeContract.claimRewards(1); + rewardBalanceAfter = stakeContract.getRewardTokenBalance(); + + // check reward balances + assertEq( + stakerTwo.balance, + ((((block.timestamp - timeOfLastUpdate_two) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + assertEq( + rewardBalanceAfter, + rewardBalanceBefore - + ((((block.timestamp - timeOfLastUpdate_two) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + // check available rewards after claiming -- stakerTwo + (_amountStaked, _availableRewards) = stakeContract.getStakeInfoForToken(1, stakerTwo); + assertEq(_amountStaked, 20); + assertEq(_availableRewards, 0); + + // check available rewards -- stakerOne + (_amountStaked, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + assertEq(_amountStaked, 50); + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate_two) * 50) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + } + + function test_revert_claimRewards_noRewards() public { + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(0, 50); + + //=================== try to claim rewards for a different token + + vm.prank(stakerOne); + vm.expectRevert("No rewards"); + stakeContract.claimRewards(1); + + //=================== try to claim rewards in same block + + vm.prank(stakerOne); + vm.expectRevert("No rewards"); + stakeContract.claimRewards(0); + + //======= withdraw tokens and claim rewards + vm.roll(100); + vm.warp(1000); + + vm.prank(stakerOne); + stakeContract.withdraw(0, 50); + vm.prank(stakerOne); + stakeContract.claimRewards(0); + + //===== try to claim rewards again + vm.roll(200); + vm.warp(2000); + vm.prank(stakerOne); + vm.expectRevert("No rewards"); + stakeContract.claimRewards(0); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: claimRewards + - default timeUnit and rewards + - same token-ids staked by stakers + //////////////////////////////////////////////////////////////*/ + + function test_state_claimRewards_defaults_sameToken() public { + //================ setup stakerOne ====================== + vm.warp(1); + + // stake 50 tokens with token-id 0 + vm.prank(stakerOne); + stakeContract.stake(0, 50); + uint256 timeOfLastUpdate_one = block.timestamp; + + //=================== warp timestamp to claim rewards + vm.roll(100); + vm.warp(1000); + + uint256 rewardBalanceBefore = stakeContract.getRewardTokenBalance(); + vm.prank(stakerOne); + stakeContract.claimRewards(0); + uint256 rewardBalanceAfter = stakeContract.getRewardTokenBalance(); + + // check reward balances + assertEq( + stakerOne.balance, + ((((block.timestamp - timeOfLastUpdate_one) * 50) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + assertEq( + rewardBalanceAfter, + rewardBalanceBefore - + ((((block.timestamp - timeOfLastUpdate_one) * 50) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + // check available rewards after claiming + (uint256 _amountStaked, uint256 _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq(_amountStaked, 50); + assertEq(_availableRewards, 0); + + //================ setup stakerTwo ====================== + + // stake 20 tokens with token-id 1 + vm.prank(stakerTwo); + stakeContract.stake(0, 20); + uint256 timeOfLastUpdate_two = block.timestamp; + + //=================== warp timestamp to claim rewards + vm.roll(200); + vm.warp(2000); + + rewardBalanceBefore = stakeContract.getRewardTokenBalance(); + vm.prank(stakerTwo); + stakeContract.claimRewards(0); + rewardBalanceAfter = stakeContract.getRewardTokenBalance(); + + // check reward balances + assertEq( + stakerTwo.balance, + ((((block.timestamp - timeOfLastUpdate_two) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + assertEq( + rewardBalanceAfter, + rewardBalanceBefore - + ((((block.timestamp - timeOfLastUpdate_two) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + // check available rewards after claiming -- stakerTwo + (_amountStaked, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerTwo); + assertEq(_amountStaked, 20); + assertEq(_availableRewards, 0); + + // check available rewards -- stakerOne + (_amountStaked, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + assertEq(_amountStaked, 50); + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate_two) * 50) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: stake conditions + - set rewards for token0 + - default time unit + //////////////////////////////////////////////////////////////*/ + + function test_state_setRewardsPerUnitTime_token0() public { + // set value and check + uint256 rewardsPerUnitTime = 50; + vm.prank(deployer); + stakeContract.setRewardsPerUnitTime(0, rewardsPerUnitTime); + assertEq(rewardsPerUnitTime, stakeContract.getRewardsPerUnitTime(0)); + + //================ stake tokens + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(0, 50); + uint256 timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set rewardsPerUnitTime + vm.roll(100); + vm.warp(1000); + + vm.prank(deployer); + stakeContract.setRewardsPerUnitTime(0, 200); + assertEq(200, stakeContract.getRewardsPerUnitTime(0)); + uint256 newTimeOfLastUpdate = block.timestamp; + + // check available rewards -- should use previous value for rewardsPerUnitTime for calculation + (, uint256 _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * 50) * rewardsPerUnitTime) / defaultTimeUnit) + ); + + //====== check rewards after some time + vm.roll(300); + vm.warp(3000); + + (, uint256 _newRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _newRewards, + _availableRewards + ((((block.timestamp - newTimeOfLastUpdate) * 50) * 200) / defaultTimeUnit) + ); + + // =========== token 1 + //================ stake tokens + + vm.prank(stakerOne); + stakeContract.stake(1, 20); + timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set rewardsPerUnitTime for token-0 + vm.roll(400); + vm.warp(4000); + + vm.prank(deployer); + stakeContract.setRewardsPerUnitTime(0, 300); + assertEq(300, stakeContract.getRewardsPerUnitTime(0)); + newTimeOfLastUpdate = block.timestamp; + + // check available rewards for token-1 -- should use defaultRewardsPerUnitTime for calculation + (, _availableRewards) = stakeContract.getStakeInfoForToken(1, stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + //====== check rewards after some time + vm.roll(500); + vm.warp(5000); + + (, _newRewards) = stakeContract.getStakeInfoForToken(1, stakerOne); + + assertEq( + _newRewards, + ((((block.timestamp - timeOfLastUpdate) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: stake conditions + - set rewards for both tokens + - default time unit + //////////////////////////////////////////////////////////////*/ + + function test_state_setRewardsPerUnitTime_bothTokens() public { + // set value and check + uint256 rewardsPerUnitTime = 50; + vm.prank(deployer); + stakeContract.setRewardsPerUnitTime(0, rewardsPerUnitTime); + assertEq(rewardsPerUnitTime, stakeContract.getRewardsPerUnitTime(0)); + + //================ stake tokens + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(0, 50); + uint256 timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set rewardsPerUnitTime + vm.roll(100); + vm.warp(1000); + + vm.prank(deployer); + stakeContract.setRewardsPerUnitTime(0, 200); + assertEq(200, stakeContract.getRewardsPerUnitTime(0)); + uint256 newTimeOfLastUpdate = block.timestamp; + + // check available rewards -- should use previous value for rewardsPerUnitTime for calculation + (, uint256 _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * 50) * rewardsPerUnitTime) / defaultTimeUnit) + ); + + //====== check rewards after some time + vm.roll(300); + vm.warp(3000); + + (, uint256 _newRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _newRewards, + _availableRewards + ((((block.timestamp - newTimeOfLastUpdate) * 50) * 200) / defaultTimeUnit) + ); + + // =========== token 1 + //================ stake tokens + + vm.prank(stakerOne); + stakeContract.stake(1, 20); + timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and set rewardsPerUnitTime for token-1 + vm.roll(400); + vm.warp(4000); + + vm.prank(deployer); + stakeContract.setRewardsPerUnitTime(1, 300); + assertEq(300, stakeContract.getRewardsPerUnitTime(1)); + newTimeOfLastUpdate = block.timestamp; + + // check available rewards for token-1 -- should use defaultRewardsPerUnitTime for calculation + (, _availableRewards) = stakeContract.getStakeInfoForToken(1, stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + //====== check rewards after some time + vm.roll(500); + vm.warp(5000); + + (, _newRewards) = stakeContract.getStakeInfoForToken(1, stakerOne); + + // should calculate based on newTimeOfLastUpdate and rewardsPerUnitTime (not default) + assertEq( + _newRewards, + _availableRewards + ((((block.timestamp - newTimeOfLastUpdate) * 20) * 300) / defaultTimeUnit) + ); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: stake conditions + - default rewards + - set time unit for token0 + //////////////////////////////////////////////////////////////*/ + + function test_state_setTimeUnit_token0() public { + // set value and check + uint80 timeUnit = 100; + vm.prank(deployer); + stakeContract.setTimeUnit(0, timeUnit); + assertEq(timeUnit, stakeContract.getTimeUnit(0)); + + //================ stake tokens + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(0, 50); + uint256 timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set timeUnit + vm.roll(100); + vm.warp(1000); + + vm.prank(deployer); + stakeContract.setTimeUnit(0, 200); + assertEq(200, stakeContract.getTimeUnit(0)); + uint256 newTimeOfLastUpdate = block.timestamp; + + // check available rewards -- should use previous value for timeUnit for calculation + (, uint256 _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * 50) * defaultRewardsPerUnitTime) / timeUnit) + ); + + //====== check rewards after some time + vm.roll(300); + vm.warp(3000); + + (, uint256 _newRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _newRewards, + _availableRewards + ((((block.timestamp - newTimeOfLastUpdate) * 50) * defaultRewardsPerUnitTime) / 200) + ); + + // =========== token 1 + //================ stake tokens + + vm.prank(stakerOne); + stakeContract.stake(1, 20); + timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set rewardsPerUnitTime for token-0 + vm.roll(400); + vm.warp(4000); + + vm.prank(deployer); + stakeContract.setTimeUnit(0, 10); + assertEq(10, stakeContract.getTimeUnit(0)); + newTimeOfLastUpdate = block.timestamp; + + // check available rewards for token-1 -- should use defaultTimeUnit for calculation + (, _availableRewards) = stakeContract.getStakeInfoForToken(1, stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + //====== check rewards after some time + vm.roll(500); + vm.warp(5000); + + (, _newRewards) = stakeContract.getStakeInfoForToken(1, stakerOne); + + assertEq( + _newRewards, + ((((block.timestamp - timeOfLastUpdate) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: stake conditions + - default rewards + - set time unit for both tokens + //////////////////////////////////////////////////////////////*/ + + function test_state_setTimeUnit_bothTokens() public { + // set value and check + uint80 timeUnit = 100; + vm.prank(deployer); + stakeContract.setTimeUnit(0, timeUnit); + assertEq(timeUnit, stakeContract.getTimeUnit(0)); + + //================ stake tokens + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(0, 50); + uint256 timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set rewardsPerUnitTime + vm.roll(100); + vm.warp(1000); + + vm.prank(deployer); + stakeContract.setTimeUnit(0, 200); + assertEq(200, stakeContract.getTimeUnit(0)); + uint256 newTimeOfLastUpdate = block.timestamp; + + // check available rewards -- should use previous value for timeUnit for calculation + (, uint256 _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * 50) * defaultRewardsPerUnitTime) / timeUnit) + ); + + //====== check rewards after some time + vm.roll(300); + vm.warp(3000); + + (, uint256 _newRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _newRewards, + _availableRewards + ((((block.timestamp - newTimeOfLastUpdate) * 50) * defaultRewardsPerUnitTime) / 200) + ); + + // =========== token 1 + //================ stake tokens + + vm.prank(stakerOne); + stakeContract.stake(1, 20); + timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and set timeUnit for token-1 + vm.roll(400); + vm.warp(4000); + + vm.prank(deployer); + stakeContract.setTimeUnit(1, 300); + assertEq(300, stakeContract.getTimeUnit(1)); + newTimeOfLastUpdate = block.timestamp; + + // check available rewards for token-1 -- should use defaultTimeUnit for calculation + (, _availableRewards) = stakeContract.getStakeInfoForToken(1, stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + //====== check rewards after some time + vm.roll(500); + vm.warp(5000); + + (, _newRewards) = stakeContract.getStakeInfoForToken(1, stakerOne); + + // should calculate based on newTimeOfLastUpdate and new time unit (not default) + assertEq( + _newRewards, + _availableRewards + ((((block.timestamp - newTimeOfLastUpdate) * 20) * defaultRewardsPerUnitTime) / 300) + ); + } + + function test_revert_setRewardsPerUnitTime_notAuthorized() public { + vm.expectRevert("Not authorized"); + stakeContract.setRewardsPerUnitTime(0, 1); + } + + function test_revert_setTimeUnit_notAuthorized() public { + vm.expectRevert("Not authorized"); + stakeContract.setTimeUnit(0, 1); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: withdraw + - different token-ids staked by stakers + //////////////////////////////////////////////////////////////*/ + + function test_state_withdraw_differentTokens() public { + //================ stake different tokens ====================== + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(0, 50); + + vm.prank(stakerTwo); + stakeContract.stake(1, 20); + + uint256 timeOfLastUpdate = block.timestamp; + + //========== warp timestamp before withdraw + vm.roll(100); + vm.warp(1000); + + // withdraw partially for stakerOne + vm.prank(stakerOne); + stakeContract.withdraw(0, 40); + uint256 timeOfLastUpdateLatest = block.timestamp; + + // check balances/ownership after withdraw + assertEq(erc1155.balanceOf(stakerOne, 0), 90); + assertEq(erc1155.balanceOf(address(stakeContract), 0), 10); + assertEq(erc1155.balanceOf(stakerTwo, 1), 80); + assertEq(erc1155.balanceOf(address(stakeContract), 1), 20); + + // check available rewards after withdraw + (, uint256 _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * 50) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + (, _availableRewards) = stakeContract.getStakeInfoForToken(1, stakerTwo); + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + // check available rewards some time after withdraw + vm.roll(200); + vm.warp(2000); + + // check rewards for stakerOne + (, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _availableRewards, + (((((timeOfLastUpdateLatest - timeOfLastUpdate) * 50)) * defaultRewardsPerUnitTime) / defaultTimeUnit) + + (((((block.timestamp - timeOfLastUpdateLatest) * 10)) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + // withdraw partially for stakerTwo + vm.prank(stakerTwo); + stakeContract.withdraw(1, 10); + timeOfLastUpdateLatest = block.timestamp; + + // check balances/ownership after withdraw + assertEq(erc1155.balanceOf(stakerOne, 0), 90); + assertEq(erc1155.balanceOf(address(stakeContract), 0), 10); + assertEq(erc1155.balanceOf(stakerTwo, 1), 90); + assertEq(erc1155.balanceOf(address(stakeContract), 1), 10); + + // check rewards for stakerTwo + (, _availableRewards) = stakeContract.getStakeInfoForToken(1, stakerTwo); + + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate) * 20)) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + // check rewards for stakerTwo after some time + vm.roll(300); + vm.warp(3000); + (, _availableRewards) = stakeContract.getStakeInfoForToken(1, stakerTwo); + + assertEq( + _availableRewards, + (((((timeOfLastUpdateLatest - timeOfLastUpdate) * 20)) * defaultRewardsPerUnitTime) / defaultTimeUnit) + + (((((block.timestamp - timeOfLastUpdateLatest) * 10)) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: withdraw + - same token-ids staked by stakers + //////////////////////////////////////////////////////////////*/ + + function test_state_withdraw_sameToken() public { + //================ stake different tokens ====================== + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(0, 50); + + vm.prank(stakerTwo); + stakeContract.stake(0, 20); + + uint256 timeOfLastUpdate = block.timestamp; + + //========== warp timestamp before withdraw + vm.roll(100); + vm.warp(1000); + + // withdraw partially for stakerOne + vm.prank(stakerOne); + stakeContract.withdraw(0, 40); + uint256 timeOfLastUpdateLatest = block.timestamp; + + // check balances/ownership after withdraw + assertEq(erc1155.balanceOf(stakerOne, 0), 90); + assertEq(erc1155.balanceOf(stakerTwo, 0), 80); + assertEq(erc1155.balanceOf(address(stakeContract), 0), 10 + 20); + + // check available rewards after withdraw + (, uint256 _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * 50) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + (, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerTwo); + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * 20) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + // check available rewards some time after withdraw + vm.roll(200); + vm.warp(2000); + + // check rewards for stakerOne + (, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerOne); + + assertEq( + _availableRewards, + (((((timeOfLastUpdateLatest - timeOfLastUpdate) * 50)) * defaultRewardsPerUnitTime) / defaultTimeUnit) + + (((((block.timestamp - timeOfLastUpdateLatest) * 10)) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + // withdraw partially for stakerTwo + vm.prank(stakerTwo); + stakeContract.withdraw(0, 10); + timeOfLastUpdateLatest = block.timestamp; + + // check balances/ownership after withdraw + assertEq(erc1155.balanceOf(stakerOne, 0), 90); + assertEq(erc1155.balanceOf(stakerTwo, 0), 90); + assertEq(erc1155.balanceOf(address(stakeContract), 0), 10 + 10); + + // check rewards for stakerTwo + (, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerTwo); + + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate) * 20)) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + + // check rewards for stakerTwo after some time + vm.roll(300); + vm.warp(3000); + (, _availableRewards) = stakeContract.getStakeInfoForToken(0, stakerTwo); + + assertEq( + _availableRewards, + (((((timeOfLastUpdateLatest - timeOfLastUpdate) * 20)) * defaultRewardsPerUnitTime) / defaultTimeUnit) + + (((((block.timestamp - timeOfLastUpdateLatest) * 10)) * defaultRewardsPerUnitTime) / defaultTimeUnit) + ); + } + + function test_revert_withdraw_withdrawingZeroTokens() public { + vm.expectRevert("Withdrawing 0 tokens"); + stakeContract.withdraw(0, 0); + } + + function test_revert_withdraw_withdrawingMoreThanStaked() public { + // stake tokens + vm.prank(stakerOne); + stakeContract.stake(0, 50); + + vm.prank(stakerTwo); + stakeContract.stake(1, 20); + + vm.prank(stakerTwo); + stakeContract.stake(0, 20); + + // view staked tokens + vm.roll(200); + vm.warp(2000); + (uint256[] memory _tokensStaked, uint256[] memory _tokenAmounts, uint256 _totalRewards) = stakeContract + .getStakeInfo(stakerOne); + + console.log("==== staker one ===="); + for (uint256 i = 0; i < _tokensStaked.length; i++) { + console.log(_tokensStaked[i], _tokenAmounts[i]); + } + + (_tokensStaked, _tokenAmounts, _totalRewards) = stakeContract.getStakeInfo(stakerTwo); + + console.log("==== staker two ===="); + for (uint256 i = 0; i < _tokensStaked.length; i++) { + console.log(_tokensStaked[i], _tokenAmounts[i]); + } + + // trying to withdraw more than staked + vm.prank(stakerOne); + vm.expectRevert("Withdrawing more than staked"); + stakeContract.withdraw(0, 60); + + // withdraw partially + vm.prank(stakerOne); + stakeContract.withdraw(0, 30); + + // trying to withdraw more than staked + vm.prank(stakerOne); + vm.expectRevert("Withdrawing more than staked"); + stakeContract.withdraw(0, 60); + + // re-stake + vm.prank(stakerOne); + stakeContract.stake(0, 30); + + // trying to withdraw more than staked + vm.prank(stakerOne); + vm.expectRevert("Withdrawing more than staked"); + stakeContract.withdraw(0, 60); + + // trying to withdraw different tokens + vm.prank(stakerOne); + vm.expectRevert("Withdrawing more than staked"); + stakeContract.withdraw(1, 20); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function test_revert_zeroTimeUnit_adminLockTokens() public { + //================ stake tokens + vm.warp(1); + + // User stakes tokens + vm.prank(stakerOne); + stakeContract.stake(0, 50); + + // set default timeUnit to zero + uint80 newTimeUnit = 0; + vm.prank(deployer); + vm.expectRevert("time-unit can't be 0"); + stakeContract.setDefaultTimeUnit(newTimeUnit); + + // set timeUnit to zero + vm.prank(deployer); + vm.expectRevert("time-unit can't be 0"); + stakeContract.setTimeUnit(0, newTimeUnit); + + // stakerOne and stakerTwo can withdraw their tokens + // vm.expectRevert(stdError.divisionError); + vm.prank(stakerOne); + stakeContract.withdraw(0, 50); + } + + function test_Macro_EditionDirectSafeTransferLocksToken() public { + uint256 tokenId = 0; + + // stakerOne mistakenly safe-transfers direct to the staking contract + vm.prank(stakerOne); + vm.expectRevert("Direct transfer"); + erc1155.safeTransferFrom(stakerOne, address(stakeContract), tokenId, 100, ""); + + // show that the transferred tokens were not properly staked + // (uint256 tokensStaked, uint256 rewards) = stakeContract.getStakeInfoForToken(tokenId, stakerOne); + // assertEq(0, tokensStaked); + + // // show that stakerOne cannot recover the tokens + // vm.expectRevert(); + // vm.prank(stakerOne); + // stakeContract.withdraw(tokenId, 100); + } +} diff --git a/src/test/staking/NFTStake.t.sol b/src/test/staking/NFTStake.t.sol new file mode 100644 index 000000000..8acd02214 --- /dev/null +++ b/src/test/staking/NFTStake.t.sol @@ -0,0 +1,583 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { NFTStake } from "contracts/prebuilts/staking/NFTStake.sol"; + +// Test imports + +import "../utils/BaseTest.sol"; + +contract NFTStakeTest is BaseTest { + NFTStake internal stakeContract; + + address internal stakerOne; + address internal stakerTwo; + + uint256 internal timeUnit; + uint256 internal rewardsPerUnitTime; + + function setUp() public override { + super.setUp(); + + timeUnit = 60; + rewardsPerUnitTime = 1; + + stakerOne = address(0x345); + stakerTwo = address(0x567); + + erc721.mint(stakerOne, 5); // mint token id 0 to 4 + erc721.mint(stakerTwo, 5); // mint token id 5 to 9 + erc20.mint(deployer, 1000 ether); // mint reward tokens to contract admin + + stakeContract = NFTStake(payable(getContract("NFTStake"))); + + // set approvals + vm.prank(stakerOne); + erc721.setApprovalForAll(address(stakeContract), true); + + vm.prank(stakerTwo); + erc721.setApprovalForAll(address(stakeContract), true); + + vm.startPrank(deployer); + erc20.approve(address(stakeContract), type(uint256).max); + stakeContract.depositRewardTokens(100 ether); + // erc20.transfer(address(stakeContract), 100 ether); + vm.stopPrank(); + assertEq(stakeContract.getRewardTokenBalance(), 100 ether); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: Stake + //////////////////////////////////////////////////////////////*/ + + function test_state_stake() public { + //================ first staker ====================== + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](3); + _tokenIdsOne[0] = 0; + _tokenIdsOne[1] = 1; + _tokenIdsOne[2] = 2; + + // stake 3 tokens + vm.prank(stakerOne); + stakeContract.stake(_tokenIdsOne); + uint256 timeOfLastUpdate_one = block.timestamp; + + // check balances/ownership of staked tokens + for (uint256 i = 0; i < _tokenIdsOne.length; i++) { + assertEq(erc721.ownerOf(_tokenIdsOne[i]), address(stakeContract)); + assertEq(stakeContract.stakerAddress(_tokenIdsOne[i]), stakerOne); + } + assertEq(erc721.balanceOf(stakerOne), 2); + assertEq(erc721.balanceOf(address(stakeContract)), _tokenIdsOne.length); + + // check available rewards right after staking + (uint256[] memory _amountStaked, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq(_amountStaked.length, _tokenIdsOne.length); + assertEq(_availableRewards, 0); + + //=================== warp timestamp to calculate rewards + vm.roll(100); + vm.warp(1000); + + // check available rewards after warp + (, _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate_one) * _tokenIdsOne.length) * rewardsPerUnitTime) / timeUnit) + ); + + //================ second staker ====================== + vm.roll(200); + vm.warp(2000); + uint256[] memory _tokenIdsTwo = new uint256[](2); + _tokenIdsTwo[0] = 5; + _tokenIdsTwo[1] = 6; + + // stake 2 tokens + vm.prank(stakerTwo); + stakeContract.stake(_tokenIdsTwo); + uint256 timeOfLastUpdate_two = block.timestamp; + + // check balances/ownership of staked tokens + for (uint256 i = 0; i < _tokenIdsTwo.length; i++) { + assertEq(erc721.ownerOf(_tokenIdsTwo[i]), address(stakeContract)); + assertEq(stakeContract.stakerAddress(_tokenIdsTwo[i]), stakerTwo); + } + assertEq(erc721.balanceOf(stakerTwo), 3); + assertEq(erc721.balanceOf(address(stakeContract)), _tokenIdsTwo.length + _tokenIdsOne.length); + + // check available rewards right after staking + (_amountStaked, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + + assertEq(_amountStaked.length, _tokenIdsTwo.length); + assertEq(_availableRewards, 0); + + //=================== warp timestamp to calculate rewards + vm.roll(300); + vm.warp(3000); + + // check available rewards for stakerOne + (, _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate_one) * _tokenIdsOne.length) * rewardsPerUnitTime) / timeUnit) + ); + + // check available rewards for stakerTwo + (, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate_two) * _tokenIdsTwo.length) * rewardsPerUnitTime) / timeUnit) + ); + } + + function test_revert_stake_stakingZeroTokens() public { + // stake 0 tokens + uint256[] memory _tokenIds; + + vm.prank(stakerOne); + vm.expectRevert("Staking 0 tokens"); + stakeContract.stake(_tokenIds); + } + + function test_revert_stake_notStaker() public { + // stake unowned tokens + uint256[] memory _tokenIds = new uint256[](1); + _tokenIds[0] = 6; + + vm.prank(stakerOne); + vm.expectRevert("ERC721: transfer from incorrect owner"); + stakeContract.stake(_tokenIds); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: claimRewards + //////////////////////////////////////////////////////////////*/ + + function test_state_claimRewards() public { + //================ first staker ====================== + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](3); + _tokenIdsOne[0] = 0; + _tokenIdsOne[1] = 1; + _tokenIdsOne[2] = 2; + + // stake 3 tokens + vm.prank(stakerOne); + stakeContract.stake(_tokenIdsOne); + uint256 timeOfLastUpdate_one = block.timestamp; + + //=================== warp timestamp to claim rewards + vm.roll(100); + vm.warp(1000); + + uint256 rewardBalanceBefore = stakeContract.getRewardTokenBalance(); + vm.prank(stakerOne); + stakeContract.claimRewards(); + uint256 rewardBalanceAfter = stakeContract.getRewardTokenBalance(); + + // check reward balances + assertEq( + erc20.balanceOf(stakerOne), + ((((block.timestamp - timeOfLastUpdate_one) * _tokenIdsOne.length) * rewardsPerUnitTime) / timeUnit) + ); + assertEq( + rewardBalanceAfter, + rewardBalanceBefore - + ((((block.timestamp - timeOfLastUpdate_one) * _tokenIdsOne.length) * rewardsPerUnitTime) / timeUnit) + ); + + // check available rewards after claiming + (uint256[] memory _amountStaked, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq(_amountStaked.length, _tokenIdsOne.length); + assertEq(_availableRewards, 0); + } + + function test_revert_claimRewards_noRewards() public { + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](3); + _tokenIdsOne[0] = 0; + _tokenIdsOne[1] = 1; + _tokenIdsOne[2] = 2; + + // stake 3 tokens + vm.prank(stakerOne); + stakeContract.stake(_tokenIdsOne); + + //=================== try to claim rewards in same block + + vm.prank(stakerOne); + vm.expectRevert("No rewards"); + stakeContract.claimRewards(); + + //======= withdraw tokens and claim rewards + vm.roll(100); + vm.warp(1000); + + vm.prank(stakerOne); + stakeContract.withdraw(_tokenIdsOne); + vm.prank(stakerOne); + stakeContract.claimRewards(); + + //===== try to claim rewards again + vm.roll(200); + vm.warp(2000); + vm.prank(stakerOne); + vm.expectRevert("No rewards"); + stakeContract.claimRewards(); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: stake conditions + //////////////////////////////////////////////////////////////*/ + + function test_state_setRewardsPerUnitTime() public { + // check current value + assertEq(rewardsPerUnitTime, stakeContract.getRewardsPerUnitTime()); + + // set new value and check + uint256 newRewardsPerUnitTime = 50; + vm.prank(deployer); + stakeContract.setRewardsPerUnitTime(newRewardsPerUnitTime); + assertEq(newRewardsPerUnitTime, stakeContract.getRewardsPerUnitTime()); + + //================ stake tokens + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](3); + _tokenIdsOne[0] = 0; + _tokenIdsOne[1] = 1; + _tokenIdsOne[2] = 2; + + // stake 3 tokens + vm.prank(stakerOne); + stakeContract.stake(_tokenIdsOne); + uint256 timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set rewardsPerUnitTime + vm.roll(100); + vm.warp(1000); + + vm.prank(deployer); + stakeContract.setRewardsPerUnitTime(200); + assertEq(200, stakeContract.getRewardsPerUnitTime()); + uint256 newTimeOfLastUpdate = block.timestamp; + + // check available rewards -- should use previous value for rewardsPerUnitTime for calculation + (, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * _tokenIdsOne.length) * newRewardsPerUnitTime) / timeUnit) + ); + + //====== check rewards after some time + vm.roll(300); + vm.warp(3000); + + (, uint256 _newRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _newRewards, + _availableRewards + ((((block.timestamp - newTimeOfLastUpdate) * _tokenIdsOne.length) * 200) / timeUnit) + ); + } + + function test_revert_setRewardsPerUnitTime_notAuthorized() public { + vm.expectRevert("Not authorized"); + stakeContract.setRewardsPerUnitTime(1); + } + + function test_state_setTimeUnit() public { + // check current value + assertEq(timeUnit, stakeContract.getTimeUnit()); + + // set new value and check + uint256 newTimeUnit = 2 minutes; + vm.prank(deployer); + stakeContract.setTimeUnit(newTimeUnit); + assertEq(newTimeUnit, stakeContract.getTimeUnit()); + + //================ stake tokens + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](3); + _tokenIdsOne[0] = 0; + _tokenIdsOne[1] = 1; + _tokenIdsOne[2] = 2; + + // stake 3 tokens + vm.prank(stakerOne); + stakeContract.stake(_tokenIdsOne); + uint256 timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set rewardsPerUnitTime + vm.roll(100); + vm.warp(1000); + + vm.prank(deployer); + stakeContract.setTimeUnit(1 seconds); + assertEq(1 seconds, stakeContract.getTimeUnit()); + uint256 newTimeOfLastUpdate = block.timestamp; + + // check available rewards -- should use previous value for rewardsPerUnitTime for calculation + (, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * _tokenIdsOne.length) * rewardsPerUnitTime) / newTimeUnit) + ); + + //====== check rewards after some time + vm.roll(300); + vm.warp(3000); + + (, uint256 _newRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _newRewards, + _availableRewards + + ((((block.timestamp - newTimeOfLastUpdate) * _tokenIdsOne.length) * rewardsPerUnitTime) / (1 seconds)) + ); + } + + function test_revert_setTimeUnit_notAuthorized() public { + vm.expectRevert("Not authorized"); + stakeContract.setTimeUnit(1); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: withdraw + //////////////////////////////////////////////////////////////*/ + + function test_state_withdraw() public { + //================ first staker ====================== + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](3); + _tokenIdsOne[0] = 0; + _tokenIdsOne[1] = 1; + _tokenIdsOne[2] = 2; + + // stake 3 tokens + vm.prank(stakerOne); + stakeContract.stake(_tokenIdsOne); + uint256 timeOfLastUpdate = block.timestamp; + + // check balances/ownership of staked tokens + for (uint256 i = 0; i < _tokenIdsOne.length; i++) { + assertEq(erc721.ownerOf(_tokenIdsOne[i]), address(stakeContract)); + assertEq(stakeContract.stakerAddress(_tokenIdsOne[i]), stakerOne); + } + assertEq(erc721.balanceOf(stakerOne), 2); + assertEq(erc721.balanceOf(address(stakeContract)), _tokenIdsOne.length); + + // check available rewards right after staking + (uint256[] memory _amountStaked, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq(_amountStaked.length, _tokenIdsOne.length); + assertEq(_availableRewards, 0); + + console.log("==== staked tokens before withdraw ===="); + for (uint256 i = 0; i < _amountStaked.length; i++) { + console.log(_amountStaked[i]); + } + + //========== warp timestamp before withdraw + vm.roll(100); + vm.warp(1000); + + uint256[] memory _tokensToWithdraw = new uint256[](1); + _tokensToWithdraw[0] = 1; + + vm.prank(stakerOne); + stakeContract.withdraw(_tokensToWithdraw); + + // check balances/ownership after withdraw + for (uint256 i = 0; i < _tokensToWithdraw.length; i++) { + assertEq(erc721.ownerOf(_tokensToWithdraw[i]), stakerOne); + assertEq(stakeContract.stakerAddress(_tokensToWithdraw[i]), address(0)); + } + assertEq(erc721.balanceOf(stakerOne), 3); + assertEq(erc721.balanceOf(address(stakeContract)), 2); + + // check available rewards after withdraw + (_amountStaked, _availableRewards) = stakeContract.getStakeInfo(stakerOne); + assertEq(_availableRewards, ((((block.timestamp - timeOfLastUpdate) * 3) * rewardsPerUnitTime) / timeUnit)); + + console.log("==== staked tokens after withdraw ===="); + for (uint256 i = 0; i < _amountStaked.length; i++) { + console.log(_amountStaked[i]); + } + + uint256 timeOfLastUpdateLatest = block.timestamp; + + // check available rewards some time after withdraw + vm.roll(200); + vm.warp(2000); + + (, _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + (((((timeOfLastUpdateLatest - timeOfLastUpdate) * 3)) * rewardsPerUnitTime) / timeUnit) + + (((((block.timestamp - timeOfLastUpdateLatest) * 2)) * rewardsPerUnitTime) / timeUnit) + ); + + // stake again + vm.prank(stakerOne); + stakeContract.stake(_tokensToWithdraw); + + _tokensToWithdraw[0] = 5; + vm.prank(stakerTwo); + stakeContract.stake(_tokensToWithdraw); + // check available rewards after re-staking + (_amountStaked, ) = stakeContract.getStakeInfo(stakerOne); + + console.log("==== staked tokens after re-staking ===="); + for (uint256 i = 0; i < _amountStaked.length; i++) { + console.log(_amountStaked[i]); + } + } + + function test_revert_withdraw_withdrawingZeroTokens() public { + uint256[] memory _tokensToWithdraw; + + vm.expectRevert("Withdrawing 0 tokens"); + stakeContract.withdraw(_tokensToWithdraw); + } + + function test_revert_withdraw_notStaker() public { + // stake tokens + uint256[] memory _tokenIds = new uint256[](2); + _tokenIds[0] = 0; + _tokenIds[1] = 1; + + vm.prank(stakerOne); + stakeContract.stake(_tokenIds); + + // trying to withdraw zero tokens + uint256[] memory _tokensToWithdraw = new uint256[](1); + _tokensToWithdraw[0] = 2; + + vm.prank(stakerOne); + vm.expectRevert("Not staker"); + stakeContract.withdraw(_tokensToWithdraw); + } + + function test_revert_withdraw_withdrawingMoreThanStaked() public { + // stake tokens + uint256[] memory _tokenIds = new uint256[](1); + _tokenIds[0] = 0; + + vm.prank(stakerOne); + stakeContract.stake(_tokenIds); + + // trying to withdraw tokens not staked by caller + uint256[] memory _tokensToWithdraw = new uint256[](2); + _tokensToWithdraw[0] = 0; + _tokensToWithdraw[1] = 1; + + vm.prank(stakerOne); + vm.expectRevert("Withdrawing more than staked"); + stakeContract.withdraw(_tokensToWithdraw); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function test_revert_zeroTimeUnit_adminLockTokens() public { + //================ stake tokens + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](1); + uint256[] memory _tokenIdsTwo = new uint256[](1); + _tokenIdsOne[0] = 0; + _tokenIdsTwo[0] = 5; + + // Two different users stake 1 tokens each + vm.prank(stakerOne); + stakeContract.stake(_tokenIdsOne); + vm.prank(stakerTwo); + stakeContract.stake(_tokenIdsTwo); + + // set timeUnit to zero + uint256 newTimeUnit = 0; + vm.prank(deployer); + vm.expectRevert("time-unit can't be 0"); + stakeContract.setTimeUnit(newTimeUnit); + + // stakerOne and stakerTwo can withdraw their tokens + // vm.expectRevert(stdError.divisionError); + vm.prank(stakerOne); + stakeContract.withdraw(_tokenIdsOne); + + // vm.expectRevert(stdError.divisionError); + vm.prank(stakerTwo); + stakeContract.withdraw(_tokenIdsTwo); + } + + function test_revert_largeRewardsPerUnitTime_adminRewardsLock() public { + //================ stake tokens + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](1); + uint256[] memory _tokenIdsTwo = new uint256[](1); + + uint256 stakerOneToken = erc721.nextTokenIdToMint(); + erc721.mint(stakerOne, 5); // mint token id 0 to 4 + uint256 stakerTwoToken = erc721.nextTokenIdToMint(); + erc721.mint(stakerTwo, 5); // mint token id 5 to 9 + _tokenIdsOne[0] = stakerOneToken; + _tokenIdsTwo[0] = stakerTwoToken; + + // Two users stake 1 tokens each + vm.prank(stakerOne); + stakeContract.stake(_tokenIdsOne); + vm.prank(stakerTwo); + stakeContract.stake(_tokenIdsTwo); + + // set rewardsPerTimeUnit to max value + uint256 rewardsPerTimeUnit = type(uint256).max; + vm.prank(deployer); + stakeContract.setRewardsPerUnitTime(rewardsPerTimeUnit); + + vm.warp(1 days); + + // stakerOne and stakerTwo can't withdraw their tokens + // vm.expectRevert(stdError.arithmeticError); + vm.prank(stakerOne); + stakeContract.withdraw(_tokenIdsOne); + + // vm.expectRevert(stdError.arithmeticError); + vm.prank(stakerTwo); + stakeContract.withdraw(_tokenIdsTwo); + + // rewardsPerTimeUnit can't be changed + rewardsPerTimeUnit = 60; + // vm.expectRevert(stdError.arithmeticError); + vm.prank(deployer); + stakeContract.setRewardsPerUnitTime(rewardsPerTimeUnit); + } + + function test_Macro_NFTDirectSafeTransferLocksToken() public { + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + + // stakerOne mistakenly safe-transfers direct to the staking contract + vm.prank(stakerOne); + vm.expectRevert("Direct transfer"); + erc721.safeTransferFrom(stakerOne, address(stakeContract), tokenIds[0]); + + // show that the transferred token was not properly staked + // (uint256[] memory tokensStaked, uint256 rewards) = stakeContract.getStakeInfo(stakerOne); + // assertEq(0, tokensStaked.length); + + // // show that stakerOne cannot recover the token + // vm.expectRevert(); + // vm.prank(stakerOne); + // stakeContract.withdraw(tokenIds); + } +} diff --git a/src/test/staking/NFTStake_EthReward.t.sol b/src/test/staking/NFTStake_EthReward.t.sol new file mode 100644 index 000000000..459f111e5 --- /dev/null +++ b/src/test/staking/NFTStake_EthReward.t.sol @@ -0,0 +1,592 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { NFTStake } from "contracts/prebuilts/staking/NFTStake.sol"; + +// Test imports + +import "../utils/BaseTest.sol"; + +contract NFTStakeEthRewardTest is BaseTest { + NFTStake internal stakeContract; + + address internal stakerOne; + address internal stakerTwo; + + uint256 internal timeUnit; + uint256 internal rewardsPerUnitTime; + + function setUp() public override { + super.setUp(); + + timeUnit = 60; + rewardsPerUnitTime = 1; + + stakerOne = address(0x345); + stakerTwo = address(0x567); + + erc721.mint(stakerOne, 5); // mint token id 0 to 4 + erc721.mint(stakerTwo, 5); // mint token id 5 to 9 + vm.deal(deployer, 1000 ether); // mint reward tokens (Eth) to contract admin + + stakeContract = NFTStake( + payable( + deployContractProxy( + "NFTStake", + abi.encodeCall( + NFTStake.initialize, + (deployer, CONTRACT_URI, forwarders(), NATIVE_TOKEN, address(erc721), 60, 1) + ) + ) + ) + ); + + // set approvals + vm.prank(stakerOne); + erc721.setApprovalForAll(address(stakeContract), true); + + vm.prank(stakerTwo); + erc721.setApprovalForAll(address(stakeContract), true); + + vm.startPrank(deployer); + stakeContract.depositRewardTokens{ value: 100 ether }(100 ether); + // erc20.transfer(address(stakeContract), 100 ether); + vm.stopPrank(); + assertEq(stakeContract.getRewardTokenBalance(), 100 ether); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: Stake + //////////////////////////////////////////////////////////////*/ + + function test_state_stake() public { + //================ first staker ====================== + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](3); + _tokenIdsOne[0] = 0; + _tokenIdsOne[1] = 1; + _tokenIdsOne[2] = 2; + + // stake 3 tokens + vm.prank(stakerOne); + stakeContract.stake(_tokenIdsOne); + uint256 timeOfLastUpdate_one = block.timestamp; + + // check balances/ownership of staked tokens + for (uint256 i = 0; i < _tokenIdsOne.length; i++) { + assertEq(erc721.ownerOf(_tokenIdsOne[i]), address(stakeContract)); + assertEq(stakeContract.stakerAddress(_tokenIdsOne[i]), stakerOne); + } + assertEq(erc721.balanceOf(stakerOne), 2); + assertEq(erc721.balanceOf(address(stakeContract)), _tokenIdsOne.length); + + // check available rewards right after staking + (uint256[] memory _amountStaked, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq(_amountStaked.length, _tokenIdsOne.length); + assertEq(_availableRewards, 0); + + //=================== warp timestamp to calculate rewards + vm.roll(100); + vm.warp(1000); + + // check available rewards after warp + (, _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate_one) * _tokenIdsOne.length) * rewardsPerUnitTime) / timeUnit) + ); + + //================ second staker ====================== + vm.roll(200); + vm.warp(2000); + uint256[] memory _tokenIdsTwo = new uint256[](2); + _tokenIdsTwo[0] = 5; + _tokenIdsTwo[1] = 6; + + // stake 2 tokens + vm.prank(stakerTwo); + stakeContract.stake(_tokenIdsTwo); + uint256 timeOfLastUpdate_two = block.timestamp; + + // check balances/ownership of staked tokens + for (uint256 i = 0; i < _tokenIdsTwo.length; i++) { + assertEq(erc721.ownerOf(_tokenIdsTwo[i]), address(stakeContract)); + assertEq(stakeContract.stakerAddress(_tokenIdsTwo[i]), stakerTwo); + } + assertEq(erc721.balanceOf(stakerTwo), 3); + assertEq(erc721.balanceOf(address(stakeContract)), _tokenIdsTwo.length + _tokenIdsOne.length); + + // check available rewards right after staking + (_amountStaked, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + + assertEq(_amountStaked.length, _tokenIdsTwo.length); + assertEq(_availableRewards, 0); + + //=================== warp timestamp to calculate rewards + vm.roll(300); + vm.warp(3000); + + // check available rewards for stakerOne + (, _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate_one) * _tokenIdsOne.length) * rewardsPerUnitTime) / timeUnit) + ); + + // check available rewards for stakerTwo + (, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate_two) * _tokenIdsTwo.length) * rewardsPerUnitTime) / timeUnit) + ); + } + + function test_revert_stake_stakingZeroTokens() public { + // stake 0 tokens + uint256[] memory _tokenIds; + + vm.prank(stakerOne); + vm.expectRevert("Staking 0 tokens"); + stakeContract.stake(_tokenIds); + } + + function test_revert_stake_notStaker() public { + // stake unowned tokens + uint256[] memory _tokenIds = new uint256[](1); + _tokenIds[0] = 6; + + vm.prank(stakerOne); + vm.expectRevert("ERC721: transfer from incorrect owner"); + stakeContract.stake(_tokenIds); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: claimRewards + //////////////////////////////////////////////////////////////*/ + + function test_state_claimRewards() public { + //================ first staker ====================== + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](3); + _tokenIdsOne[0] = 0; + _tokenIdsOne[1] = 1; + _tokenIdsOne[2] = 2; + + // stake 3 tokens + vm.prank(stakerOne); + stakeContract.stake(_tokenIdsOne); + uint256 timeOfLastUpdate_one = block.timestamp; + + //=================== warp timestamp to claim rewards + vm.roll(100); + vm.warp(1000); + + uint256 rewardBalanceBefore = stakeContract.getRewardTokenBalance(); + vm.prank(stakerOne); + stakeContract.claimRewards(); + uint256 rewardBalanceAfter = stakeContract.getRewardTokenBalance(); + + // check reward balances + assertEq( + stakerOne.balance, + ((((block.timestamp - timeOfLastUpdate_one) * _tokenIdsOne.length) * rewardsPerUnitTime) / timeUnit) + ); + assertEq( + rewardBalanceAfter, + rewardBalanceBefore - + ((((block.timestamp - timeOfLastUpdate_one) * _tokenIdsOne.length) * rewardsPerUnitTime) / timeUnit) + ); + + // check available rewards after claiming + (uint256[] memory _amountStaked, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq(_amountStaked.length, _tokenIdsOne.length); + assertEq(_availableRewards, 0); + } + + function test_revert_claimRewards_noRewards() public { + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](3); + _tokenIdsOne[0] = 0; + _tokenIdsOne[1] = 1; + _tokenIdsOne[2] = 2; + + // stake 3 tokens + vm.prank(stakerOne); + stakeContract.stake(_tokenIdsOne); + + //=================== try to claim rewards in same block + + vm.prank(stakerOne); + vm.expectRevert("No rewards"); + stakeContract.claimRewards(); + + //======= withdraw tokens and claim rewards + vm.roll(100); + vm.warp(1000); + + vm.prank(stakerOne); + stakeContract.withdraw(_tokenIdsOne); + vm.prank(stakerOne); + stakeContract.claimRewards(); + + //===== try to claim rewards again + vm.roll(200); + vm.warp(2000); + vm.prank(stakerOne); + vm.expectRevert("No rewards"); + stakeContract.claimRewards(); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: stake conditions + //////////////////////////////////////////////////////////////*/ + + function test_state_setRewardsPerUnitTime() public { + // check current value + assertEq(rewardsPerUnitTime, stakeContract.getRewardsPerUnitTime()); + + // set new value and check + uint256 newRewardsPerUnitTime = 50; + vm.prank(deployer); + stakeContract.setRewardsPerUnitTime(newRewardsPerUnitTime); + assertEq(newRewardsPerUnitTime, stakeContract.getRewardsPerUnitTime()); + + //================ stake tokens + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](3); + _tokenIdsOne[0] = 0; + _tokenIdsOne[1] = 1; + _tokenIdsOne[2] = 2; + + // stake 3 tokens + vm.prank(stakerOne); + stakeContract.stake(_tokenIdsOne); + uint256 timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set rewardsPerUnitTime + vm.roll(100); + vm.warp(1000); + + vm.prank(deployer); + stakeContract.setRewardsPerUnitTime(200); + assertEq(200, stakeContract.getRewardsPerUnitTime()); + uint256 newTimeOfLastUpdate = block.timestamp; + + // check available rewards -- should use previous value for rewardsPerUnitTime for calculation + (, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * _tokenIdsOne.length) * newRewardsPerUnitTime) / timeUnit) + ); + + //====== check rewards after some time + vm.roll(300); + vm.warp(3000); + + (, uint256 _newRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _newRewards, + _availableRewards + ((((block.timestamp - newTimeOfLastUpdate) * _tokenIdsOne.length) * 200) / timeUnit) + ); + } + + function test_revert_setRewardsPerUnitTime_notAuthorized() public { + vm.expectRevert("Not authorized"); + stakeContract.setRewardsPerUnitTime(1); + } + + function test_state_setTimeUnit() public { + // check current value + assertEq(timeUnit, stakeContract.getTimeUnit()); + + // set new value and check + uint256 newTimeUnit = 2 minutes; + vm.prank(deployer); + stakeContract.setTimeUnit(newTimeUnit); + assertEq(newTimeUnit, stakeContract.getTimeUnit()); + + //================ stake tokens + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](3); + _tokenIdsOne[0] = 0; + _tokenIdsOne[1] = 1; + _tokenIdsOne[2] = 2; + + // stake 3 tokens + vm.prank(stakerOne); + stakeContract.stake(_tokenIdsOne); + uint256 timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set rewardsPerUnitTime + vm.roll(100); + vm.warp(1000); + + vm.prank(deployer); + stakeContract.setTimeUnit(1 seconds); + assertEq(1 seconds, stakeContract.getTimeUnit()); + uint256 newTimeOfLastUpdate = block.timestamp; + + // check available rewards -- should use previous value for rewardsPerUnitTime for calculation + (, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + ((((block.timestamp - timeOfLastUpdate) * _tokenIdsOne.length) * rewardsPerUnitTime) / newTimeUnit) + ); + + //====== check rewards after some time + vm.roll(300); + vm.warp(3000); + + (, uint256 _newRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _newRewards, + _availableRewards + + ((((block.timestamp - newTimeOfLastUpdate) * _tokenIdsOne.length) * rewardsPerUnitTime) / (1 seconds)) + ); + } + + function test_revert_setTimeUnit_notAuthorized() public { + vm.expectRevert("Not authorized"); + stakeContract.setTimeUnit(1); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: withdraw + //////////////////////////////////////////////////////////////*/ + + function test_state_withdraw() public { + //================ first staker ====================== + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](3); + _tokenIdsOne[0] = 0; + _tokenIdsOne[1] = 1; + _tokenIdsOne[2] = 2; + + // stake 3 tokens + vm.prank(stakerOne); + stakeContract.stake(_tokenIdsOne); + uint256 timeOfLastUpdate = block.timestamp; + + // check balances/ownership of staked tokens + for (uint256 i = 0; i < _tokenIdsOne.length; i++) { + assertEq(erc721.ownerOf(_tokenIdsOne[i]), address(stakeContract)); + assertEq(stakeContract.stakerAddress(_tokenIdsOne[i]), stakerOne); + } + assertEq(erc721.balanceOf(stakerOne), 2); + assertEq(erc721.balanceOf(address(stakeContract)), _tokenIdsOne.length); + + // check available rewards right after staking + (uint256[] memory _amountStaked, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq(_amountStaked.length, _tokenIdsOne.length); + assertEq(_availableRewards, 0); + + console.log("==== staked tokens before withdraw ===="); + for (uint256 i = 0; i < _amountStaked.length; i++) { + console.log(_amountStaked[i]); + } + + //========== warp timestamp before withdraw + vm.roll(100); + vm.warp(1000); + + uint256[] memory _tokensToWithdraw = new uint256[](1); + _tokensToWithdraw[0] = 1; + + vm.prank(stakerOne); + stakeContract.withdraw(_tokensToWithdraw); + + // check balances/ownership after withdraw + for (uint256 i = 0; i < _tokensToWithdraw.length; i++) { + assertEq(erc721.ownerOf(_tokensToWithdraw[i]), stakerOne); + assertEq(stakeContract.stakerAddress(_tokensToWithdraw[i]), address(0)); + } + assertEq(erc721.balanceOf(stakerOne), 3); + assertEq(erc721.balanceOf(address(stakeContract)), 2); + + // check available rewards after withdraw + (_amountStaked, _availableRewards) = stakeContract.getStakeInfo(stakerOne); + assertEq(_availableRewards, ((((block.timestamp - timeOfLastUpdate) * 3) * rewardsPerUnitTime) / timeUnit)); + + console.log("==== staked tokens after withdraw ===="); + for (uint256 i = 0; i < _amountStaked.length; i++) { + console.log(_amountStaked[i]); + } + + uint256 timeOfLastUpdateLatest = block.timestamp; + + // check available rewards some time after withdraw + vm.roll(200); + vm.warp(2000); + + (, _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + (((((timeOfLastUpdateLatest - timeOfLastUpdate) * 3)) * rewardsPerUnitTime) / timeUnit) + + (((((block.timestamp - timeOfLastUpdateLatest) * 2)) * rewardsPerUnitTime) / timeUnit) + ); + + // stake again + vm.prank(stakerOne); + stakeContract.stake(_tokensToWithdraw); + + _tokensToWithdraw[0] = 5; + vm.prank(stakerTwo); + stakeContract.stake(_tokensToWithdraw); + // check available rewards after re-staking + (_amountStaked, ) = stakeContract.getStakeInfo(stakerOne); + + console.log("==== staked tokens after re-staking ===="); + for (uint256 i = 0; i < _amountStaked.length; i++) { + console.log(_amountStaked[i]); + } + } + + function test_revert_withdraw_withdrawingZeroTokens() public { + uint256[] memory _tokensToWithdraw; + + vm.expectRevert("Withdrawing 0 tokens"); + stakeContract.withdraw(_tokensToWithdraw); + } + + function test_revert_withdraw_notStaker() public { + // stake tokens + uint256[] memory _tokenIds = new uint256[](2); + _tokenIds[0] = 0; + _tokenIds[1] = 1; + + vm.prank(stakerOne); + stakeContract.stake(_tokenIds); + + // trying to withdraw zero tokens + uint256[] memory _tokensToWithdraw = new uint256[](1); + _tokensToWithdraw[0] = 2; + + vm.prank(stakerOne); + vm.expectRevert("Not staker"); + stakeContract.withdraw(_tokensToWithdraw); + } + + function test_revert_withdraw_withdrawingMoreThanStaked() public { + // stake tokens + uint256[] memory _tokenIds = new uint256[](1); + _tokenIds[0] = 0; + + vm.prank(stakerOne); + stakeContract.stake(_tokenIds); + + // trying to withdraw tokens not staked by caller + uint256[] memory _tokensToWithdraw = new uint256[](2); + _tokensToWithdraw[0] = 0; + _tokensToWithdraw[1] = 1; + + vm.prank(stakerOne); + vm.expectRevert("Withdrawing more than staked"); + stakeContract.withdraw(_tokensToWithdraw); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function test_revert_zeroTimeUnit_adminLockTokens() public { + //================ stake tokens + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](1); + uint256[] memory _tokenIdsTwo = new uint256[](1); + _tokenIdsOne[0] = 0; + _tokenIdsTwo[0] = 5; + + // Two different users stake 1 tokens each + vm.prank(stakerOne); + stakeContract.stake(_tokenIdsOne); + vm.prank(stakerTwo); + stakeContract.stake(_tokenIdsTwo); + + // set timeUnit to zero + uint256 newTimeUnit = 0; + vm.prank(deployer); + vm.expectRevert("time-unit can't be 0"); + stakeContract.setTimeUnit(newTimeUnit); + + // stakerOne and stakerTwo can withdraw their tokens + // vm.expectRevert(stdError.divisionError); + vm.prank(stakerOne); + stakeContract.withdraw(_tokenIdsOne); + + // vm.expectRevert(stdError.divisionError); + vm.prank(stakerTwo); + stakeContract.withdraw(_tokenIdsTwo); + } + + function test_revert_largeRewardsPerUnitTime_adminRewardsLock() public { + //================ stake tokens + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](1); + uint256[] memory _tokenIdsTwo = new uint256[](1); + + uint256 stakerOneToken = erc721.nextTokenIdToMint(); + erc721.mint(stakerOne, 5); // mint token id 0 to 4 + uint256 stakerTwoToken = erc721.nextTokenIdToMint(); + erc721.mint(stakerTwo, 5); // mint token id 5 to 9 + _tokenIdsOne[0] = stakerOneToken; + _tokenIdsTwo[0] = stakerTwoToken; + + // Two users stake 1 tokens each + vm.prank(stakerOne); + stakeContract.stake(_tokenIdsOne); + vm.prank(stakerTwo); + stakeContract.stake(_tokenIdsTwo); + + // set rewardsPerTimeUnit to max value + uint256 rewardsPerTimeUnit = type(uint256).max; + vm.prank(deployer); + stakeContract.setRewardsPerUnitTime(rewardsPerTimeUnit); + + vm.warp(1 days); + + // stakerOne and stakerTwo can't withdraw their tokens + // vm.expectRevert(stdError.arithmeticError); + vm.prank(stakerOne); + stakeContract.withdraw(_tokenIdsOne); + + // vm.expectRevert(stdError.arithmeticError); + vm.prank(stakerTwo); + stakeContract.withdraw(_tokenIdsTwo); + + // rewardsPerTimeUnit can't be changed + rewardsPerTimeUnit = 60; + // vm.expectRevert(stdError.arithmeticError); + vm.prank(deployer); + stakeContract.setRewardsPerUnitTime(rewardsPerTimeUnit); + } + + function test_Macro_NFTDirectSafeTransferLocksToken() public { + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + + // stakerOne mistakenly safe-transfers direct to the staking contract + vm.prank(stakerOne); + vm.expectRevert("Direct transfer"); + erc721.safeTransferFrom(stakerOne, address(stakeContract), tokenIds[0]); + + // show that the transferred token was not properly staked + // (uint256[] memory tokensStaked, uint256 rewards) = stakeContract.getStakeInfo(stakerOne); + // assertEq(0, tokensStaked.length); + + // // show that stakerOne cannot recover the token + // vm.expectRevert(); + // vm.prank(stakerOne); + // stakeContract.withdraw(tokenIds); + } +} diff --git a/src/test/staking/TokenStake.t.sol b/src/test/staking/TokenStake.t.sol new file mode 100644 index 000000000..f2a7d63ae --- /dev/null +++ b/src/test/staking/TokenStake.t.sol @@ -0,0 +1,876 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { TokenStake } from "contracts/prebuilts/staking/TokenStake.sol"; + +// Test imports + +import "../utils/BaseTest.sol"; + +contract TokenStakeTest is BaseTest { + TokenStake internal stakeContract; + + address internal stakerOne; + address internal stakerTwo; + + uint80 internal timeUnit; + uint256 internal rewardRatioNumerator; + uint256 internal rewardRatioDenominator; + + function setUp() public override { + super.setUp(); + + timeUnit = 60; + rewardRatioNumerator = 3; + rewardRatioDenominator = 50; + + stakerOne = address(0x345); + stakerTwo = address(0x567); + + erc20Aux.mint(stakerOne, 1000); // mint 1000 tokens to stakerOne + erc20Aux.mint(stakerTwo, 1000); // mint 1000 tokens to stakerTwo + + erc20.mint(deployer, 1000 ether); // mint reward tokens to contract admin + + stakeContract = TokenStake(payable(getContract("TokenStake"))); + + // set approvals + vm.prank(stakerOne); + erc20Aux.approve(address(stakeContract), type(uint256).max); + + vm.prank(stakerTwo); + erc20Aux.approve(address(stakeContract), type(uint256).max); + + vm.startPrank(deployer); + erc20.approve(address(stakeContract), type(uint256).max); + stakeContract.depositRewardTokens(100 ether); + // erc20.transfer(address(stakeContract), 100 ether); + vm.stopPrank(); + assertEq(stakeContract.getRewardTokenBalance(), 100 ether); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: Stake + //////////////////////////////////////////////////////////////*/ + + function test_state_stake() public { + //================ first staker ====================== + vm.warp(1); + + // stake 400 tokens + vm.prank(stakerOne); + stakeContract.stake(400); + uint256 timeOfLastUpdate_one = block.timestamp; + + // check balances/ownership of staked tokens + assertEq(erc20Aux.balanceOf(address(stakeContract)), 400); + assertEq(erc20Aux.balanceOf(address(stakerOne)), 600); + + // check available rewards right after staking + (uint256 _amountStaked, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq(_amountStaked, 400); + assertEq(_availableRewards, 0); + + //=================== warp timestamp to calculate rewards + vm.roll(100); + vm.warp(1000); + + // check available rewards after warp + (, _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate_one) * 400) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + //================ second staker ====================== + vm.roll(200); + vm.warp(2000); + + // stake 20 tokens with token-id 0 + vm.prank(stakerTwo); + stakeContract.stake(200); + uint256 timeOfLastUpdate_two = block.timestamp; + + // check balances/ownership of staked tokens + assertEq(erc20Aux.balanceOf(address(stakeContract)), 200 + 400); // sum of staked tokens by both stakers + assertEq(erc20Aux.balanceOf(address(stakerTwo)), 800); + + // check available rewards right after staking + (_amountStaked, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + + assertEq(_amountStaked, 200); + assertEq(_availableRewards, 0); + + //=================== warp timestamp to calculate rewards + vm.roll(300); + vm.warp(3000); + + // check available rewards for stakerOne + (, _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate_one) * 400) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + // check available rewards for stakerTwo + (, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate_two) * 200) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + } + + function test_revert_stake_stakingZeroTokens() public { + // stake 0 tokens + + vm.prank(stakerOne); + vm.expectRevert("Staking 0 tokens"); + stakeContract.stake(0); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: claimRewards + //////////////////////////////////////////////////////////////*/ + + function test_state_claimRewards() public { + //================ setup stakerOne ====================== + vm.warp(1); + + // stake 50 tokens with token-id 0 + vm.prank(stakerOne); + stakeContract.stake(400); + uint256 timeOfLastUpdate_one = block.timestamp; + + //=================== warp timestamp to claim rewards + vm.roll(100); + vm.warp(1000); + + uint256 rewardBalanceBefore = stakeContract.getRewardTokenBalance(); + vm.prank(stakerOne); + stakeContract.claimRewards(); + uint256 rewardBalanceAfter = stakeContract.getRewardTokenBalance(); + + // check reward balances + assertEq( + erc20.balanceOf(stakerOne), + (((((block.timestamp - timeOfLastUpdate_one) * 400) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + assertEq( + rewardBalanceAfter, + rewardBalanceBefore - + (((((block.timestamp - timeOfLastUpdate_one) * 400) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + // check available rewards after claiming + (uint256 _amountStaked, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq(_amountStaked, 400); + assertEq(_availableRewards, 0); + + //================ setup stakerTwo ====================== + + // stake 20 tokens with token-id 1 + vm.prank(stakerTwo); + stakeContract.stake(200); + uint256 timeOfLastUpdate_two = block.timestamp; + + //=================== warp timestamp to claim rewards + vm.roll(200); + vm.warp(2000); + + rewardBalanceBefore = stakeContract.getRewardTokenBalance(); + vm.prank(stakerTwo); + stakeContract.claimRewards(); + rewardBalanceAfter = stakeContract.getRewardTokenBalance(); + + // check reward balances + assertEq( + erc20.balanceOf(stakerTwo), + (((((block.timestamp - timeOfLastUpdate_two) * 200) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + assertEq( + rewardBalanceAfter, + rewardBalanceBefore - + (((((block.timestamp - timeOfLastUpdate_two) * 200) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + // check available rewards after claiming -- stakerTwo + (_amountStaked, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + assertEq(_amountStaked, 200); + assertEq(_availableRewards, 0); + + // check available rewards -- stakerOne + (_amountStaked, _availableRewards) = stakeContract.getStakeInfo(stakerOne); + assertEq(_amountStaked, 400); + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate_two) * 400) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + } + + function test_revert_claimRewards_noRewards() public { + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(400); + + //=================== try to claim rewards in same block + + vm.prank(stakerOne); + vm.expectRevert("No rewards"); + stakeContract.claimRewards(); + + //======= withdraw tokens and claim rewards + vm.roll(100); + vm.warp(1000); + + vm.prank(stakerOne); + stakeContract.withdraw(400); + vm.prank(stakerOne); + stakeContract.claimRewards(); + + //===== try to claim rewards again + vm.roll(200); + vm.warp(2000); + vm.prank(stakerOne); + vm.expectRevert("No rewards"); + stakeContract.claimRewards(); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: stake conditions + //////////////////////////////////////////////////////////////*/ + + function test_state_setRewardRatio() public { + // set value and check + vm.prank(deployer); + stakeContract.setRewardRatio(3, 70); + (uint256 numerator, uint256 denominator) = stakeContract.getRewardRatio(); + assertEq(3, numerator); + assertEq(70, denominator); + + //================ stake tokens + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(400); + uint256 timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set rewardsPerUnitTime + vm.roll(100); + vm.warp(1000); + + vm.prank(deployer); + stakeContract.setRewardRatio(3, 80); + (numerator, denominator) = stakeContract.getRewardRatio(); + assertEq(3, numerator); + assertEq(80, denominator); + uint256 newTimeOfLastUpdate = block.timestamp; + + // check available rewards -- should use previous value for rewardsPerUnitTime for calculation + (, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq(_availableRewards, (((((block.timestamp - timeOfLastUpdate) * 400) * 3) / timeUnit) / 70)); + + //====== check rewards after some time + vm.roll(300); + vm.warp(3000); + + (, uint256 _newRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _newRewards, + _availableRewards + (((((block.timestamp - newTimeOfLastUpdate) * 400) * 3) / timeUnit) / 80) + ); + } + + function test_state_setTimeUnit() public { + // set value and check + uint80 timeUnitToSet = 100; + vm.prank(deployer); + stakeContract.setTimeUnit(timeUnitToSet); + assertEq(timeUnitToSet, stakeContract.getTimeUnit()); + + //================ stake tokens + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(400); + uint256 timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set timeUnit + vm.roll(100); + vm.warp(1000); + + vm.prank(deployer); + stakeContract.setTimeUnit(200); + assertEq(200, stakeContract.getTimeUnit()); + uint256 newTimeOfLastUpdate = block.timestamp; + + // check available rewards -- should use previous value for timeUnit for calculation + (, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate) * 400) * rewardRatioNumerator) / timeUnitToSet) / + rewardRatioDenominator) + ); + + //====== check rewards after some time + vm.roll(300); + vm.warp(3000); + + (, uint256 _newRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _newRewards, + _availableRewards + + (((((block.timestamp - newTimeOfLastUpdate) * 400) * rewardRatioNumerator) / 200) / + rewardRatioDenominator) + ); + } + + function test_revert_setRewardRatio_notAuthorized() public { + vm.expectRevert("Not authorized"); + stakeContract.setRewardRatio(1, 2); + } + + function test_revert_setRewardRatio_divideByZero() public { + vm.prank(deployer); + vm.expectRevert("divide by 0"); + stakeContract.setRewardRatio(1, 0); + } + + function test_revert_setTimeUnit_notAuthorized() public { + vm.expectRevert("Not authorized"); + stakeContract.setTimeUnit(1); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: withdraw + //////////////////////////////////////////////////////////////*/ + + function test_state_withdraw() public { + //================ stake different tokens ====================== + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(400); + + vm.prank(stakerTwo); + stakeContract.stake(200); + + uint256 timeOfLastUpdate = block.timestamp; + + //========== warp timestamp before withdraw + vm.roll(100); + vm.warp(1000); + + // withdraw partially for stakerOne + vm.prank(stakerOne); + stakeContract.withdraw(100); + uint256 timeOfLastUpdateLatest = block.timestamp; + + // check balances/ownership after withdraw + assertEq(erc20Aux.balanceOf(stakerOne), 700); + assertEq(erc20Aux.balanceOf(stakerTwo), 800); + assertEq(erc20Aux.balanceOf(address(stakeContract)), (400 - 100) + 200); + + // check available rewards after withdraw + (, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate) * 400) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + (, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate) * 200) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + // check available rewards some time after withdraw + vm.roll(200); + vm.warp(2000); + + // check rewards for stakerOne + (, _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + ((((((timeOfLastUpdateLatest - timeOfLastUpdate) * 400)) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + + ((((((block.timestamp - timeOfLastUpdateLatest) * 300)) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + // withdraw partially for stakerTwo + vm.prank(stakerTwo); + stakeContract.withdraw(100); + timeOfLastUpdateLatest = block.timestamp; + + // check balances/ownership after withdraw + assertEq(erc20Aux.balanceOf(stakerOne), 700); + assertEq(erc20Aux.balanceOf(stakerTwo), 900); + assertEq(erc20Aux.balanceOf(address(stakeContract)), (400 - 100) + (200 - 100)); + + // check rewards for stakerTwo + (, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + + assertEq( + _availableRewards, + ((((((block.timestamp - timeOfLastUpdate) * 200)) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + // check rewards for stakerTwo after some time + vm.roll(300); + vm.warp(3000); + (, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + + assertEq( + _availableRewards, + ((((((timeOfLastUpdateLatest - timeOfLastUpdate) * 200)) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + + ((((((block.timestamp - timeOfLastUpdateLatest) * 100)) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + } + + function test_revert_withdraw_withdrawingZeroTokens() public { + vm.expectRevert("Withdrawing 0 tokens"); + stakeContract.withdraw(0); + } + + function test_revert_withdraw_withdrawingMoreThanStaked() public { + // stake tokens + vm.prank(stakerOne); + stakeContract.stake(400); + + vm.prank(stakerTwo); + stakeContract.stake(200); + + // trying to withdraw more than staked + vm.roll(200); + vm.warp(2000); + + vm.prank(stakerOne); + vm.expectRevert("Withdrawing more than staked"); + stakeContract.withdraw(500); + + // withdraw partially + vm.prank(stakerOne); + stakeContract.withdraw(300); + + // trying to withdraw more than staked + vm.prank(stakerOne); + vm.expectRevert("Withdrawing more than staked"); + stakeContract.withdraw(500); + + // re-stake + vm.prank(stakerOne); + stakeContract.stake(300); + + // trying to withdraw more than staked + vm.prank(stakerOne); + vm.expectRevert("Withdrawing more than staked"); + stakeContract.withdraw(500); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function test_revert_zeroTimeUnit_adminLockTokens() public { + //================ stake tokens + vm.warp(1); + + // User stakes tokens + vm.prank(stakerOne); + stakeContract.stake(400); + + // set timeUnit to zero + uint80 newTimeUnit = 0; + vm.prank(deployer); + vm.expectRevert("time-unit can't be 0"); + stakeContract.setTimeUnit(newTimeUnit); + + // stakerOne and stakerTwo can withdraw their tokens + // vm.expectRevert(stdError.divisionError); + vm.prank(stakerOne); + stakeContract.withdraw(400); + } +} + +contract MockERC20Decimals is MockERC20 { + uint8 private immutable DECIMALS; + + constructor(uint8 _decimals) MockERC20() { + DECIMALS = _decimals; + } + + function decimals() public view virtual override returns (uint8) { + return DECIMALS; + } +} + +// Test scenario where reward token has 6 decimals and staking token has 18 +contract Macro_TokenStake_Rewards6_Staking18_Test is BaseTest { + MockERC20Decimals public erc20_reward6; + MockERC20Decimals public erc20_staking18; + + TokenStake internal stakeContract_reward6_staking18; + + address internal stakerOne; + + uint80 internal timeUnit; + uint256 internal rewardRatioNumerator; + uint256 internal rewardRatioDenominator; + + function setUp() public override { + super.setUp(); + + erc20_reward6 = new MockERC20Decimals(6); + erc20_staking18 = new MockERC20Decimals(18); + + // every 60s earns 1 reward token per 2 tokens staked + timeUnit = 60; + rewardRatioNumerator = 1; + rewardRatioDenominator = 2; + + deployContractProxy( + "TokenStake", + abi.encodeCall( + TokenStake.initialize, + ( + deployer, + CONTRACT_URI, + forwarders(), + address(erc20_reward6), + address(erc20_staking18), + timeUnit, + rewardRatioNumerator, + rewardRatioDenominator + ) + ) + ); + + stakeContract_reward6_staking18 = TokenStake(payable(getContract("TokenStake"))); + + stakerOne = address(0x345); + + // mint 1000 tokens to stakerOne + erc20_staking18.mint(stakerOne, 1000e18); + + // mint 1000 reward tokens to contract admin + erc20_reward6.mint(deployer, 1000e6); + + // set approvals + vm.prank(stakerOne); + erc20_staking18.approve(address(stakeContract_reward6_staking18), type(uint256).max); + + // transfer 100 reward tokens + vm.startPrank(deployer); + erc20_reward6.approve(address(stakeContract_reward6_staking18), type(uint256).max); + // erc20_reward6.transfer(address(stakeContract_reward6_staking18), 100e6); + stakeContract_reward6_staking18.depositRewardTokens(100e6); + vm.stopPrank(); + } + + //===== Reward Token 6 Decimals, Staking Token 18 Decimals =====// + function test_Macro_reward6_staking18() public { + vm.warp(1); + + // stake 400 tokens + vm.prank(stakerOne); + stakeContract_reward6_staking18.stake(400e18); + uint256 timeOfLastUpdate = block.timestamp; + + // check balances/ownership of staked tokens + assertEq(erc20_staking18.balanceOf(address(stakeContract_reward6_staking18)), 400e18); + assertEq(erc20_staking18.balanceOf(address(stakerOne)), 600e18); + + // check available rewards right after staking + (uint256 _amountStaked, uint256 _availableRewards) = stakeContract_reward6_staking18.getStakeInfo(stakerOne); + + assertEq(_amountStaked, 400e18); + assertEq(_availableRewards, 0); + + //=================== warp ahead exactly 1 timeUnit: 60s + vm.roll(4); + vm.warp(61); + assertEq(timeUnit, block.timestamp - timeOfLastUpdate); + + // With 400 tokens staked, we expect 200 reward tokens earned + (, _availableRewards) = stakeContract_reward6_staking18.getStakeInfo(stakerOne); + console2.log("Expect 200 reward tokens. Amount earned: ", _availableRewards / 1e6); + assertEq(_availableRewards, 200e6); + } +} + +// Test scenario where reward token has 18 decimals and staking token has 6 +contract Macro_TokenStake_Rewards18_Staking6_Test is BaseTest { + MockERC20Decimals public erc20_reward18; + MockERC20Decimals public erc20_staking6; + + TokenStake internal stakeContract_reward18_staking6; + + address internal stakerOne; + + uint80 internal timeUnit; + uint256 internal rewardRatioNumerator; + uint256 internal rewardRatioDenominator; + + function setUp() public override { + super.setUp(); + + erc20_reward18 = new MockERC20Decimals(18); + erc20_staking6 = new MockERC20Decimals(6); + + // every 60s earns 1 reward token per 2 tokens staked + timeUnit = 60; + rewardRatioNumerator = 1; + rewardRatioDenominator = 2; + + deployContractProxy( + "TokenStake", + abi.encodeCall( + TokenStake.initialize, + ( + deployer, + CONTRACT_URI, + forwarders(), + address(erc20_reward18), + address(erc20_staking6), + timeUnit, + rewardRatioNumerator, + rewardRatioDenominator + ) + ) + ); + + stakeContract_reward18_staking6 = TokenStake(payable(getContract("TokenStake"))); + + stakerOne = address(0x345); + + // mint 1000 tokens to stakerOne + erc20_staking6.mint(stakerOne, 1000e6); + + // mint 1000 reward tokens to contract admin + erc20_reward18.mint(deployer, 1000e18); + + // set approvals + vm.prank(stakerOne); + erc20_staking6.approve(address(stakeContract_reward18_staking6), type(uint256).max); + + // transfer 100 reward tokens + vm.startPrank(deployer); + erc20_reward18.approve(address(stakeContract_reward18_staking6), type(uint256).max); + // erc20_reward18.transfer(address(stakeContract_reward18_staking6), 100e18); + stakeContract_reward18_staking6.depositRewardTokens(100e18); + vm.stopPrank(); + } + + //===== Reward Token 18 Decimals, Staking Token 6 Decimals =====// + function test_Macro_reward18_staking6() public { + vm.warp(1); + + // stake 400 tokens + vm.prank(stakerOne); + stakeContract_reward18_staking6.stake(400e6); + uint256 timeOfLastUpdate = block.timestamp; + + // check balances/ownership of staked tokens + assertEq(erc20_staking6.balanceOf(address(stakeContract_reward18_staking6)), 400e6); + assertEq(erc20_staking6.balanceOf(address(stakerOne)), 600e6); + + // check available rewards right after staking + (uint256 _amountStaked, uint256 _availableRewards) = stakeContract_reward18_staking6.getStakeInfo(stakerOne); + + assertEq(_amountStaked, 400e6); + assertEq(_availableRewards, 0); + + //=================== warp ahead exactly 1 timeUnit: 60s + vm.roll(4); + vm.warp(61); + assertEq(timeUnit, block.timestamp - timeOfLastUpdate); + + // With 400 tokens staked, we expect 200 reward tokens earned + (, _availableRewards) = stakeContract_reward18_staking6.getStakeInfo(stakerOne); + console2.log("Expect 200 reward tokens. Amount earned: ", _availableRewards / 1e18); + assertEq(_availableRewards, 200e18); + } +} + +contract Macro_TokenStakeTest is BaseTest { + TokenStake internal stakeContract; + + uint80 internal timeUnit; + uint256 internal rewardsPerUnitTime; + uint256 internal rewardRatioNumerator; + uint256 internal rewardRatioDenominator; + uint256 internal tokenAmount = 100; + address internal stakerOne = address(0x345); + address internal stakerTwo = address(0x567); + + function setUp() public override { + super.setUp(); + + timeUnit = 60; + rewardRatioNumerator = 3; + rewardRatioDenominator = 50; + // mint 1000 tokens to stakerOne + erc20Aux.mint(stakerOne, tokenAmount); + // mint 1000 tokens to stakerOne + erc20Aux.mint(stakerTwo, tokenAmount); + // mint reward tokens to contract admin + erc20.mint(deployer, 1000 ether); + + stakeContract = TokenStake(payable(getContract("TokenStake"))); + + // set approvals + vm.prank(stakerOne); + erc20Aux.approve(address(stakeContract), type(uint256).max); + vm.prank(stakerTwo); + erc20Aux.approve(address(stakeContract), type(uint256).max); + + vm.startPrank(deployer); + erc20.approve(address(stakeContract), type(uint256).max); + // erc20.transfer(address(stakeContract), 100 ether); + stakeContract.depositRewardTokens(100 ether); + vm.stopPrank(); + } + + // Demostrate setting unitTime to 0 locks the tokens irreversibly + function testToken_adminLockTokens() public { + //================ stake tokens + vm.warp(1); + + // Two users stake 1 tokens each + vm.prank(stakerOne); + stakeContract.stake(tokenAmount); + vm.prank(stakerTwo); + stakeContract.stake(tokenAmount); + + // set timeUnit to zero + uint80 newTimeUnit = 0; + vm.prank(deployer); + vm.expectRevert("time-unit can't be 0"); + stakeContract.setTimeUnit(newTimeUnit); + } + + function testToken_demostrate_adminRewardsLock() public { + //================ stake tokens + vm.warp(1); + // Two users stake 1 tokens each + vm.prank(stakerOne); + stakeContract.stake(tokenAmount); + vm.prank(stakerTwo); + stakeContract.stake(tokenAmount); + + // set timeUnit to a fraction of uint256 maximum value + uint256 newRewardsPerTimeUnit = type(uint256).max / 100; + vm.prank(deployer); + stakeContract.setRewardRatio(newRewardsPerTimeUnit, 1); + + vm.warp(1 days); + + // stakerOne and stakerTwo can't withdraw their tokens + // vm.expectRevert(stdError.arithmeticError); + vm.prank(stakerOne); + stakeContract.withdraw(tokenAmount); + + // vm.expectRevert(stdError.arithmeticError); + vm.prank(stakerTwo); + stakeContract.withdraw(tokenAmount); + + // rewardRatio can't be changed back + newRewardsPerTimeUnit = 60; + // vm.expectRevert(stdError.arithmeticError); + vm.prank(deployer); + stakeContract.setRewardRatio(newRewardsPerTimeUnit, 1); + } +} + +contract Macro_TokenStake_Tax is BaseTest { + TokenStake internal stakeContract; + uint256 internal tokenAmount = 100 ether; + address internal stakerOne = address(0x345); + address internal stakerTwo = address(0x567); + + function setUp() public override { + super.setUp(); + + stakeContract = TokenStake(payable(getContract("TokenStake"))); + + // mint reward tokens to contract admin + erc20.mint(deployer, tokenAmount); + // mint 100 tokens to stakers + erc20Aux.mint(stakerOne, tokenAmount); + erc20Aux.mint(stakerTwo, tokenAmount); + + // Activate Mock tax + erc20Aux.toggleTax(); + + vm.prank(stakerOne); + erc20Aux.approve(address(stakeContract), type(uint256).max); + + vm.startPrank(deployer); + erc20.approve(address(stakeContract), type(uint256).max); + // erc20.transfer(address(stakeContract), 100 ether); + stakeContract.depositRewardTokens(100 ether); + vm.stopPrank(); + } + + // Demonstrate griefer can drain staked tokens for other users + function testToken_demonstrate_inaccurate_amount() public { + // First user stakes 100 tokens + vm.prank(stakerOne); + stakeContract.stake(tokenAmount); + + // Since there is 10% tax only 90 should be in the contract + uint256 stakingTokenBalance = erc20Aux.balanceOf(address(stakeContract)); + assertEq(stakingTokenBalance, 90 ether); + // Assert the amount was correctly assigned + (uint256 stakingTokenAmount, ) = stakeContract.getStakeInfo(stakerOne); + assertEq(stakingTokenAmount, 90 ether); + + // Users stake and withdraw tokens, draining other users staked balances + // for (uint256 i = 1; i <= 9; i++) { + // address staker = vm.addr(i); + // erc20Aux.mint(staker, tokenAmount); + // vm.startPrank(staker); + // erc20Aux.approve(address(stakeContract), type(uint256).max); + // stakeContract.stake(tokenAmount); + // stakeContract.withdraw(tokenAmount); + // vm.stopPrank(); + // } + + // // Staked amount still remains unchanged for stakerOne + // (stakingTokenAmount, ) = stakeContract.getStakeInfo(stakerOne); + // assertEq(stakingTokenAmount, 100 ether); + + // // However there are no tokens left in the contract + // stakingTokenBalance = erc20Aux.balanceOf(address(stakeContract)); + // assertEq(stakingTokenBalance, 0 ether); + + // // StakerOne can't withdraw since there is no balance left + // vm.expectRevert("ERC20: transfer amount exceeds balance"); + // vm.prank(stakerOne); + // stakeContract.withdraw(stakingTokenAmount); + } +} diff --git a/src/test/staking/TokenStake_EthReward.t.sol b/src/test/staking/TokenStake_EthReward.t.sol new file mode 100644 index 000000000..9cef37deb --- /dev/null +++ b/src/test/staking/TokenStake_EthReward.t.sol @@ -0,0 +1,526 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { TokenStake } from "contracts/prebuilts/staking/TokenStake.sol"; + +// Test imports + +import "../utils/BaseTest.sol"; + +contract TokenStakeEthRewardTest is BaseTest { + TokenStake internal stakeContract; + + address internal stakerOne; + address internal stakerTwo; + + uint256 internal timeUnit; + uint256 internal rewardRatioNumerator; + uint256 internal rewardRatioDenominator; + + function setUp() public override { + super.setUp(); + + timeUnit = 60; + rewardRatioNumerator = 3; + rewardRatioDenominator = 50; + + stakerOne = address(0x345); + stakerTwo = address(0x567); + + erc20Aux.mint(stakerOne, 1000); // mint 1000 tokens to stakerOne + erc20Aux.mint(stakerTwo, 1000); // mint 1000 tokens to stakerTwo + + vm.deal(deployer, 1000 ether); // mint reward tokens (Eth) to contract admin + + stakeContract = TokenStake( + payable( + deployContractProxy( + "TokenStake", + abi.encodeCall( + TokenStake.initialize, + (deployer, CONTRACT_URI, forwarders(), NATIVE_TOKEN, address(erc20Aux), 60, 3, 50) + ) + ) + ) + ); + + // set approvals + vm.prank(stakerOne); + erc20Aux.approve(address(stakeContract), type(uint256).max); + + vm.prank(stakerTwo); + erc20Aux.approve(address(stakeContract), type(uint256).max); + + vm.startPrank(deployer); + stakeContract.depositRewardTokens{ value: 100 ether }(100 ether); + // erc20.transfer(address(stakeContract), 100 ether); + vm.stopPrank(); + assertEq(stakeContract.getRewardTokenBalance(), 100 ether); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: Stake + //////////////////////////////////////////////////////////////*/ + + function test_state_stake() public { + //================ first staker ====================== + vm.warp(1); + + // stake 400 tokens + vm.prank(stakerOne); + stakeContract.stake(400); + uint256 timeOfLastUpdate_one = block.timestamp; + + // check balances/ownership of staked tokens + assertEq(erc20Aux.balanceOf(address(stakeContract)), 400); + assertEq(erc20Aux.balanceOf(address(stakerOne)), 600); + + // check available rewards right after staking + (uint256 _amountStaked, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq(_amountStaked, 400); + assertEq(_availableRewards, 0); + + //=================== warp timestamp to calculate rewards + vm.roll(100); + vm.warp(1000); + + // check available rewards after warp + (, _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate_one) * 400) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + //================ second staker ====================== + vm.roll(200); + vm.warp(2000); + + // stake 20 tokens with token-id 0 + vm.prank(stakerTwo); + stakeContract.stake(200); + uint256 timeOfLastUpdate_two = block.timestamp; + + // check balances/ownership of staked tokens + assertEq(erc20Aux.balanceOf(address(stakeContract)), 200 + 400); // sum of staked tokens by both stakers + assertEq(erc20Aux.balanceOf(address(stakerTwo)), 800); + + // check available rewards right after staking + (_amountStaked, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + + assertEq(_amountStaked, 200); + assertEq(_availableRewards, 0); + + //=================== warp timestamp to calculate rewards + vm.roll(300); + vm.warp(3000); + + // check available rewards for stakerOne + (, _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate_one) * 400) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + // check available rewards for stakerTwo + (, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate_two) * 200) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + } + + function test_revert_stake_stakingZeroTokens() public { + // stake 0 tokens + + vm.prank(stakerOne); + vm.expectRevert("Staking 0 tokens"); + stakeContract.stake(0); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: claimRewards + //////////////////////////////////////////////////////////////*/ + + function test_state_claimRewards() public { + //================ setup stakerOne ====================== + vm.warp(1); + + // stake 50 tokens with token-id 0 + vm.prank(stakerOne); + stakeContract.stake(400); + uint256 timeOfLastUpdate_one = block.timestamp; + + //=================== warp timestamp to claim rewards + vm.roll(100); + vm.warp(1000); + + uint256 rewardBalanceBefore = stakeContract.getRewardTokenBalance(); + vm.prank(stakerOne); + stakeContract.claimRewards(); + uint256 rewardBalanceAfter = stakeContract.getRewardTokenBalance(); + + // check reward balances + assertEq( + stakerOne.balance, + (((((block.timestamp - timeOfLastUpdate_one) * 400) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + assertEq( + rewardBalanceAfter, + rewardBalanceBefore - + (((((block.timestamp - timeOfLastUpdate_one) * 400) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + // check available rewards after claiming + (uint256 _amountStaked, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq(_amountStaked, 400); + assertEq(_availableRewards, 0); + + //================ setup stakerTwo ====================== + + // stake 20 tokens with token-id 1 + vm.prank(stakerTwo); + stakeContract.stake(200); + uint256 timeOfLastUpdate_two = block.timestamp; + + //=================== warp timestamp to claim rewards + vm.roll(200); + vm.warp(2000); + + rewardBalanceBefore = stakeContract.getRewardTokenBalance(); + vm.prank(stakerTwo); + stakeContract.claimRewards(); + rewardBalanceAfter = stakeContract.getRewardTokenBalance(); + + // check reward balances + assertEq( + stakerTwo.balance, + (((((block.timestamp - timeOfLastUpdate_two) * 200) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + assertEq( + rewardBalanceAfter, + rewardBalanceBefore - + (((((block.timestamp - timeOfLastUpdate_two) * 200) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + // check available rewards after claiming -- stakerTwo + (_amountStaked, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + assertEq(_amountStaked, 200); + assertEq(_availableRewards, 0); + + // check available rewards -- stakerOne + (_amountStaked, _availableRewards) = stakeContract.getStakeInfo(stakerOne); + assertEq(_amountStaked, 400); + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate_two) * 400) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + } + + function test_revert_claimRewards_noRewards() public { + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(400); + + //=================== try to claim rewards in same block + + vm.prank(stakerOne); + vm.expectRevert("No rewards"); + stakeContract.claimRewards(); + + //======= withdraw tokens and claim rewards + vm.roll(100); + vm.warp(1000); + + vm.prank(stakerOne); + stakeContract.withdraw(400); + vm.prank(stakerOne); + stakeContract.claimRewards(); + + //===== try to claim rewards again + vm.roll(200); + vm.warp(2000); + vm.prank(stakerOne); + vm.expectRevert("No rewards"); + stakeContract.claimRewards(); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: stake conditions + //////////////////////////////////////////////////////////////*/ + + function test_state_setRewardRatio() public { + // set value and check + vm.prank(deployer); + stakeContract.setRewardRatio(3, 70); + (uint256 numerator, uint256 denominator) = stakeContract.getRewardRatio(); + assertEq(3, numerator); + assertEq(70, denominator); + + //================ stake tokens + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(400); + uint256 timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set rewardsPerUnitTime + vm.roll(100); + vm.warp(1000); + + vm.prank(deployer); + stakeContract.setRewardRatio(3, 80); + (numerator, denominator) = stakeContract.getRewardRatio(); + assertEq(3, numerator); + assertEq(80, denominator); + uint256 newTimeOfLastUpdate = block.timestamp; + + // check available rewards -- should use previous value for rewardsPerUnitTime for calculation + (, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq(_availableRewards, (((((block.timestamp - timeOfLastUpdate) * 400) * 3) / timeUnit) / 70)); + + //====== check rewards after some time + vm.roll(300); + vm.warp(3000); + + (, uint256 _newRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _newRewards, + _availableRewards + (((((block.timestamp - newTimeOfLastUpdate) * 400) * 3) / timeUnit) / 80) + ); + } + + function test_state_setTimeUnit() public { + // set value and check + uint80 timeUnitToSet = 100; + vm.prank(deployer); + stakeContract.setTimeUnit(timeUnitToSet); + assertEq(timeUnitToSet, stakeContract.getTimeUnit()); + + //================ stake tokens + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(400); + uint256 timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set timeUnit + vm.roll(100); + vm.warp(1000); + + vm.prank(deployer); + stakeContract.setTimeUnit(200); + assertEq(200, stakeContract.getTimeUnit()); + uint256 newTimeOfLastUpdate = block.timestamp; + + // check available rewards -- should use previous value for timeUnit for calculation + (, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate) * 400) * rewardRatioNumerator) / timeUnitToSet) / + rewardRatioDenominator) + ); + + //====== check rewards after some time + vm.roll(300); + vm.warp(3000); + + (, uint256 _newRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _newRewards, + _availableRewards + + (((((block.timestamp - newTimeOfLastUpdate) * 400) * rewardRatioNumerator) / 200) / + rewardRatioDenominator) + ); + } + + function test_revert_setRewardRatio_notAuthorized() public { + vm.expectRevert("Not authorized"); + stakeContract.setRewardRatio(1, 2); + } + + function test_revert_setRewardRatio_divideByZero() public { + vm.prank(deployer); + vm.expectRevert("divide by 0"); + stakeContract.setRewardRatio(1, 0); + } + + function test_revert_setTimeUnit_notAuthorized() public { + vm.expectRevert("Not authorized"); + stakeContract.setTimeUnit(1); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: withdraw + //////////////////////////////////////////////////////////////*/ + + function test_state_withdraw() public { + //================ stake different tokens ====================== + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake(400); + + vm.prank(stakerTwo); + stakeContract.stake(200); + + uint256 timeOfLastUpdate = block.timestamp; + + //========== warp timestamp before withdraw + vm.roll(100); + vm.warp(1000); + + // withdraw partially for stakerOne + vm.prank(stakerOne); + stakeContract.withdraw(100); + uint256 timeOfLastUpdateLatest = block.timestamp; + + // check balances/ownership after withdraw + assertEq(erc20Aux.balanceOf(stakerOne), 700); + assertEq(erc20Aux.balanceOf(stakerTwo), 800); + assertEq(erc20Aux.balanceOf(address(stakeContract)), (400 - 100) + 200); + + // check available rewards after withdraw + (, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate) * 400) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + (, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate) * 200) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + // check available rewards some time after withdraw + vm.roll(200); + vm.warp(2000); + + // check rewards for stakerOne + (, _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + ((((((timeOfLastUpdateLatest - timeOfLastUpdate) * 400)) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + + ((((((block.timestamp - timeOfLastUpdateLatest) * 300)) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + // withdraw partially for stakerTwo + vm.prank(stakerTwo); + stakeContract.withdraw(100); + timeOfLastUpdateLatest = block.timestamp; + + // check balances/ownership after withdraw + assertEq(erc20Aux.balanceOf(stakerOne), 700); + assertEq(erc20Aux.balanceOf(stakerTwo), 900); + assertEq(erc20Aux.balanceOf(address(stakeContract)), (400 - 100) + (200 - 100)); + + // check rewards for stakerTwo + (, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + + assertEq( + _availableRewards, + ((((((block.timestamp - timeOfLastUpdate) * 200)) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + // check rewards for stakerTwo after some time + vm.roll(300); + vm.warp(3000); + (, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + + assertEq( + _availableRewards, + ((((((timeOfLastUpdateLatest - timeOfLastUpdate) * 200)) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + + ((((((block.timestamp - timeOfLastUpdateLatest) * 100)) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + } + + function test_revert_withdraw_withdrawingZeroTokens() public { + vm.expectRevert("Withdrawing 0 tokens"); + stakeContract.withdraw(0); + } + + function test_revert_withdraw_withdrawingMoreThanStaked() public { + // stake tokens + vm.prank(stakerOne); + stakeContract.stake(400); + + vm.prank(stakerTwo); + stakeContract.stake(200); + + // trying to withdraw more than staked + vm.roll(200); + vm.warp(2000); + + vm.prank(stakerOne); + vm.expectRevert("Withdrawing more than staked"); + stakeContract.withdraw(500); + + // withdraw partially + vm.prank(stakerOne); + stakeContract.withdraw(300); + + // trying to withdraw more than staked + vm.prank(stakerOne); + vm.expectRevert("Withdrawing more than staked"); + stakeContract.withdraw(500); + + // re-stake + vm.prank(stakerOne); + stakeContract.stake(300); + + // trying to withdraw more than staked + vm.prank(stakerOne); + vm.expectRevert("Withdrawing more than staked"); + stakeContract.withdraw(500); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function test_revert_zeroTimeUnit_adminLockTokens() public { + //================ stake tokens + vm.warp(1); + + // User stakes tokens + vm.prank(stakerOne); + stakeContract.stake(400); + + // set timeUnit to zero + uint80 newTimeUnit = 0; + vm.prank(deployer); + vm.expectRevert("time-unit can't be 0"); + stakeContract.setTimeUnit(newTimeUnit); + + // stakerOne and stakerTwo can withdraw their tokens + // vm.expectRevert(stdError.divisionError); + vm.prank(stakerOne); + stakeContract.withdraw(400); + } +} diff --git a/src/test/staking/TokenStake_EthStake.t.sol b/src/test/staking/TokenStake_EthStake.t.sol new file mode 100644 index 000000000..8630d08a9 --- /dev/null +++ b/src/test/staking/TokenStake_EthStake.t.sol @@ -0,0 +1,520 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { TokenStake } from "contracts/prebuilts/staking/TokenStake.sol"; + +// Test imports + +import "../utils/BaseTest.sol"; + +contract TokenStakeEthStakeTest is BaseTest { + TokenStake internal stakeContract; + + address internal stakerOne; + address internal stakerTwo; + + uint256 internal timeUnit; + uint256 internal rewardRatioNumerator; + uint256 internal rewardRatioDenominator; + + function setUp() public override { + super.setUp(); + + timeUnit = 60; + rewardRatioNumerator = 3; + rewardRatioDenominator = 50; + + stakerOne = address(0x345); + stakerTwo = address(0x567); + + vm.deal(stakerOne, 1000); // mint 1000 tokens to stakerOne + vm.deal(stakerTwo, 1000); // mint 1000 tokens to stakerTwo + + erc20.mint(deployer, 1000 ether); // mint reward tokens to contract admin + + stakeContract = TokenStake( + payable( + deployContractProxy( + "TokenStake", + abi.encodeCall( + TokenStake.initialize, + (deployer, CONTRACT_URI, forwarders(), address(erc20), NATIVE_TOKEN, 60, 3, 50) + ) + ) + ) + ); + + vm.startPrank(deployer); + erc20.approve(address(stakeContract), type(uint256).max); + stakeContract.depositRewardTokens(100 ether); + // erc20.transfer(address(stakeContract), 100 ether); + vm.stopPrank(); + assertEq(stakeContract.getRewardTokenBalance(), 100 ether); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: Stake + //////////////////////////////////////////////////////////////*/ + + function test_state_stake() public { + //================ first staker ====================== + vm.warp(1); + + // stake 400 tokens + vm.prank(stakerOne); + stakeContract.stake{ value: 400 }(400); + uint256 timeOfLastUpdate_one = block.timestamp; + + // check balances/ownership of staked tokens + assertEq(weth.balanceOf(address(stakeContract)), 400); + assertEq(address(stakerOne).balance, 600); + + // check available rewards right after staking + (uint256 _amountStaked, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq(_amountStaked, 400); + assertEq(_availableRewards, 0); + + //=================== warp timestamp to calculate rewards + vm.roll(100); + vm.warp(1000); + + // check available rewards after warp + (, _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate_one) * 400) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + //================ second staker ====================== + vm.roll(200); + vm.warp(2000); + + // stake 20 tokens with token-id 0 + vm.prank(stakerTwo); + stakeContract.stake{ value: 200 }(200); + uint256 timeOfLastUpdate_two = block.timestamp; + + // check balances/ownership of staked tokens + assertEq(weth.balanceOf(address(stakeContract)), 200 + 400); // sum of staked tokens by both stakers + assertEq(address(stakerTwo).balance, 800); + + // check available rewards right after staking + (_amountStaked, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + + assertEq(_amountStaked, 200); + assertEq(_availableRewards, 0); + + //=================== warp timestamp to calculate rewards + vm.roll(300); + vm.warp(3000); + + // check available rewards for stakerOne + (, _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate_one) * 400) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + // check available rewards for stakerTwo + (, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate_two) * 200) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + } + + function test_revert_stake_stakingZeroTokens() public { + // stake 0 tokens + + vm.prank(stakerOne); + vm.expectRevert("Staking 0 tokens"); + stakeContract.stake(0); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: claimRewards + //////////////////////////////////////////////////////////////*/ + + function test_state_claimRewards() public { + //================ setup stakerOne ====================== + vm.warp(1); + + // stake 50 tokens with token-id 0 + vm.prank(stakerOne); + stakeContract.stake{ value: 400 }(400); + uint256 timeOfLastUpdate_one = block.timestamp; + + //=================== warp timestamp to claim rewards + vm.roll(100); + vm.warp(1000); + + uint256 rewardBalanceBefore = stakeContract.getRewardTokenBalance(); + vm.prank(stakerOne); + stakeContract.claimRewards(); + uint256 rewardBalanceAfter = stakeContract.getRewardTokenBalance(); + + // check reward balances + assertEq( + erc20.balanceOf(stakerOne), + (((((block.timestamp - timeOfLastUpdate_one) * 400) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + assertEq( + rewardBalanceAfter, + rewardBalanceBefore - + (((((block.timestamp - timeOfLastUpdate_one) * 400) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + // check available rewards after claiming + (uint256 _amountStaked, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq(_amountStaked, 400); + assertEq(_availableRewards, 0); + + //================ setup stakerTwo ====================== + + // stake 20 tokens with token-id 1 + vm.prank(stakerTwo); + stakeContract.stake{ value: 200 }(200); + uint256 timeOfLastUpdate_two = block.timestamp; + + //=================== warp timestamp to claim rewards + vm.roll(200); + vm.warp(2000); + + rewardBalanceBefore = stakeContract.getRewardTokenBalance(); + vm.prank(stakerTwo); + stakeContract.claimRewards(); + rewardBalanceAfter = stakeContract.getRewardTokenBalance(); + + // check reward balances + assertEq( + erc20.balanceOf(stakerTwo), + (((((block.timestamp - timeOfLastUpdate_two) * 200) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + assertEq( + rewardBalanceAfter, + rewardBalanceBefore - + (((((block.timestamp - timeOfLastUpdate_two) * 200) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + // check available rewards after claiming -- stakerTwo + (_amountStaked, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + assertEq(_amountStaked, 200); + assertEq(_availableRewards, 0); + + // check available rewards -- stakerOne + (_amountStaked, _availableRewards) = stakeContract.getStakeInfo(stakerOne); + assertEq(_amountStaked, 400); + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate_two) * 400) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + } + + function test_revert_claimRewards_noRewards() public { + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake{ value: 400 }(400); + + //=================== try to claim rewards in same block + + vm.prank(stakerOne); + vm.expectRevert("No rewards"); + stakeContract.claimRewards(); + + //======= withdraw tokens and claim rewards + vm.roll(100); + vm.warp(1000); + + vm.prank(stakerOne); + stakeContract.withdraw(400); + vm.prank(stakerOne); + stakeContract.claimRewards(); + + //===== try to claim rewards again + vm.roll(200); + vm.warp(2000); + vm.prank(stakerOne); + vm.expectRevert("No rewards"); + stakeContract.claimRewards(); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: stake conditions + //////////////////////////////////////////////////////////////*/ + + function test_state_setRewardRatio() public { + // set value and check + vm.prank(deployer); + stakeContract.setRewardRatio(3, 70); + (uint256 numerator, uint256 denominator) = stakeContract.getRewardRatio(); + assertEq(3, numerator); + assertEq(70, denominator); + + //================ stake tokens + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake{ value: 400 }(400); + uint256 timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set rewardsPerUnitTime + vm.roll(100); + vm.warp(1000); + + vm.prank(deployer); + stakeContract.setRewardRatio(3, 80); + (numerator, denominator) = stakeContract.getRewardRatio(); + assertEq(3, numerator); + assertEq(80, denominator); + uint256 newTimeOfLastUpdate = block.timestamp; + + // check available rewards -- should use previous value for rewardsPerUnitTime for calculation + (, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq(_availableRewards, (((((block.timestamp - timeOfLastUpdate) * 400) * 3) / timeUnit) / 70)); + + //====== check rewards after some time + vm.roll(300); + vm.warp(3000); + + (, uint256 _newRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _newRewards, + _availableRewards + (((((block.timestamp - newTimeOfLastUpdate) * 400) * 3) / timeUnit) / 80) + ); + } + + function test_state_setTimeUnit() public { + // set value and check + uint80 timeUnitToSet = 100; + vm.prank(deployer); + stakeContract.setTimeUnit(timeUnitToSet); + assertEq(timeUnitToSet, stakeContract.getTimeUnit()); + + //================ stake tokens + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake{ value: 400 }(400); + uint256 timeOfLastUpdate = block.timestamp; + + //=================== warp timestamp and again set timeUnit + vm.roll(100); + vm.warp(1000); + + vm.prank(deployer); + stakeContract.setTimeUnit(200); + assertEq(200, stakeContract.getTimeUnit()); + uint256 newTimeOfLastUpdate = block.timestamp; + + // check available rewards -- should use previous value for timeUnit for calculation + (, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate) * 400) * rewardRatioNumerator) / timeUnitToSet) / + rewardRatioDenominator) + ); + + //====== check rewards after some time + vm.roll(300); + vm.warp(3000); + + (, uint256 _newRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _newRewards, + _availableRewards + + (((((block.timestamp - newTimeOfLastUpdate) * 400) * rewardRatioNumerator) / 200) / + rewardRatioDenominator) + ); + } + + function test_revert_setRewardRatio_notAuthorized() public { + vm.expectRevert("Not authorized"); + stakeContract.setRewardRatio(1, 2); + } + + function test_revert_setRewardRatio_divideByZero() public { + vm.prank(deployer); + vm.expectRevert("divide by 0"); + stakeContract.setRewardRatio(1, 0); + } + + function test_revert_setTimeUnit_notAuthorized() public { + vm.expectRevert("Not authorized"); + stakeContract.setTimeUnit(1); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: withdraw + //////////////////////////////////////////////////////////////*/ + + function test_state_withdraw() public { + //================ stake different tokens ====================== + vm.warp(1); + + vm.prank(stakerOne); + stakeContract.stake{ value: 400 }(400); + + vm.prank(stakerTwo); + stakeContract.stake{ value: 200 }(200); + + uint256 timeOfLastUpdate = block.timestamp; + + //========== warp timestamp before withdraw + vm.roll(100); + vm.warp(1000); + + // withdraw partially for stakerOne + vm.prank(stakerOne); + stakeContract.withdraw(100); + uint256 timeOfLastUpdateLatest = block.timestamp; + + // check balances/ownership after withdraw + assertEq(stakerOne.balance, 700); + assertEq(stakerTwo.balance, 800); + assertEq(weth.balanceOf(address(stakeContract)), (400 - 100) + 200); + + // check available rewards after withdraw + (, uint256 _availableRewards) = stakeContract.getStakeInfo(stakerOne); + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate) * 400) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + (, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + assertEq( + _availableRewards, + (((((block.timestamp - timeOfLastUpdate) * 200) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + // check available rewards some time after withdraw + vm.roll(200); + vm.warp(2000); + + // check rewards for stakerOne + (, _availableRewards) = stakeContract.getStakeInfo(stakerOne); + + assertEq( + _availableRewards, + ((((((timeOfLastUpdateLatest - timeOfLastUpdate) * 400)) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + + ((((((block.timestamp - timeOfLastUpdateLatest) * 300)) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + // withdraw partially for stakerTwo + vm.prank(stakerTwo); + stakeContract.withdraw(100); + timeOfLastUpdateLatest = block.timestamp; + + // check balances/ownership after withdraw + assertEq(stakerOne.balance, 700); + assertEq(stakerTwo.balance, 900); + assertEq(weth.balanceOf(address(stakeContract)), (400 - 100) + (200 - 100)); + + // check rewards for stakerTwo + (, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + + assertEq( + _availableRewards, + ((((((block.timestamp - timeOfLastUpdate) * 200)) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + + // check rewards for stakerTwo after some time + vm.roll(300); + vm.warp(3000); + (, _availableRewards) = stakeContract.getStakeInfo(stakerTwo); + + assertEq( + _availableRewards, + ((((((timeOfLastUpdateLatest - timeOfLastUpdate) * 200)) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + + ((((((block.timestamp - timeOfLastUpdateLatest) * 100)) * rewardRatioNumerator) / timeUnit) / + rewardRatioDenominator) + ); + } + + function test_revert_withdraw_withdrawingZeroTokens() public { + vm.expectRevert("Withdrawing 0 tokens"); + stakeContract.withdraw(0); + } + + function test_revert_withdraw_withdrawingMoreThanStaked() public { + // stake tokens + vm.prank(stakerOne); + stakeContract.stake{ value: 400 }(400); + + vm.prank(stakerTwo); + stakeContract.stake{ value: 200 }(200); + + // trying to withdraw more than staked + vm.roll(200); + vm.warp(2000); + + vm.prank(stakerOne); + vm.expectRevert("Withdrawing more than staked"); + stakeContract.withdraw(500); + + // withdraw partially + vm.prank(stakerOne); + stakeContract.withdraw(300); + + // trying to withdraw more than staked + vm.prank(stakerOne); + vm.expectRevert("Withdrawing more than staked"); + stakeContract.withdraw(500); + + // re-stake + vm.prank(stakerOne); + stakeContract.stake{ value: 300 }(300); + + // trying to withdraw more than staked + vm.prank(stakerOne); + vm.expectRevert("Withdrawing more than staked"); + stakeContract.withdraw(500); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function test_revert_zeroTimeUnit_adminLockTokens() public { + //================ stake tokens + vm.warp(1); + + // User stakes tokens + vm.prank(stakerOne); + stakeContract.stake{ value: 400 }(400); + + // set timeUnit to zero + uint80 newTimeUnit = 0; + vm.prank(deployer); + vm.expectRevert("time-unit can't be 0"); + stakeContract.setTimeUnit(newTimeUnit); + + // stakerOne and stakerTwo can withdraw their tokens + // vm.expectRevert(stdError.divisionError); + vm.prank(stakerOne); + stakeContract.withdraw(400); + } +} diff --git a/src/test/token/TokenERC1155.t.sol b/src/test/token/TokenERC1155.t.sol new file mode 100644 index 000000000..516ada04c --- /dev/null +++ b/src/test/token/TokenERC1155.t.sol @@ -0,0 +1,951 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { TokenERC1155, IPlatformFee, NFTMetadata } from "contracts/prebuilts/token/TokenERC1155.sol"; + +// Test imports + +import "../utils/BaseTest.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; + +contract TokenERC1155Test is BaseTest { + using Strings for uint256; + + event TokensMinted(address indexed mintedTo, uint256 indexed tokenIdMinted, string uri, uint256 quantityMinted); + event TokensMintedWithSignature( + address indexed signer, + address indexed mintedTo, + uint256 indexed tokenIdMinted, + TokenERC1155.MintRequest mintRequest + ); + event OwnerUpdated(address indexed prevOwner, address indexed newOwner); + event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps); + event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, uint256 royaltyBps); + event PrimarySaleRecipientUpdated(address indexed recipient); + event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps); + + TokenERC1155 public tokenContract; + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + bytes private emptyEncodedBytes = abi.encode("", ""); + + TokenERC1155.MintRequest _mintrequest; + bytes _signature; + + address internal deployerSigner; + address internal recipient; + + address private defaultFeeRecipient; + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + deployerSigner = signer; + recipient = address(0x123); + tokenContract = TokenERC1155(getContract("TokenERC1155")); + defaultFeeRecipient = tokenContract.DEFAULT_FEE_RECIPIENT(); + + erc20.mint(deployerSigner, 1_000); + vm.deal(deployerSigner, 1_000); + + erc20.mint(recipient, 1_000); + vm.deal(recipient, 1_000); + + typehashMintRequest = keccak256( + "MintRequest(address to,address royaltyRecipient,uint256 royaltyBps,address primarySaleRecipient,uint256 tokenId,string uri,uint256 quantity,uint256 pricePerToken,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + nameHash = keccak256(bytes("TokenERC1155")); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256( + abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(tokenContract)) + ); + + // construct default mintrequest + _mintrequest.to = recipient; + _mintrequest.royaltyRecipient = royaltyRecipient; + _mintrequest.royaltyBps = royaltyBps; + _mintrequest.primarySaleRecipient = saleRecipient; + _mintrequest.tokenId = type(uint256).max; + _mintrequest.uri = "ipfs://"; + _mintrequest.quantity = 100; + _mintrequest.pricePerToken = 0; + _mintrequest.currency = address(0); + _mintrequest.validityStartTimestamp = 1000; + _mintrequest.validityEndTimestamp = 2000; + _mintrequest.uid = bytes32(0); + + _signature = signMintRequest(_mintrequest, privateKey); + } + + function signMintRequest( + TokenERC1155.MintRequest memory _request, + uint256 _privateKey + ) internal view returns (bytes memory) { + bytes memory encodedRequest = bytes.concat( + abi.encode( + typehashMintRequest, + _request.to, + _request.royaltyRecipient, + _request.royaltyBps, + _request.primarySaleRecipient, + _request.tokenId, + keccak256(bytes(_request.uri)) + ), + abi.encode( + _request.quantity, + _request.pricePerToken, + _request.currency, + _request.validityStartTimestamp, + _request.validityEndTimestamp, + _request.uid + ) + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, typedDataHash); + bytes memory sig = abi.encodePacked(r, s, v); + + return sig; + } + + function test_compareEncoding() public { + bytes memory encodedRequestOne = abi.encode( + typehashMintRequest, + _mintrequest.to, + _mintrequest.royaltyRecipient, + _mintrequest.royaltyBps, + _mintrequest.primarySaleRecipient, + keccak256(bytes(_mintrequest.uri)), + _mintrequest.quantity, + _mintrequest.pricePerToken, + _mintrequest.currency, + _mintrequest.validityStartTimestamp, + _mintrequest.validityEndTimestamp, + _mintrequest.uid + ); + bytes memory encodedRequestTwo = bytes.concat( + abi.encode( + typehashMintRequest, + _mintrequest.to, + _mintrequest.royaltyRecipient, + _mintrequest.royaltyBps, + _mintrequest.primarySaleRecipient, + keccak256(bytes(_mintrequest.uri)) + ), + abi.encode( + _mintrequest.quantity, + _mintrequest.pricePerToken, + _mintrequest.currency, + _mintrequest.validityStartTimestamp, + _mintrequest.validityEndTimestamp, + _mintrequest.uid + ) + ); + bytes32 structHashOne = keccak256(encodedRequestOne); + bytes32 typedDataHashOne = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHashOne)); + + bytes32 structHashTwo = keccak256(encodedRequestTwo); + bytes32 typedDataHashTwo = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHashTwo)); + + assertEq(structHashOne, structHashTwo); + assertEq(typedDataHashOne, typedDataHashTwo); + console.logBytes32(structHashOne); + console.logBytes32(structHashTwo); + console.logBytes32(typedDataHashOne); + console.logBytes32(typedDataHashTwo); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `mintWithSignature` + //////////////////////////////////////////////////////////////*/ + + function test_state_mintWithSignature_NewTokenId() public { + vm.warp(1000); + + // initial balances and state + uint256 nextTokenId = tokenContract.nextTokenIdToMint(); + uint256 currentBalanceOfRecipient = tokenContract.balanceOf(recipient, nextTokenId); + + // mint with signature + vm.prank(recipient); + tokenContract.mintWithSignature(_mintrequest, _signature); + + // check state after minting + assertEq(tokenContract.nextTokenIdToMint(), nextTokenId + 1); + assertEq(tokenContract.uri(nextTokenId), string(_mintrequest.uri)); + assertEq(tokenContract.balanceOf(recipient, nextTokenId), currentBalanceOfRecipient + _mintrequest.quantity); + } + + function test_state_mintWithSignature_ExistingTokenId() public { + vm.warp(1000); + uint256 nextTokenId = tokenContract.nextTokenIdToMint(); + + // first mint of new tokenId + vm.prank(recipient); + tokenContract.mintWithSignature(_mintrequest, _signature); + + // initial balances and state + uint256 currentBalanceOfRecipient = tokenContract.balanceOf(recipient, nextTokenId); + uint256 _uid = 1; + + // update mintrequest + _mintrequest.tokenId = nextTokenId; + _mintrequest.uid = bytes32(_uid); + _signature = signMintRequest(_mintrequest, privateKey); + + // mint existing tokenId + vm.prank(recipient); + tokenContract.mintWithSignature(_mintrequest, _signature); + + // check state after minting + assertEq(tokenContract.nextTokenIdToMint(), nextTokenId + 1); + assertEq(tokenContract.uri(nextTokenId), string(_mintrequest.uri)); + assertEq(tokenContract.balanceOf(recipient, nextTokenId), currentBalanceOfRecipient + _mintrequest.quantity); + } + + function test_revert_mintWithSignature_InvalidTokenId() public { + vm.warp(1000); + + // update mintrequest + _mintrequest.tokenId = 0; + _signature = signMintRequest(_mintrequest, privateKey); + + // mint non-existent tokenId + vm.prank(recipient); + vm.expectRevert("invalid id"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_state_mintWithSignature_NonZeroPrice_ERC20() public { + vm.warp(1000); + + // update mintrequest data + _mintrequest.pricePerToken = 1; + _mintrequest.currency = address(erc20); + _signature = signMintRequest(_mintrequest, privateKey); + + // approve erc20 tokens to tokenContract + vm.prank(recipient); + erc20.approve(address(tokenContract), _mintrequest.pricePerToken * _mintrequest.quantity); + + // initial balances and state + uint256 nextTokenId = tokenContract.nextTokenIdToMint(); + uint256 currentBalanceOfRecipient = tokenContract.balanceOf(recipient, nextTokenId); + + uint256 erc20BalanceOfSeller = erc20.balanceOf(address(saleRecipient)); + uint256 erc20BalanceOfRecipient = erc20.balanceOf(address(recipient)); + + // mint with signature + vm.prank(recipient); + tokenContract.mintWithSignature(_mintrequest, _signature); + + // check state after minting + assertEq(tokenContract.nextTokenIdToMint(), nextTokenId + 1); + assertEq(tokenContract.uri(nextTokenId), string(_mintrequest.uri)); + assertEq(tokenContract.balanceOf(recipient, nextTokenId), currentBalanceOfRecipient + _mintrequest.quantity); + + // check erc20 balances after minting + uint256 defaultFee = ((_mintrequest.pricePerToken * _mintrequest.quantity) * 100) / MAX_BPS; + uint256 _platformFees = ((_mintrequest.pricePerToken * _mintrequest.quantity) * platformFeeBps) / MAX_BPS; + assertEq( + erc20.balanceOf(recipient), + erc20BalanceOfRecipient - (_mintrequest.pricePerToken * _mintrequest.quantity) + ); + assertEq( + erc20.balanceOf(address(saleRecipient)), + erc20BalanceOfSeller + (_mintrequest.pricePerToken * _mintrequest.quantity) - _platformFees - defaultFee + ); + assertEq(erc20.balanceOf(address(defaultFeeRecipient)), defaultFee); + } + + function test_state_mintWithSignature_NonZeroPrice_NativeToken() public { + vm.warp(1000); + + // update mintrequest data + _mintrequest.pricePerToken = 1; + _mintrequest.currency = address(NATIVE_TOKEN); + _signature = signMintRequest(_mintrequest, privateKey); + + // initial balances and state + uint256 nextTokenId = tokenContract.nextTokenIdToMint(); + uint256 currentBalanceOfRecipient = tokenContract.balanceOf(recipient, nextTokenId); + + uint256 etherBalanceOfSeller = address(saleRecipient).balance; + uint256 etherBalanceOfRecipient = address(recipient).balance; + + // mint with signature + vm.prank(recipient); + tokenContract.mintWithSignature{ value: _mintrequest.pricePerToken * _mintrequest.quantity }( + _mintrequest, + _signature + ); + + // check state after minting + assertEq(tokenContract.nextTokenIdToMint(), nextTokenId + 1); + assertEq(tokenContract.uri(nextTokenId), string(_mintrequest.uri)); + assertEq(tokenContract.balanceOf(recipient, nextTokenId), currentBalanceOfRecipient + _mintrequest.quantity); + + // check balances after minting + uint256 defaultFee = ((_mintrequest.pricePerToken * _mintrequest.quantity) * 100) / MAX_BPS; + uint256 _platformFees = ((_mintrequest.pricePerToken * _mintrequest.quantity) * platformFeeBps) / MAX_BPS; + assertEq( + address(recipient).balance, + etherBalanceOfRecipient - (_mintrequest.pricePerToken * _mintrequest.quantity) + ); + assertEq( + address(saleRecipient).balance, + etherBalanceOfSeller + (_mintrequest.pricePerToken * _mintrequest.quantity) - _platformFees - defaultFee + ); + assertEq(address(defaultFeeRecipient).balance, defaultFee); + } + + function test_revert_mintWithSignature_MustSendTotalPrice() public { + vm.warp(1000); + + _mintrequest.pricePerToken = 1; + _mintrequest.currency = address(NATIVE_TOKEN); + _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(recipient); + vm.expectRevert("must send total price."); + tokenContract.mintWithSignature{ value: 0 }(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_MsgValueNotZero() public { + vm.warp(1000); + + _mintrequest.pricePerToken = 1; + _mintrequest.currency = address(erc20); + _signature = signMintRequest(_mintrequest, privateKey); + + // shouldn't send native-token when it is not the currency + vm.prank(recipient); + vm.expectRevert("msg value not zero"); + tokenContract.mintWithSignature{ value: 1 }(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_InvalidSignature() public { + vm.warp(1000); + + uint256 incorrectKey = 3456; + _signature = signMintRequest(_mintrequest, incorrectKey); + + vm.prank(recipient); + vm.expectRevert("invalid signature"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_RequestExpired() public { + _signature = signMintRequest(_mintrequest, privateKey); + + // warp time more out of range + vm.warp(3000); + + vm.prank(recipient); + vm.expectRevert("request expired"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_RecipientUndefined() public { + vm.warp(1000); + + _mintrequest.to = address(0); + _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(recipient); + vm.expectRevert("recipient undefined"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_ZeroQuantity() public { + vm.warp(1000); + + _mintrequest.quantity = 0; + _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(recipient); + vm.expectRevert("zero quantity"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_event_mintWithSignature() public { + vm.warp(1000); + + vm.expectEmit(true, true, true, true); + emit TokensMintedWithSignature(deployerSigner, recipient, 0, _mintrequest); + + // mint with signature + vm.prank(recipient); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `mintTo` + //////////////////////////////////////////////////////////////*/ + + function test_state_mintTo() public { + string memory _tokenURI = "tokenURI"; + uint256 _amount = 100; + + uint256 nextTokenId = tokenContract.nextTokenIdToMint(); + uint256 currentBalanceOfRecipient = tokenContract.balanceOf(recipient, nextTokenId); + + vm.prank(deployerSigner); + tokenContract.mintTo(recipient, type(uint256).max, _tokenURI, _amount); + + assertEq(tokenContract.nextTokenIdToMint(), nextTokenId + 1); + assertEq(tokenContract.uri(nextTokenId), _tokenURI); + assertEq(tokenContract.balanceOf(recipient, nextTokenId), currentBalanceOfRecipient + _amount); + } + + function test_revert_mintTo_NotAuthorized() public { + string memory _tokenURI = "tokenURI"; + uint256 _amount = 100; + bytes32 role = keccak256("MINTER_ROLE"); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(address(0x1)), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ); + vm.prank(address(0x1)); + tokenContract.mintTo(recipient, type(uint256).max, _tokenURI, _amount); + } + + function test_event_mintTo() public { + string memory _tokenURI = "tokenURI"; + uint256 _amount = 100; + + vm.expectEmit(true, true, true, true); + emit TokensMinted(recipient, 0, _tokenURI, _amount); + + // mint + vm.prank(deployerSigner); + tokenContract.mintTo(recipient, type(uint256).max, _tokenURI, _amount); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `burn` + //////////////////////////////////////////////////////////////*/ + + function test_state_burn_TokenOwner() public { + string memory _tokenURI = "tokenURI"; + uint256 _amount = 100; + + uint256 nextTokenId = tokenContract.nextTokenIdToMint(); + uint256 currentBalanceOfRecipient = tokenContract.balanceOf(recipient, nextTokenId); + + vm.prank(deployerSigner); + tokenContract.mintTo(recipient, type(uint256).max, _tokenURI, _amount); + + vm.prank(recipient); + tokenContract.burn(recipient, nextTokenId, _amount); + + assertEq(tokenContract.nextTokenIdToMint(), nextTokenId + 1); + assertEq(tokenContract.uri(nextTokenId), _tokenURI); + assertEq(tokenContract.balanceOf(recipient, nextTokenId), currentBalanceOfRecipient); + } + + function test_state_burn_TokenOperator() public { + string memory _tokenURI = "tokenURI"; + uint256 _amount = 100; + + address operator = address(0x789); + + uint256 nextTokenId = tokenContract.nextTokenIdToMint(); + uint256 currentBalanceOfRecipient = tokenContract.balanceOf(recipient, nextTokenId); + + vm.prank(deployerSigner); + tokenContract.mintTo(recipient, type(uint256).max, _tokenURI, _amount); + + vm.prank(recipient); + tokenContract.setApprovalForAll(operator, true); + + vm.prank(operator); + tokenContract.burn(recipient, nextTokenId, _amount); + + assertEq(tokenContract.nextTokenIdToMint(), nextTokenId + 1); + assertEq(tokenContract.uri(nextTokenId), _tokenURI); + assertEq(tokenContract.balanceOf(recipient, nextTokenId), currentBalanceOfRecipient); + } + + function test_revert_burn_NotOwnerNorApproved() public { + string memory _tokenURI = "tokenURI"; + uint256 _amount = 100; + + uint256 nextTokenId = tokenContract.nextTokenIdToMint(); + + vm.prank(deployerSigner); + tokenContract.mintTo(recipient, type(uint256).max, _tokenURI, _amount); + + vm.prank(address(0x789)); + vm.expectRevert("ERC1155: caller is not owner nor approved."); + tokenContract.burn(recipient, nextTokenId, _amount); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: owner + //////////////////////////////////////////////////////////////*/ + + function test_state_setOwner() public { + address newOwner = address(0x123); + bytes32 role = tokenContract.DEFAULT_ADMIN_ROLE(); + + vm.prank(deployerSigner); + tokenContract.grantRole(role, newOwner); + + vm.prank(deployerSigner); + tokenContract.setOwner(newOwner); + + assertEq(tokenContract.owner(), newOwner); + } + + function test_revert_setOwner_NotModuleAdmin() public { + vm.expectRevert("new owner not module admin."); + vm.prank(deployerSigner); + tokenContract.setOwner(address(0x1234)); + } + + function test_event_setOwner() public { + address newOwner = address(0x123); + bytes32 role = tokenContract.DEFAULT_ADMIN_ROLE(); + + vm.startPrank(deployerSigner); + tokenContract.grantRole(role, newOwner); + + vm.expectEmit(true, true, true, true); + emit OwnerUpdated(deployerSigner, newOwner); + + tokenContract.setOwner(newOwner); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: royalty + //////////////////////////////////////////////////////////////*/ + + function test_state_setDefaultRoyaltyInfo() public { + address _royaltyRecipient = address(0x123); + uint256 _royaltyBps = 1000; + + vm.prank(deployerSigner); + tokenContract.setDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + + (address newRoyaltyRecipient, uint256 newRoyaltyBps) = tokenContract.getDefaultRoyaltyInfo(); + assertEq(newRoyaltyRecipient, _royaltyRecipient); + assertEq(newRoyaltyBps, _royaltyBps); + + (address receiver, uint256 royaltyAmount) = tokenContract.royaltyInfo(0, 100); + assertEq(receiver, _royaltyRecipient); + assertEq(royaltyAmount, (100 * 1000) / 10_000); + } + + function test_revert_setDefaultRoyaltyInfo_NotAuthorized() public { + address _royaltyRecipient = address(0x123); + uint256 _royaltyBps = 1000; + bytes32 role = tokenContract.DEFAULT_ADMIN_ROLE(); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(address(0x1)), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ); + vm.prank(address(0x1)); + tokenContract.setDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + } + + function test_revert_setDefaultRoyaltyInfo_ExceedsRoyaltyBps() public { + address _royaltyRecipient = address(0x123); + uint256 _royaltyBps = 10001; + + vm.expectRevert("exceed royalty bps"); + vm.prank(deployerSigner); + tokenContract.setDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + } + + function test_state_setRoyaltyInfoForToken() public { + uint256 _tokenId = 1; + address _recipient = address(0x123); + uint256 _bps = 1000; + + vm.prank(deployerSigner); + tokenContract.setRoyaltyInfoForToken(_tokenId, _recipient, _bps); + + (address receiver, uint256 royaltyAmount) = tokenContract.royaltyInfo(_tokenId, 100); + assertEq(receiver, _recipient); + assertEq(royaltyAmount, (100 * 1000) / 10_000); + } + + function test_revert_setRoyaltyInfo_NotAuthorized() public { + uint256 _tokenId = 1; + address _recipient = address(0x123); + uint256 _bps = 1000; + bytes32 role = tokenContract.DEFAULT_ADMIN_ROLE(); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(address(0x1)), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ); + vm.prank(address(0x1)); + tokenContract.setRoyaltyInfoForToken(_tokenId, _recipient, _bps); + } + + function test_revert_setRoyaltyInfoForToken_ExceedsRoyaltyBps() public { + uint256 _tokenId = 1; + address _recipient = address(0x123); + uint256 _bps = 10001; + + vm.expectRevert("exceed royalty bps"); + vm.prank(deployerSigner); + tokenContract.setRoyaltyInfoForToken(_tokenId, _recipient, _bps); + } + + function test_event_defaultRoyalty() public { + address _royaltyRecipient = address(0x123); + uint256 _royaltyBps = 1000; + + vm.expectEmit(true, true, true, true); + emit DefaultRoyalty(_royaltyRecipient, _royaltyBps); + + vm.prank(deployerSigner); + tokenContract.setDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + } + + function test_event_royaltyForToken() public { + uint256 _tokenId = 1; + address _recipient = address(0x123); + uint256 _bps = 1000; + + vm.expectEmit(true, true, true, true); + emit RoyaltyForToken(_tokenId, _recipient, _bps); + + vm.prank(deployerSigner); + tokenContract.setRoyaltyInfoForToken(_tokenId, _recipient, _bps); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: primary sale + //////////////////////////////////////////////////////////////*/ + + function test_state_setPrimarySaleRecipient() public { + address _primarySaleRecipient = address(0x123); + + vm.prank(deployerSigner); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + + address recipient_ = tokenContract.primarySaleRecipient(); + assertEq(recipient_, _primarySaleRecipient); + } + + function test_revert_setPrimarySaleRecipient_NotAuthorized() public { + address _primarySaleRecipient = address(0x123); + bytes32 role = tokenContract.DEFAULT_ADMIN_ROLE(); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(address(0x1)), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ); + vm.prank(address(0x1)); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + } + + function test_event_setPrimarySaleRecipient() public { + address _primarySaleRecipient = address(0x123); + + vm.expectEmit(true, true, true, true); + emit PrimarySaleRecipientUpdated(_primarySaleRecipient); + + vm.prank(deployerSigner); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: platform fee + //////////////////////////////////////////////////////////////*/ + + function test_state_PlatformFee_Flat_ERC20() public { + vm.warp(1000); + uint256 flatPlatformFee = 10; + + vm.startPrank(deployerSigner); + tokenContract.setFlatPlatformFeeInfo(platformFeeRecipient, flatPlatformFee); + tokenContract.setPlatformFeeType(IPlatformFee.PlatformFeeType.Flat); + vm.stopPrank(); + + // update mintrequest data + _mintrequest.pricePerToken = 1; + _mintrequest.currency = address(erc20); + _signature = signMintRequest(_mintrequest, privateKey); + + uint256 defaultFee = (_mintrequest.pricePerToken * _mintrequest.quantity * 100) / 10_000; + + // approve erc20 tokens to tokenContract + vm.prank(recipient); + erc20.approve(address(tokenContract), _mintrequest.pricePerToken * _mintrequest.quantity); + + // initial balances and state + uint256 nextTokenId = tokenContract.nextTokenIdToMint(); + uint256 currentBalanceOfRecipient = tokenContract.balanceOf(recipient, nextTokenId); + + uint256 erc20BalanceOfSeller = erc20.balanceOf(address(saleRecipient)); + uint256 erc20BalanceOfRecipient = erc20.balanceOf(address(recipient)); + uint256 defaultFeeRecipientBefore = erc20.balanceOf(address(defaultFeeRecipient)); + + // mint with signature + vm.prank(recipient); + tokenContract.mintWithSignature(_mintrequest, _signature); + + // check state after minting + assertEq(tokenContract.nextTokenIdToMint(), nextTokenId + 1); + assertEq(tokenContract.uri(nextTokenId), string(_mintrequest.uri)); + assertEq(tokenContract.balanceOf(recipient, nextTokenId), currentBalanceOfRecipient + _mintrequest.quantity); + + // check erc20 balances after minting + assertEq( + erc20.balanceOf(recipient), + erc20BalanceOfRecipient - (_mintrequest.pricePerToken * _mintrequest.quantity) + ); + assertEq( + erc20.balanceOf(address(saleRecipient)), + erc20BalanceOfSeller + (_mintrequest.pricePerToken * _mintrequest.quantity) - flatPlatformFee - defaultFee + ); + assertEq(erc20.balanceOf(address(defaultFeeRecipient)), defaultFeeRecipientBefore + defaultFee); + } + + function test_state_PlatformFee_NativeToken() public { + vm.warp(1000); + uint256 flatPlatformFee = 10; + + vm.startPrank(deployerSigner); + tokenContract.setFlatPlatformFeeInfo(platformFeeRecipient, flatPlatformFee); + tokenContract.setPlatformFeeType(IPlatformFee.PlatformFeeType.Flat); + vm.stopPrank(); + + // update mintrequest data + _mintrequest.pricePerToken = 1; + _mintrequest.currency = address(NATIVE_TOKEN); + _signature = signMintRequest(_mintrequest, privateKey); + + // initial balances and state + uint256 nextTokenId = tokenContract.nextTokenIdToMint(); + uint256 currentBalanceOfRecipient = tokenContract.balanceOf(recipient, nextTokenId); + + uint256 etherBalanceOfSeller = address(saleRecipient).balance; + uint256 etherBalanceOfRecipient = address(recipient).balance; + + // mint with signature + vm.prank(recipient); + tokenContract.mintWithSignature{ value: _mintrequest.pricePerToken * _mintrequest.quantity }( + _mintrequest, + _signature + ); + + uint256 defaultFee = (_mintrequest.pricePerToken * _mintrequest.quantity * 100) / 10_000; + + // check state after minting + assertEq(tokenContract.nextTokenIdToMint(), nextTokenId + 1); + assertEq(tokenContract.uri(nextTokenId), string(_mintrequest.uri)); + assertEq(tokenContract.balanceOf(recipient, nextTokenId), currentBalanceOfRecipient + _mintrequest.quantity); + + // check balances after minting + assertEq( + address(recipient).balance, + etherBalanceOfRecipient - (_mintrequest.pricePerToken * _mintrequest.quantity) + ); + assertEq( + address(saleRecipient).balance, + etherBalanceOfSeller + (_mintrequest.pricePerToken * _mintrequest.quantity) - flatPlatformFee - defaultFee + ); + assertEq(address(defaultFeeRecipient).balance, defaultFee); + } + + function test_revert_PlatformFeeGreaterThanPrice() public { + vm.warp(1000); + uint256 flatPlatformFee = 1 ether; + + vm.startPrank(deployerSigner); + tokenContract.setFlatPlatformFeeInfo(platformFeeRecipient, flatPlatformFee); + tokenContract.setPlatformFeeType(IPlatformFee.PlatformFeeType.Flat); + vm.stopPrank(); + + // update mintrequest data + _mintrequest.pricePerToken = 1; + _mintrequest.currency = address(erc20); + _signature = signMintRequest(_mintrequest, privateKey); + + // mint with signature + vm.prank(recipient); + vm.expectRevert("price less than platform fee"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_state_setPlatformFeeInfo() public { + address _platformFeeRecipient = address(0x123); + uint256 _platformFeeBps = 1000; + + vm.prank(deployerSigner); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + + (address recipient_, uint16 bps) = tokenContract.getPlatformFeeInfo(); + assertEq(_platformFeeRecipient, recipient_); + assertEq(_platformFeeBps, bps); + } + + function test_state_setFlatPlatformFee() public { + address _platformFeeRecipient = address(0x123); + uint256 _flatFee = 1000; + + vm.prank(deployerSigner); + tokenContract.setFlatPlatformFeeInfo(_platformFeeRecipient, _flatFee); + + (address recipient_, uint256 fee) = tokenContract.getFlatPlatformFeeInfo(); + assertEq(_platformFeeRecipient, recipient_); + assertEq(_flatFee, fee); + } + + function test_state_setPlatformFeeType() public { + address _platformFeeRecipient = address(0x123); + uint256 _flatFee = 1000; + IPlatformFee.PlatformFeeType _feeType = IPlatformFee.PlatformFeeType.Flat; + + vm.prank(deployerSigner); + tokenContract.setFlatPlatformFeeInfo(_platformFeeRecipient, _flatFee); + + vm.prank(deployerSigner); + tokenContract.setPlatformFeeType(_feeType); + + IPlatformFee.PlatformFeeType updatedFeeType = tokenContract.getPlatformFeeType(); + assertTrue(updatedFeeType == _feeType); + } + + function test_revert_setPlatformFeeInfo_ExceedsMaxBps() public { + address _platformFeeRecipient = address(0x123); + uint256 _platformFeeBps = 10001; + + vm.expectRevert("exceeds MAX_BPS"); + vm.prank(deployerSigner); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } + + function test_revert_setPlatformFeeInfo_NotAuthorized() public { + bytes32 role = tokenContract.DEFAULT_ADMIN_ROLE(); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(address(0x1)), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ); + vm.prank(address(0x1)); + tokenContract.setPlatformFeeInfo(address(1), 1000); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(address(0x1)), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ); + vm.prank(address(0x1)); + tokenContract.setFlatPlatformFeeInfo(address(1), 1000); + } + + function test_event_platformFeeInfo() public { + address _platformFeeRecipient = address(0x123); + uint256 _platformFeeBps = 1000; + + vm.expectEmit(true, true, true, true); + emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); + + vm.prank(deployerSigner); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: contract metadata + //////////////////////////////////////////////////////////////*/ + + function test_state_setContractURI() public { + string memory uri = "uri_string"; + + vm.prank(deployerSigner); + tokenContract.setContractURI(uri); + + string memory _contractURI = tokenContract.contractURI(); + + assertEq(_contractURI, uri); + } + + function test_revert_setContractURI() public { + bytes32 role = tokenContract.DEFAULT_ADMIN_ROLE(); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(address(0x1)), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ); + vm.prank(address(0x1)); + tokenContract.setContractURI(""); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: setTokenURI + //////////////////////////////////////////////////////////////*/ + + function test_setTokenURI_state() public { + string memory uri = "uri_string"; + + vm.prank(deployerSigner); + tokenContract.setTokenURI(0, uri); + + string memory _tokenURI = tokenContract.uri(0); + + assertEq(_tokenURI, uri); + } + + function test_setTokenURI_revert_NotAuthorized() public { + string memory uri = "uri_string"; + + vm.expectRevert(abi.encodeWithSelector(NFTMetadata.NFTMetadataUnauthorized.selector)); + vm.prank(address(0x1)); + tokenContract.setTokenURI(0, uri); + } + + function test_setTokenURI_revert_Frozen() public { + string memory uri = "uri_string"; + + vm.startPrank(deployerSigner); + tokenContract.freezeMetadata(); + + vm.expectRevert(abi.encodeWithSelector(NFTMetadata.NFTMetadataFrozen.selector, 0)); + tokenContract.setTokenURI(0, uri); + } +} diff --git a/src/test/token/TokenERC20.t.sol b/src/test/token/TokenERC20.t.sol new file mode 100644 index 000000000..03e44d81c --- /dev/null +++ b/src/test/token/TokenERC20.t.sol @@ -0,0 +1,424 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { TokenERC20 } from "contracts/prebuilts/token/TokenERC20.sol"; + +// Test imports + +import "../utils/BaseTest.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; + +contract TokenERC20Test is BaseTest { + using Strings for uint256; + + event TokensMinted(address indexed mintedTo, uint256 quantityMinted); + event TokensMintedWithSignature( + address indexed signer, + address indexed mintedTo, + TokenERC20.MintRequest mintRequest + ); + + event PrimarySaleRecipientUpdated(address indexed recipient); + event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps); + + TokenERC20 public tokenContract; + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + bytes32 internal permitTypehash; + + bytes private emptyEncodedBytes = abi.encode("", ""); + + TokenERC20.MintRequest _mintrequest; + bytes _signature; + + address internal deployerSigner; + address internal recipient; + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + deployerSigner = signer; + recipient = address(0x123); + tokenContract = TokenERC20(getContract("TokenERC20")); + + erc20.mint(deployerSigner, 1_000); + vm.deal(deployerSigner, 1_000); + + erc20.mint(recipient, 1_000); + vm.deal(recipient, 1_000); + + typehashMintRequest = keccak256( + "MintRequest(address to,address primarySaleRecipient,uint256 quantity,uint256 price,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + nameHash = keccak256(bytes(NAME)); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256( + abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(tokenContract)) + ); + permitTypehash = keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ); + + // construct default mintrequest + _mintrequest.to = recipient; + _mintrequest.primarySaleRecipient = saleRecipient; + _mintrequest.quantity = 100; + _mintrequest.price = 0; + _mintrequest.currency = address(0); + _mintrequest.validityStartTimestamp = 1000; + _mintrequest.validityEndTimestamp = 2000; + _mintrequest.uid = bytes32(0); + + _signature = signMintRequest(_mintrequest, privateKey); + } + + function signMintRequest( + TokenERC20.MintRequest memory _request, + uint256 _privateKey + ) internal view returns (bytes memory) { + bytes memory encodedRequest = abi.encode( + typehashMintRequest, + _request.to, + _request.primarySaleRecipient, + _request.quantity, + _request.price, + _request.currency, + _request.validityStartTimestamp, + _request.validityEndTimestamp, + _request.uid + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, typedDataHash); + bytes memory sig = abi.encodePacked(r, s, v); + + return sig; + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `mintWithSignature` + //////////////////////////////////////////////////////////////*/ + + function test_state_mintWithSignature_ZeroPrice() public { + vm.warp(1000); + + // initial balances and state + uint256 currentTotalSupply = tokenContract.totalSupply(); + uint256 currentBalanceOfRecipient = tokenContract.balanceOf(recipient); + + // mint with signature + vm.prank(recipient); + tokenContract.mintWithSignature(_mintrequest, _signature); + + // check state after minting + assertEq(tokenContract.totalSupply(), currentTotalSupply + _mintrequest.quantity); + assertEq(tokenContract.balanceOf(recipient), currentBalanceOfRecipient + _mintrequest.quantity); + } + + function test_state_mintWithSignature_NonZeroPrice_ERC20() public { + vm.warp(1000); + + // update mintrequest data + _mintrequest.price = 1; + _mintrequest.currency = address(erc20); + _signature = signMintRequest(_mintrequest, privateKey); + + // approve erc20 tokens to tokenContract + vm.prank(recipient); + erc20.approve(address(tokenContract), _mintrequest.price); + + // initial balances and state + uint256 currentTotalSupply = tokenContract.totalSupply(); + uint256 currentBalanceOfRecipient = tokenContract.balanceOf(recipient); + + uint256 erc20BalanceOfSeller = erc20.balanceOf(address(saleRecipient)); + uint256 erc20BalanceOfRecipient = erc20.balanceOf(address(recipient)); + + // mint with signature + vm.prank(recipient); + tokenContract.mintWithSignature(_mintrequest, _signature); + + // check state after minting + assertEq(tokenContract.totalSupply(), currentTotalSupply + _mintrequest.quantity); + assertEq(tokenContract.balanceOf(recipient), currentBalanceOfRecipient + _mintrequest.quantity); + + // check erc20 balances after minting + uint256 _platformFees = (_mintrequest.price * platformFeeBps) / MAX_BPS; + assertEq(erc20.balanceOf(recipient), erc20BalanceOfRecipient - _mintrequest.price); + assertEq(erc20.balanceOf(address(saleRecipient)), erc20BalanceOfSeller + _mintrequest.price - _platformFees); + } + + function test_state_mintWithSignature_NonZeroPrice_NativeToken() public { + vm.warp(1000); + + // update mintrequest data + _mintrequest.price = 1; + _mintrequest.currency = address(NATIVE_TOKEN); + _signature = signMintRequest(_mintrequest, privateKey); + + // initial balances and state + uint256 currentTotalSupply = tokenContract.totalSupply(); + uint256 currentBalanceOfRecipient = tokenContract.balanceOf(recipient); + + uint256 etherBalanceOfSeller = address(saleRecipient).balance; + uint256 etherBalanceOfRecipient = address(recipient).balance; + + // mint with signature + vm.prank(recipient); + tokenContract.mintWithSignature{ value: _mintrequest.price }(_mintrequest, _signature); + + // check state after minting + assertEq(tokenContract.totalSupply(), currentTotalSupply + _mintrequest.quantity); + assertEq(tokenContract.balanceOf(recipient), currentBalanceOfRecipient + _mintrequest.quantity); + + // check balances after minting + uint256 _platformFees = (_mintrequest.price * platformFeeBps) / MAX_BPS; + assertEq(address(recipient).balance, etherBalanceOfRecipient - _mintrequest.price); + assertEq(address(saleRecipient).balance, etherBalanceOfSeller + _mintrequest.price - _platformFees); + } + + function test_revert_mintWithSignature_MustSendTotalPrice() public { + vm.warp(1000); + + _mintrequest.price = 1; + _mintrequest.currency = address(NATIVE_TOKEN); + _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(recipient); + vm.expectRevert("must send total price."); + tokenContract.mintWithSignature{ value: 0 }(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_MsgValueNotZero() public { + vm.warp(1000); + + _mintrequest.price = 1; + _mintrequest.currency = address(erc20); + _signature = signMintRequest(_mintrequest, privateKey); + + // shouldn't send native-token when it is not the currency + vm.prank(recipient); + vm.expectRevert("msg value not zero"); + tokenContract.mintWithSignature{ value: 1 }(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_InvalidSignature() public { + vm.warp(1000); + + uint256 incorrectKey = 3456; + _signature = signMintRequest(_mintrequest, incorrectKey); + + vm.prank(recipient); + vm.expectRevert("invalid signature"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_RequestExpired() public { + _signature = signMintRequest(_mintrequest, privateKey); + + // warp time more out of range + vm.warp(3000); + + vm.prank(recipient); + vm.expectRevert("request expired"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_RecipientUndefined() public { + vm.warp(1000); + + _mintrequest.to = address(0); + _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(recipient); + vm.expectRevert("recipient undefined"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_ZeroQuantity() public { + vm.warp(1000); + + _mintrequest.quantity = 0; + _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(recipient); + vm.expectRevert("zero quantity"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_event_mintWithSignature() public { + vm.warp(1000); + + vm.expectEmit(true, true, true, true); + emit TokensMintedWithSignature(deployerSigner, recipient, _mintrequest); + + // mint with signature + vm.prank(recipient); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `mintTo` + //////////////////////////////////////////////////////////////*/ + + function test_state_mintTo() public { + uint256 _amount = 100; + + uint256 currentTotalSupply = tokenContract.totalSupply(); + uint256 currentBalanceOfRecipient = tokenContract.balanceOf(recipient); + + vm.prank(deployerSigner); + tokenContract.mintTo(recipient, _amount); + + assertEq(tokenContract.totalSupply(), currentTotalSupply + _amount); + assertEq(tokenContract.balanceOf(recipient), currentBalanceOfRecipient + _amount); + } + + function test_revert_mintTo_NotAuthorized() public { + uint256 _amount = 100; + + vm.expectRevert("not minter."); + vm.prank(address(0x1)); + tokenContract.mintTo(recipient, _amount); + } + + function test_event_mintTo() public { + uint256 _amount = 100; + + vm.expectEmit(true, true, true, true); + emit TokensMinted(recipient, _amount); + + // mint + vm.prank(deployerSigner); + tokenContract.mintTo(recipient, _amount); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: primary sale + //////////////////////////////////////////////////////////////*/ + + function test_state_setPrimarySaleRecipient() public { + address _primarySaleRecipient = address(0x123); + + vm.prank(deployerSigner); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + + address recipient_ = tokenContract.primarySaleRecipient(); + assertEq(recipient_, _primarySaleRecipient); + } + + function test_revert_setPrimarySaleRecipient_NotAuthorized() public { + address _primarySaleRecipient = address(0x123); + bytes32 role = tokenContract.DEFAULT_ADMIN_ROLE(); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(address(0x1)), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ); + vm.prank(address(0x1)); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + } + + function test_event_setPrimarySaleRecipient() public { + address _primarySaleRecipient = address(0x123); + + vm.expectEmit(true, true, true, true); + emit PrimarySaleRecipientUpdated(_primarySaleRecipient); + + vm.prank(deployerSigner); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: platform fee + //////////////////////////////////////////////////////////////*/ + + function test_state_setPlatformFeeInfo() public { + address _platformFeeRecipient = address(0x123); + uint256 _platformFeeBps = 1000; + + vm.prank(deployerSigner); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + + (address recipient_, uint16 bps) = tokenContract.getPlatformFeeInfo(); + assertEq(_platformFeeRecipient, recipient_); + assertEq(_platformFeeBps, bps); + } + + function test_revert_setPlatformFeeInfo_ExceedsMaxBps() public { + address _platformFeeRecipient = address(0x123); + uint256 _platformFeeBps = 10001; + + vm.expectRevert("exceeds MAX_BPS"); + vm.prank(deployerSigner); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } + + function test_revert_setPlatformFeeInfo_NotAuthorized() public { + bytes32 role = tokenContract.DEFAULT_ADMIN_ROLE(); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(address(0x1)), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ); + vm.prank(address(0x1)); + tokenContract.setPlatformFeeInfo(address(1), 1000); + } + + function test_event_platformFeeInfo() public { + address _platformFeeRecipient = address(0x123); + uint256 _platformFeeBps = 1000; + + vm.expectEmit(true, true, true, true); + emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); + + vm.prank(deployerSigner); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: contract metadata + //////////////////////////////////////////////////////////////*/ + + function test_state_setContractURI() public { + string memory uri = "uri_string"; + + vm.prank(deployerSigner); + tokenContract.setContractURI(uri); + + string memory _contractURI = tokenContract.contractURI(); + + assertEq(_contractURI, uri); + } + + function test_revert_setContractURI_NotAuthorized() public { + bytes32 role = tokenContract.DEFAULT_ADMIN_ROLE(); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(address(0x1)), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ); + vm.prank(address(0x1)); + tokenContract.setContractURI(""); + } +} diff --git a/src/test/token/TokenERC721.t.sol b/src/test/token/TokenERC721.t.sol new file mode 100644 index 000000000..bd9c0519a --- /dev/null +++ b/src/test/token/TokenERC721.t.sol @@ -0,0 +1,692 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { TokenERC721, NFTMetadata } from "contracts/prebuilts/token/TokenERC721.sol"; + +// Test imports + +import "../utils/BaseTest.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; + +contract TokenERC721Test is BaseTest { + using Strings for uint256; + + event TokensMinted(address indexed mintedTo, uint256 indexed tokenIdMinted, string uri); + event TokensMintedWithSignature( + address indexed signer, + address indexed mintedTo, + uint256 indexed tokenIdMinted, + TokenERC721.MintRequest mintRequest + ); + event OwnerUpdated(address indexed prevOwner, address indexed newOwner); + event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps); + event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, uint256 royaltyBps); + event PrimarySaleRecipientUpdated(address indexed recipient); + event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps); + + TokenERC721 public tokenContract; + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + bytes private emptyEncodedBytes = abi.encode("", ""); + + TokenERC721.MintRequest _mintrequest; + bytes _signature; + + address internal deployerSigner; + address internal recipient; + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + deployerSigner = signer; + recipient = address(0x123); + tokenContract = TokenERC721(getContract("TokenERC721")); + + erc20.mint(deployerSigner, 1_000); + vm.deal(deployerSigner, 1_000); + + erc20.mint(recipient, 1_000); + vm.deal(recipient, 1_000); + + typehashMintRequest = keccak256( + "MintRequest(address to,address royaltyRecipient,uint256 royaltyBps,address primarySaleRecipient,string uri,uint256 price,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + nameHash = keccak256(bytes("TokenERC721")); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256( + abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(tokenContract)) + ); + + // construct default mintrequest + _mintrequest.to = recipient; + _mintrequest.royaltyRecipient = royaltyRecipient; + _mintrequest.royaltyBps = royaltyBps; + _mintrequest.primarySaleRecipient = saleRecipient; + _mintrequest.uri = "ipfs://"; + _mintrequest.price = 0; + _mintrequest.currency = address(0); + _mintrequest.validityStartTimestamp = 1000; + _mintrequest.validityEndTimestamp = 2000; + _mintrequest.uid = bytes32(0); + + _signature = signMintRequest(_mintrequest, privateKey); + } + + function signMintRequest( + TokenERC721.MintRequest memory _request, + uint256 _privateKey + ) internal view returns (bytes memory) { + bytes memory encodedRequest = abi.encode( + typehashMintRequest, + _request.to, + _request.royaltyRecipient, + _request.royaltyBps, + _request.primarySaleRecipient, + keccak256(bytes(_request.uri)), + _request.price, + _request.currency, + _request.validityStartTimestamp, + _request.validityEndTimestamp, + _request.uid + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, typedDataHash); + bytes memory sig = abi.encodePacked(r, s, v); + + return sig; + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `mintWithSignature` + //////////////////////////////////////////////////////////////*/ + + function test_state_mintWithSignature_ZeroPrice() public { + vm.warp(1000); + + // initial balances and state + uint256 nextTokenId = tokenContract.nextTokenIdToMint(); + uint256 currentTotalSupply = tokenContract.totalSupply(); + uint256 currentBalanceOfRecipient = tokenContract.balanceOf(recipient); + + // mint with signature + vm.prank(recipient); + tokenContract.mintWithSignature(_mintrequest, _signature); + + // check state after minting + assertEq(tokenContract.nextTokenIdToMint(), nextTokenId + 1); + assertEq(tokenContract.tokenURI(nextTokenId), string(_mintrequest.uri)); + assertEq(tokenContract.totalSupply(), currentTotalSupply + 1); + assertEq(tokenContract.balanceOf(recipient), currentBalanceOfRecipient + 1); + assertEq(tokenContract.ownerOf(nextTokenId), recipient); + } + + function test_state_mintWithSignature_NonZeroPrice_ERC20() public { + vm.warp(1000); + + // update mintrequest data + _mintrequest.price = 1; + _mintrequest.currency = address(erc20); + _signature = signMintRequest(_mintrequest, privateKey); + + // approve erc20 tokens to tokenContract + vm.prank(recipient); + erc20.approve(address(tokenContract), 1); + + // initial balances and state + uint256 nextTokenId = tokenContract.nextTokenIdToMint(); + uint256 currentTotalSupply = tokenContract.totalSupply(); + uint256 currentBalanceOfRecipient = tokenContract.balanceOf(recipient); + + uint256 erc20BalanceOfSeller = erc20.balanceOf(address(saleRecipient)); + uint256 erc20BalanceOfRecipient = erc20.balanceOf(address(recipient)); + + // mint with signature + vm.prank(recipient); + tokenContract.mintWithSignature(_mintrequest, _signature); + + // check state after minting + assertEq(tokenContract.nextTokenIdToMint(), nextTokenId + 1); + assertEq(tokenContract.tokenURI(nextTokenId), string(_mintrequest.uri)); + assertEq(tokenContract.totalSupply(), currentTotalSupply + 1); + assertEq(tokenContract.balanceOf(recipient), currentBalanceOfRecipient + 1); + assertEq(tokenContract.ownerOf(nextTokenId), recipient); + + // check erc20 balances after minting + uint256 _platformFees = (_mintrequest.price * platformFeeBps) / MAX_BPS; + assertEq(erc20.balanceOf(recipient), erc20BalanceOfRecipient - _mintrequest.price); + assertEq(erc20.balanceOf(address(saleRecipient)), erc20BalanceOfSeller + _mintrequest.price - _platformFees); + } + + function test_state_mintWithSignature_NonZeroPrice_NativeToken() public { + vm.warp(1000); + + // update mintrequest data + _mintrequest.price = 1; + _mintrequest.currency = address(NATIVE_TOKEN); + _signature = signMintRequest(_mintrequest, privateKey); + + // initial balances and state + uint256 nextTokenId = tokenContract.nextTokenIdToMint(); + uint256 currentTotalSupply = tokenContract.totalSupply(); + uint256 currentBalanceOfRecipient = tokenContract.balanceOf(recipient); + + uint256 etherBalanceOfSeller = address(saleRecipient).balance; + uint256 etherBalanceOfRecipient = address(recipient).balance; + + // mint with signature + vm.prank(recipient); + tokenContract.mintWithSignature{ value: 1 }(_mintrequest, _signature); + + // check state after minting + assertEq(tokenContract.nextTokenIdToMint(), nextTokenId + 1); + assertEq(tokenContract.tokenURI(nextTokenId), string(_mintrequest.uri)); + assertEq(tokenContract.totalSupply(), currentTotalSupply + 1); + assertEq(tokenContract.balanceOf(recipient), currentBalanceOfRecipient + 1); + assertEq(tokenContract.ownerOf(nextTokenId), recipient); + + // check erc20 balances after minting + uint256 _platformFees = (_mintrequest.price * platformFeeBps) / MAX_BPS; + assertEq(address(recipient).balance, etherBalanceOfRecipient - _mintrequest.price); + assertEq(address(saleRecipient).balance, etherBalanceOfSeller + _mintrequest.price - _platformFees); + } + + function test_revert_mintWithSignature_MustSendTotalPrice() public { + vm.warp(1000); + + _mintrequest.price = 1; + _mintrequest.currency = address(NATIVE_TOKEN); + _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(recipient); + vm.expectRevert("must send total price."); + tokenContract.mintWithSignature{ value: 0 }(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_MsgValueNotZero() public { + vm.warp(1000); + + _mintrequest.price = 1; + _mintrequest.currency = address(erc20); + _signature = signMintRequest(_mintrequest, privateKey); + + // shouldn't send native-token when it is not the currency + vm.prank(recipient); + vm.expectRevert("msg value not zero"); + tokenContract.mintWithSignature{ value: 1 }(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_InvalidSignature() public { + vm.warp(1000); + + uint256 incorrectKey = 3456; + _signature = signMintRequest(_mintrequest, incorrectKey); + + vm.prank(recipient); + vm.expectRevert("invalid signature"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_RequestExpired() public { + _signature = signMintRequest(_mintrequest, privateKey); + + // warp time more out of range + vm.warp(3000); + + vm.prank(recipient); + vm.expectRevert("request expired"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_RecipientUndefined() public { + vm.warp(1000); + + _mintrequest.to = address(0); + _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(recipient); + vm.expectRevert("recipient undefined"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_event_mintWithSignature() public { + vm.warp(1000); + + vm.expectEmit(true, true, true, true); + emit TokensMintedWithSignature(deployerSigner, recipient, 0, _mintrequest); + + // mint with signature + vm.prank(recipient); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `mintTo` + //////////////////////////////////////////////////////////////*/ + + function test_state_mintTo() public { + string memory _tokenURI = "tokenURI"; + + uint256 nextTokenId = tokenContract.nextTokenIdToMint(); + uint256 currentTotalSupply = tokenContract.totalSupply(); + uint256 currentBalanceOfRecipient = tokenContract.balanceOf(recipient); + + vm.prank(deployerSigner); + tokenContract.mintTo(recipient, _tokenURI); + + assertEq(tokenContract.nextTokenIdToMint(), nextTokenId + 1); + assertEq(tokenContract.tokenURI(nextTokenId), _tokenURI); + assertEq(tokenContract.totalSupply(), currentTotalSupply + 1); + assertEq(tokenContract.balanceOf(recipient), currentBalanceOfRecipient + 1); + assertEq(tokenContract.ownerOf(nextTokenId), recipient); + } + + function test_revert_mintTo_NotAuthorized() public { + string memory _tokenURI = "tokenURI"; + bytes32 role = keccak256("MINTER_ROLE"); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(address(0x1)), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ); + vm.prank(address(0x1)); + tokenContract.mintTo(recipient, _tokenURI); + } + + function test_revert_mintTo_emptyURI() public { + // mint + vm.prank(deployerSigner); + vm.expectRevert("empty uri."); + tokenContract.mintTo(recipient, ""); + } + + function test_event_mintTo() public { + string memory _tokenURI = "tokenURI"; + + vm.expectEmit(true, true, true, true); + emit TokensMinted(recipient, 0, _tokenURI); + + // mint + vm.prank(deployerSigner); + tokenContract.mintTo(recipient, _tokenURI); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `burn` + //////////////////////////////////////////////////////////////*/ + + function test_state_burn_TokenOwner() public { + string memory _tokenURI = "tokenURI"; + + uint256 nextTokenId = tokenContract.nextTokenIdToMint(); + uint256 currentTotalSupply = tokenContract.totalSupply(); + uint256 currentBalanceOfRecipient = tokenContract.balanceOf(recipient); + + vm.prank(deployerSigner); + tokenContract.mintTo(recipient, _tokenURI); + + vm.prank(recipient); + tokenContract.burn(nextTokenId); + + assertEq(tokenContract.nextTokenIdToMint(), nextTokenId + 1); + assertEq(tokenContract.tokenURI(nextTokenId), _tokenURI); + assertEq(tokenContract.totalSupply(), currentTotalSupply); + assertEq(tokenContract.balanceOf(recipient), currentBalanceOfRecipient); + + vm.expectRevert("ERC721: invalid token ID"); + assertEq(tokenContract.ownerOf(nextTokenId), address(0)); + } + + function test_state_burn_TokenOperator() public { + string memory _tokenURI = "tokenURI"; + + address operator = address(0x789); + + uint256 nextTokenId = tokenContract.nextTokenIdToMint(); + uint256 currentTotalSupply = tokenContract.totalSupply(); + uint256 currentBalanceOfRecipient = tokenContract.balanceOf(recipient); + + vm.prank(deployerSigner); + tokenContract.mintTo(recipient, _tokenURI); + + vm.prank(recipient); + tokenContract.setApprovalForAll(operator, true); + + vm.prank(operator); + tokenContract.burn(nextTokenId); + + assertEq(tokenContract.nextTokenIdToMint(), nextTokenId + 1); + assertEq(tokenContract.tokenURI(nextTokenId), _tokenURI); + assertEq(tokenContract.totalSupply(), currentTotalSupply); + assertEq(tokenContract.balanceOf(recipient), currentBalanceOfRecipient); + + vm.expectRevert("ERC721: invalid token ID"); + assertEq(tokenContract.ownerOf(nextTokenId), address(0)); + } + + function test_revert_burn_NotOwnerNorApproved() public { + string memory _tokenURI = "tokenURI"; + + uint256 nextTokenId = tokenContract.nextTokenIdToMint(); + + vm.prank(deployerSigner); + tokenContract.mintTo(recipient, _tokenURI); + + vm.prank(address(0x789)); + vm.expectRevert("ERC721Burnable: caller is not owner nor approved"); + tokenContract.burn(nextTokenId); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: owner + //////////////////////////////////////////////////////////////*/ + + function test_state_setOwner() public { + address newOwner = address(0x123); + bytes32 role = tokenContract.DEFAULT_ADMIN_ROLE(); + + vm.prank(deployerSigner); + tokenContract.grantRole(role, newOwner); + + vm.prank(deployerSigner); + tokenContract.setOwner(newOwner); + + assertEq(tokenContract.owner(), newOwner); + } + + function test_revert_setOwner_NotModuleAdmin() public { + vm.expectRevert("new owner not module admin."); + vm.prank(deployerSigner); + tokenContract.setOwner(address(0x1234)); + } + + function test_event_setOwner() public { + address newOwner = address(0x123); + bytes32 role = tokenContract.DEFAULT_ADMIN_ROLE(); + + vm.startPrank(deployerSigner); + tokenContract.grantRole(role, newOwner); + + vm.expectEmit(true, true, true, true); + emit OwnerUpdated(deployerSigner, newOwner); + + tokenContract.setOwner(newOwner); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: royalty + //////////////////////////////////////////////////////////////*/ + + function test_state_setDefaultRoyaltyInfo() public { + address _royaltyRecipient = address(0x123); + uint256 _royaltyBps = 1000; + + vm.prank(deployerSigner); + tokenContract.setDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + + (address newRoyaltyRecipient, uint256 newRoyaltyBps) = tokenContract.getDefaultRoyaltyInfo(); + assertEq(newRoyaltyRecipient, _royaltyRecipient); + assertEq(newRoyaltyBps, _royaltyBps); + + (address receiver, uint256 royaltyAmount) = tokenContract.royaltyInfo(0, 100); + assertEq(receiver, _royaltyRecipient); + assertEq(royaltyAmount, (100 * 1000) / 10_000); + } + + function test_revert_setDefaultRoyaltyInfo_NotAuthorized() public { + address _royaltyRecipient = address(0x123); + uint256 _royaltyBps = 1000; + bytes32 role = tokenContract.DEFAULT_ADMIN_ROLE(); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(address(0x1)), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ); + vm.prank(address(0x1)); + tokenContract.setDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + } + + function test_revert_setDefaultRoyaltyInfo_ExceedsRoyaltyBps() public { + address _royaltyRecipient = address(0x123); + uint256 _royaltyBps = 10001; + + vm.expectRevert("exceed royalty bps"); + vm.prank(deployerSigner); + tokenContract.setDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + } + + function test_state_setRoyaltyInfoForToken() public { + uint256 _tokenId = 1; + address _recipient = address(0x123); + uint256 _bps = 1000; + + vm.prank(deployerSigner); + tokenContract.setRoyaltyInfoForToken(_tokenId, _recipient, _bps); + + (address receiver, uint256 royaltyAmount) = tokenContract.royaltyInfo(_tokenId, 100); + assertEq(receiver, _recipient); + assertEq(royaltyAmount, (100 * 1000) / 10_000); + } + + function test_revert_setRoyaltyInfo_NotAuthorized() public { + uint256 _tokenId = 1; + address _recipient = address(0x123); + uint256 _bps = 1000; + bytes32 role = tokenContract.DEFAULT_ADMIN_ROLE(); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(address(0x1)), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ); + vm.prank(address(0x1)); + tokenContract.setRoyaltyInfoForToken(_tokenId, _recipient, _bps); + } + + function test_revert_setRoyaltyInfoForToken_ExceedsRoyaltyBps() public { + uint256 _tokenId = 1; + address _recipient = address(0x123); + uint256 _bps = 10001; + + vm.expectRevert("exceed royalty bps"); + vm.prank(deployerSigner); + tokenContract.setRoyaltyInfoForToken(_tokenId, _recipient, _bps); + } + + function test_event_defaultRoyalty() public { + address _royaltyRecipient = address(0x123); + uint256 _royaltyBps = 1000; + + vm.expectEmit(true, true, true, true); + emit DefaultRoyalty(_royaltyRecipient, _royaltyBps); + + vm.prank(deployerSigner); + tokenContract.setDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + } + + function test_event_royaltyForToken() public { + uint256 _tokenId = 1; + address _recipient = address(0x123); + uint256 _bps = 1000; + + vm.expectEmit(true, true, true, true); + emit RoyaltyForToken(_tokenId, _recipient, _bps); + + vm.prank(deployerSigner); + tokenContract.setRoyaltyInfoForToken(_tokenId, _recipient, _bps); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: primary sale + //////////////////////////////////////////////////////////////*/ + + function test_state_setPrimarySaleRecipient() public { + address _primarySaleRecipient = address(0x123); + + vm.prank(deployerSigner); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + + address recipient_ = tokenContract.primarySaleRecipient(); + assertEq(recipient_, _primarySaleRecipient); + } + + function test_revert_setPrimarySaleRecipient_NotAuthorized() public { + address _primarySaleRecipient = address(0x123); + bytes32 role = tokenContract.DEFAULT_ADMIN_ROLE(); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(address(0x1)), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ); + vm.prank(address(0x1)); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + } + + function test_event_setPrimarySaleRecipient() public { + address _primarySaleRecipient = address(0x123); + + vm.expectEmit(true, true, true, true); + emit PrimarySaleRecipientUpdated(_primarySaleRecipient); + + vm.prank(deployerSigner); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: platform fee + //////////////////////////////////////////////////////////////*/ + + function test_state_setPlatformFeeInfo() public { + address _platformFeeRecipient = address(0x123); + uint256 _platformFeeBps = 1000; + + vm.prank(deployerSigner); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + + (address recipient_, uint16 bps) = tokenContract.getPlatformFeeInfo(); + assertEq(_platformFeeRecipient, recipient_); + assertEq(_platformFeeBps, bps); + } + + function test_revert_setPlatformFeeInfo_ExceedsMaxBps() public { + address _platformFeeRecipient = address(0x123); + uint256 _platformFeeBps = 10001; + + vm.expectRevert("exceeds MAX_BPS"); + vm.prank(deployerSigner); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } + + function test_revert_setPlatformFeeInfo_NotAuthorized() public { + bytes32 role = tokenContract.DEFAULT_ADMIN_ROLE(); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(address(0x1)), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ); + vm.prank(address(0x1)); + tokenContract.setPlatformFeeInfo(address(1), 1000); + } + + function test_event_platformFeeInfo() public { + address _platformFeeRecipient = address(0x123); + uint256 _platformFeeBps = 1000; + + vm.expectEmit(true, true, true, true); + emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); + + vm.prank(deployerSigner); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: contract metadata + //////////////////////////////////////////////////////////////*/ + + function test_state_setContractURI() public { + string memory uri = "uri_string"; + + vm.prank(deployerSigner); + tokenContract.setContractURI(uri); + + string memory _contractURI = tokenContract.contractURI(); + + assertEq(_contractURI, uri); + } + + function test_revert_setContractURI() public { + bytes32 role = tokenContract.DEFAULT_ADMIN_ROLE(); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(address(0x1)), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ); + vm.prank(address(0x1)); + tokenContract.setContractURI(""); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: setTokenURI + //////////////////////////////////////////////////////////////*/ + + function test_setTokenURI_state() public { + string memory uri = "uri_string"; + + vm.prank(deployerSigner); + tokenContract.setTokenURI(0, uri); + + string memory _tokenURI = tokenContract.tokenURI(0); + + assertEq(_tokenURI, uri); + } + + function test_setTokenURI_revert_NotAuthorized() public { + string memory uri = "uri_string"; + + vm.expectRevert(abi.encodeWithSelector(NFTMetadata.NFTMetadataUnauthorized.selector)); + vm.prank(address(0x1)); + tokenContract.setTokenURI(0, uri); + } + + function test_setTokenURI_revert_Frozen() public { + string memory uri = "uri_string"; + + vm.startPrank(deployerSigner); + tokenContract.freezeMetadata(); + + vm.expectRevert(abi.encodeWithSelector(NFTMetadata.NFTMetadataFrozen.selector, 0)); + tokenContract.setTokenURI(0, uri); + } +} diff --git a/src/test/tokenerc1155-BTT/burn-batch/burnBatch.t.sol b/src/test/tokenerc1155-BTT/burn-batch/burnBatch.t.sol new file mode 100644 index 000000000..273cb2db4 --- /dev/null +++ b/src/test/tokenerc1155-BTT/burn-batch/burnBatch.t.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract TokenERC1155Test_BurnBatch is BaseTest { + address public implementation; + address public proxy; + address public caller; + address public recipient; + string public uri; + uint256 public amount; + + MyTokenERC1155 internal tokenContract; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + caller = getActor(1); + recipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + uri = "uri"; + amount = 100; + + vm.prank(deployer); + tokenContract.grantRole(keccak256("MINTER_ROLE"), caller); + } + + function test_burn_whenNotOwnerNorApproved() public { + // mint two tokenIds + vm.startPrank(caller); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + vm.stopPrank(); + + uint256[] memory ids = new uint256[](2); + uint256[] memory amounts = new uint256[](2); + + ids[0] = 0; + ids[1] = 1; + amounts[0] = 10; + amounts[1] = 10; + + // burn + vm.expectRevert("ERC1155: caller is not owner nor approved."); + tokenContract.burnBatch(recipient, ids, amounts); + } + + function test_burn_whenOwner_invalidAmount() public { + // mint two tokenIds + vm.startPrank(caller); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + vm.stopPrank(); + + uint256[] memory ids = new uint256[](2); + uint256[] memory amounts = new uint256[](2); + + ids[0] = 0; + ids[1] = 1; + amounts[0] = 1000 ether; + amounts[1] = 10; + + // burn + vm.prank(recipient); + vm.expectRevert(); + tokenContract.burnBatch(recipient, ids, amounts); + } + + function test_burn_whenOwner() public { + // mint two tokenIds + vm.startPrank(caller); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + vm.stopPrank(); + + uint256[] memory ids = new uint256[](2); + uint256[] memory amounts = new uint256[](2); + + ids[0] = 0; + ids[1] = 1; + amounts[0] = 10; + amounts[1] = 10; + + // burn + vm.prank(recipient); + tokenContract.burnBatch(recipient, ids, amounts); + + assertEq(tokenContract.balanceOf(recipient, ids[0]), amount - amounts[0]); + assertEq(tokenContract.balanceOf(recipient, ids[1]), amount - amounts[1]); + } + + function test_burn_whenApproved() public { + // mint two tokenIds + vm.startPrank(caller); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + vm.stopPrank(); + + uint256[] memory ids = new uint256[](2); + uint256[] memory amounts = new uint256[](2); + + ids[0] = 0; + ids[1] = 1; + amounts[0] = 10; + amounts[1] = 10; + + vm.prank(recipient); + tokenContract.setApprovalForAll(caller, true); + + // burn + vm.prank(caller); + tokenContract.burnBatch(recipient, ids, amounts); + + assertEq(tokenContract.balanceOf(recipient, ids[0]), amount - amounts[0]); + assertEq(tokenContract.balanceOf(recipient, ids[1]), amount - amounts[1]); + } +} diff --git a/src/test/tokenerc1155-BTT/burn-batch/burnBatch.tree b/src/test/tokenerc1155-BTT/burn-batch/burnBatch.tree new file mode 100644 index 000000000..dca6fa537 --- /dev/null +++ b/src/test/tokenerc1155-BTT/burn-batch/burnBatch.tree @@ -0,0 +1,14 @@ +burnBatch( + address account, + uint256[] memory ids, + uint256[] memory values +) +├── when the caller isn't `account` or `account` hasn't approved tokens to caller +│ └── it should revert ✅ +└── when the caller is `account` with balances less than `values` for corresponding `ids` +│ └── it should revert ✅ +└── when the caller is `account` with balances greater than or equal to `values` +│ └── it should burn `values` amounts of `ids` tokens from account ✅ +└── when the `account` has approved `values` amount of tokens to caller + └── it should burn the token ✅ + diff --git a/src/test/tokenerc1155-BTT/burn/burn.t.sol b/src/test/tokenerc1155-BTT/burn/burn.t.sol new file mode 100644 index 000000000..1bf2575b9 --- /dev/null +++ b/src/test/tokenerc1155-BTT/burn/burn.t.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract TokenERC1155Test_Burn is BaseTest { + address public implementation; + address public proxy; + address public caller; + address public recipient; + string public uri; + uint256 public amount; + + MyTokenERC1155 internal tokenContract; + + event MetadataUpdate(uint256 _tokenId); + event TokensMinted(address indexed mintedTo, uint256 indexed tokenIdMinted, string uri); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + caller = getActor(1); + recipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + uri = "uri"; + amount = 100; + + vm.prank(deployer); + tokenContract.grantRole(keccak256("MINTER_ROLE"), caller); + } + + function test_burn_whenNotOwnerNorApproved() public { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + + // burn + vm.expectRevert("ERC1155: caller is not owner nor approved."); + tokenContract.burn(recipient, _tokenIdToMint, amount); + } + + function test_burn_whenOwner_invalidAmount() public { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + + // burn + vm.prank(recipient); + vm.expectRevert(); + tokenContract.burn(recipient, _tokenIdToMint, amount + 1); + } + + function test_burn_whenOwner() public { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + + // burn + vm.prank(recipient); + tokenContract.burn(recipient, _tokenIdToMint, amount); + + assertEq(tokenContract.balanceOf(recipient, _tokenIdToMint), 0); + } + + function test_burn_whenApproved() public { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + + vm.prank(recipient); + tokenContract.setApprovalForAll(caller, true); + + // burn + vm.prank(caller); + tokenContract.burn(recipient, _tokenIdToMint, amount); + + assertEq(tokenContract.balanceOf(recipient, _tokenIdToMint), 0); + } +} diff --git a/src/test/tokenerc1155-BTT/burn/burn.tree b/src/test/tokenerc1155-BTT/burn/burn.tree new file mode 100644 index 000000000..8232a832d --- /dev/null +++ b/src/test/tokenerc1155-BTT/burn/burn.tree @@ -0,0 +1,14 @@ +burn( + address account, + uint256 id, + uint256 value +) +├── when the caller isn't `account` or `account` hasn't approved tokens to caller +│ └── it should revert ✅ +└── when the caller is `account` with balance less than `value` +│ └── it should revert ✅ +└── when the caller is `account` with balance greater than or equal to `value` +│ └── it should burn `value` amount of `id` tokens from ✅ +└── when the `account` has approved `value` amount of tokens to caller + └── it should burn the token ✅ + diff --git a/src/test/tokenerc1155-BTT/initialize/initialize.t.sol b/src/test/tokenerc1155-BTT/initialize/initialize.t.sol new file mode 100644 index 000000000..5340ff595 --- /dev/null +++ b/src/test/tokenerc1155-BTT/initialize/initialize.t.sol @@ -0,0 +1,313 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { IPlatformFee } from "contracts/extension/interface/IPlatformFee.sol"; + +contract MyTokenERC1155 is TokenERC1155 { + function eip712NameHash() external view returns (bytes32) { + return _EIP712NameHash(); + } + + function eip712VersionHash() external view returns (bytes32) { + return _EIP712VersionHash(); + } +} + +contract TokenERC1155Test_Initialize is BaseTest { + address public implementation; + address public proxy; + + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + } + + function test_initialize_initializingImplementation() public { + vm.expectRevert("Initializable: contract is already initialized"); + TokenERC1155(implementation).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + modifier whenNotImplementation() { + _; + } + + function test_initialize_proxyAlreadyInitialized() public whenNotImplementation { + vm.expectRevert("Initializable: contract is already initialized"); + MyTokenERC1155(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + modifier whenProxyNotInitialized() { + proxy = address(new TWProxy(implementation, "")); + _; + } + + function test_initialize_exceedsMaxBps() public whenNotImplementation whenProxyNotInitialized { + vm.expectRevert("exceeds MAX_BPS"); + MyTokenERC1155(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + uint128(MAX_BPS) + 1, // platformFeeBps greater than MAX_BPS + platformFeeRecipient + ); + } + + modifier whenPlatformFeeBpsWithinMaxBps() { + _; + } + + function test_initialize() public whenNotImplementation whenProxyNotInitialized whenPlatformFeeBpsWithinMaxBps { + MyTokenERC1155(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + + // check state + MyTokenERC1155 tokenContract = MyTokenERC1155(proxy); + + assertEq(tokenContract.eip712NameHash(), keccak256(bytes("TokenERC1155"))); + assertEq(tokenContract.eip712VersionHash(), keccak256(bytes("1"))); + + address[] memory _trustedForwarders = forwarders(); + for (uint256 i = 0; i < _trustedForwarders.length; i++) { + assertTrue(tokenContract.isTrustedForwarder(_trustedForwarders[i])); + } + + assertEq(tokenContract.name(), NAME); + assertEq(tokenContract.symbol(), SYMBOL); + assertEq(tokenContract.contractURI(), CONTRACT_URI); + + (address _platformFeeRecipient, uint16 _platformFeeBps) = tokenContract.getPlatformFeeInfo(); + assertEq(_platformFeeBps, platformFeeBps); + assertEq(_platformFeeRecipient, platformFeeRecipient); + assertEq(tokenContract.platformFeeRecipient(), platformFeeRecipient); + assertEq(uint8(tokenContract.getPlatformFeeType()), uint8(IPlatformFee.PlatformFeeType.Bps)); + + (address _royaltyRecipient, uint16 _royaltyBps) = tokenContract.getDefaultRoyaltyInfo(); + (address _royaltyRecipientForToken, uint16 _royaltyBpsForToken) = tokenContract.getRoyaltyInfoForToken(1); // random tokenId + assertEq(_royaltyBps, royaltyBps); + assertEq(_royaltyRecipient, royaltyRecipient); + assertEq(_royaltyRecipient, _royaltyRecipientForToken); + assertEq(_royaltyBps, _royaltyBpsForToken); + + assertEq(tokenContract.primarySaleRecipient(), saleRecipient); + + assertEq(tokenContract.owner(), deployer); + assertTrue(tokenContract.hasRole(bytes32(0x00), deployer)); + assertTrue(tokenContract.hasRole(keccak256("TRANSFER_ROLE"), deployer)); + assertTrue(tokenContract.hasRole(keccak256("TRANSFER_ROLE"), address(0))); + assertTrue(tokenContract.hasRole(keccak256("MINTER_ROLE"), deployer)); + assertTrue(tokenContract.hasRole(keccak256("METADATA_ROLE"), deployer)); + assertEq(tokenContract.getRoleAdmin(keccak256("METADATA_ROLE")), keccak256("METADATA_ROLE")); + } + + function test_initialize_event_RoleGranted_DefaultAdmin() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _defaultAdminRole = bytes32(0x00); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_defaultAdminRole, deployer, deployer); + MyTokenERC1155(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleGranted_MinterRole() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _minterRole = keccak256("MINTER_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_minterRole, deployer, deployer); + MyTokenERC1155(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleGranted_TransferRole() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _transferRole = keccak256("TRANSFER_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_transferRole, deployer, deployer); + MyTokenERC1155(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleGranted_TransferRole_AddressZero() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _transferRole = keccak256("TRANSFER_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_transferRole, address(0), deployer); + MyTokenERC1155(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleGranted_MetadataRole() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _metadataRole = keccak256("METADATA_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_metadataRole, deployer, deployer); + MyTokenERC1155(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleAdminChanged_MetadataRole() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _metadataRole = keccak256("METADATA_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleAdminChanged(_metadataRole, bytes32(0x00), _metadataRole); + MyTokenERC1155(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } +} diff --git a/src/test/tokenerc1155-BTT/initialize/initialize.tree b/src/test/tokenerc1155-BTT/initialize/initialize.tree new file mode 100644 index 000000000..15c2ba936 --- /dev/null +++ b/src/test/tokenerc1155-BTT/initialize/initialize.tree @@ -0,0 +1,43 @@ +initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _primarySaleRecipient, + address _royaltyRecipient, + uint128 _royaltyBps, + uint128 _platformFeeBps, + address _platformFeeRecipient +) +├── when initializing the implementation contract (not proxy) +│ └── it should revert ✅ +└── when it is a proxy to the implementation + └── when it is already initialized + │ └── it should revert ✅ + └── when it is not initialized + └── when platformFeeBps is greater than MAX_BPS + │ └── it should revert ✅ + └── when platformFeeBps is less than or equal to MAX_BPS + └── it should correctly set EIP712 name hash and version hash ✅ + └── it should set trustedForwarder mapping to true for all addresses in `_trustedForwarders` ✅ + └── it should set name and symbol to `_name` and `_symbol` param values respectively ✅ + └── it should set contractURI to `_contractURI` param value ✅ + └── it should set platformFeeRecipient and platformFeeBps as `_platformFeeRecipient` and `_platformFeeBps` respectively ✅ + └── it should set platformFeeType to `Bps` ✅ + └── it should set royaltyRecipient and royaltyBps as `_royaltyRecipient` and `_royaltyBps` respectively ✅ + └── it should set primary sale recipient as `_primarySaleRecipient` param value ✅ + └── it should set _owner to `_defaultAdmin` param value ✅ + └── it should grant 0x00 (DEFAULT_ADMIN_ROLE) to `_defaultAdmin` address ✅ + └── it should emit RoleGranted event ✅ + └── it should grant MINTER_ROLE to `_defaultAdmin` address ✅ + └── it should emit RoleGranted event ✅ + └── it should grant TRANSFER_ROLE to `_defaultAdmin` address ✅ + └── it should emit RoleGranted event ✅ + └── it should grant TRANSFER_ROLE to address(0) ✅ + └── it should emit RoleGranted event ✅ + └── it should grant METADATA_ROLE to `_defaultAdmin` address ✅ + └── it should emit RoleGranted event ✅ + └── it should set METADATA_ROLE as role admin for METADATA_ROLE ✅ + └── it should emit RoleAdminChanged event ✅ + diff --git a/src/test/tokenerc1155-BTT/mint-to/mintTo.t.sol b/src/test/tokenerc1155-BTT/mint-to/mintTo.t.sol new file mode 100644 index 000000000..b0a8703bb --- /dev/null +++ b/src/test/tokenerc1155-BTT/mint-to/mintTo.t.sol @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract ERC1155ReceiverCompliant is IERC1155Receiver { + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes calldata data + ) external view virtual override returns (bytes4) { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external returns (bytes4) { + return this.onERC1155BatchReceived.selector; + } + + function supportsInterface(bytes4 interfaceId) external view returns (bool) {} +} + +contract TokenERC1155Test_MintTo is BaseTest { + address public implementation; + address public proxy; + address public caller; + address public recipient; + string public uri; + uint256 public amount; + + MyTokenERC1155 internal tokenContract; + ERC1155ReceiverCompliant internal erc1155ReceiverContract; + + event MetadataUpdate(uint256 _tokenId); + event TokensMinted(address indexed mintedTo, uint256 indexed tokenIdMinted, string uri, uint256 quantityMinted); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + erc1155ReceiverContract = new ERC1155ReceiverCompliant(); + caller = getActor(1); + recipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + amount = 100; + uri = "ipfs://uri"; + } + + function test_mintTo_notMinterRole() public { + vm.prank(caller); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(caller), 20), + " is missing role ", + Strings.toHexString(uint256(keccak256("MINTER_ROLE")), 32) + ) + ); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + } + + modifier whenMinterRole() { + vm.prank(deployer); + tokenContract.grantRole(keccak256("MINTER_ROLE"), caller); + _; + } + + // ================== + // ======= Test branch: `tokenId` input param is type(uint256).max + // ================== + + function test_mintTo_maxTokenId_EOA() public whenMinterRole { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + + // check state after + assertEq(_tokenIdToMint, 0); + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.balanceOf(recipient, _tokenIdToMint), amount); + assertEq(tokenContract.uri(_tokenIdToMint), uri); + } + + function test_mintTo_maxTokenId_EOA_TokensMintedEvent() public whenMinterRole { + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(true, true, false, true); + emit TokensMinted(recipient, _tokenIdToMint, uri, amount); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + } + + function test_mintTo_maxTokenId_EOA_MetadataUpdateEvent() public whenMinterRole { + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(false, false, false, true); + emit MetadataUpdate(_tokenIdToMint); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + } + + function test_mintTo_maxTokenId_EOA_uriAlreadyPresent() public whenMinterRole { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(deployer); + tokenContract.setTokenURI(_tokenIdToMint, "ipfs://uriOld"); + + // mint + vm.prank(caller); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + + // check state after + assertEq(_tokenIdToMint, 0); + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.balanceOf(recipient, _tokenIdToMint), amount); + assertEq(tokenContract.uri(_tokenIdToMint), "ipfs://uriOld"); + } + + function test_mintTo_maxTokenId_nonERC1155ReceiverContract() public whenMinterRole { + recipient = address(this); + vm.prank(caller); + vm.expectRevert(); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + } + + modifier whenERC1155Receiver() { + recipient = address(erc1155ReceiverContract); + _; + } + + function test_mintTo_maxTokenId_contract() public whenMinterRole whenERC1155Receiver { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + + // check state after + assertEq(_tokenIdToMint, 0); + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.balanceOf(recipient, _tokenIdToMint), amount); + assertEq(tokenContract.uri(_tokenIdToMint), uri); + } + + function test_mintTo_maxTokenId_contract_TokensMintedEvent() public whenMinterRole whenERC1155Receiver { + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(true, true, false, true); + emit TokensMinted(recipient, _tokenIdToMint, uri, amount); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + } + + function test_mintTo_maxTokenId_contract_MetadataUpdateEvent() public whenMinterRole whenERC1155Receiver { + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(false, false, false, true); + emit MetadataUpdate(_tokenIdToMint); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + } + + function test_mintTo_maxTokenId_contract_uriAlreadyPresent() public whenMinterRole whenERC1155Receiver { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(deployer); + tokenContract.setTokenURI(_tokenIdToMint, "ipfs://uriOld"); + + // mint + vm.prank(caller); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + + // check state after + assertEq(_tokenIdToMint, 0); + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.balanceOf(recipient, _tokenIdToMint), amount); + assertEq(tokenContract.uri(_tokenIdToMint), "ipfs://uriOld"); + } + + // ================== + // ======= Test branch: `tokenId` input param is not type(uint256).max + // ================== + + modifier whenNotMaxTokenId() { + // pre-mint the first token (i.e. id 0), so that nextTokenIdToMint is 1, for this code path + vm.prank(deployer); + tokenContract.mintTo(deployer, type(uint256).max, "uri1", amount); + _; + } + + function test_mintTo_EOA_invalidId() public whenMinterRole whenNotMaxTokenId { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + vm.expectRevert("invalid id"); + tokenContract.mintTo(recipient, _tokenIdToMint, uri, amount); + } + + modifier whenValidId() { + _; + } + + function test_mintTo_EOA() public whenMinterRole whenNotMaxTokenId whenValidId { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint() - 1; + + // mint + vm.prank(caller); + tokenContract.mintTo(recipient, _tokenIdToMint, uri, amount); + + // check state after + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.balanceOf(recipient, _tokenIdToMint), amount); + assertEq(tokenContract.uri(_tokenIdToMint), "uri1"); + } + + function test_mintTo_EOA_TokensMintedEvent() public whenMinterRole whenNotMaxTokenId whenValidId { + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint() - 1; + + vm.prank(caller); + vm.expectEmit(true, true, false, true); + emit TokensMinted(recipient, _tokenIdToMint, "uri1", amount); + tokenContract.mintTo(recipient, _tokenIdToMint, uri, amount); + } + + function test_mintTo_nonERC1155ReceiverContract() public whenMinterRole whenNotMaxTokenId whenValidId { + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint() - 1; + + recipient = address(this); + vm.prank(caller); + vm.expectRevert(); + tokenContract.mintTo(recipient, _tokenIdToMint, uri, amount); + } + + function test_mintTo_contract() public whenMinterRole whenNotMaxTokenId whenERC1155Receiver whenValidId { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint() - 1; + + // mint + vm.prank(caller); + tokenContract.mintTo(recipient, _tokenIdToMint, uri, amount); + + // check state after + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.balanceOf(recipient, _tokenIdToMint), amount); + assertEq(tokenContract.uri(_tokenIdToMint), "uri1"); + } + + function test_mintTo_contract_TokensMintedEvent() + public + whenMinterRole + whenNotMaxTokenId + whenERC1155Receiver + whenValidId + { + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint() - 1; + + vm.prank(caller); + vm.expectEmit(true, true, false, true); + emit TokensMinted(recipient, _tokenIdToMint, "uri1", amount); + tokenContract.mintTo(recipient, _tokenIdToMint, uri, amount); + } +} diff --git a/src/test/tokenerc1155-BTT/mint-to/mintTo.tree b/src/test/tokenerc1155-BTT/mint-to/mintTo.tree new file mode 100644 index 000000000..facd1e8eb --- /dev/null +++ b/src/test/tokenerc1155-BTT/mint-to/mintTo.tree @@ -0,0 +1,48 @@ +mintTo( + address _to, + uint256 _tokenId, + string calldata _uri, + uint256 _amount +) +├── when caller doesn't have MINTER_ROLE + │ └── it should revert ✅ + └── when caller has MINTER_ROLE + ├── when `_tokenId` is type(uint256).max + │ ├── when `_to` address is an EOA + │ │ └── it should mint tokenId equal to current value of `nextTokenIdToMint` ✅ + │ │ └── it should increment `nextTokenIdToMint` by 1 ✅ + │ │ └── it should mint the `_amount` number of tokens to the `_to` address ✅ + │ │ └── it should emit TokensMinted event ✅ + │ │ └── when there is no uri associated with the minted tokenId + │ │ └── it should set uri for minted tokenId equal to `_uri` ✅ + │ │ └── it should emit MetadataUpdate event ✅ + │ └── when `_to` address is a contract + │ ├── when `_to` address is non ERC1155Receiver implementer + │ │ └── it should revert ✅ + │ └── when `_to` address implements ERC1155Receiver + │ └── it should mint tokenId equal to current value of `nextTokenIdToMint` ✅ + │ └── it should increment `nextTokenIdToMint` by 1 ✅ + │ └── it should mint the `_amount` number of tokens to the `_to` address ✅ + │ └── it should emit TokensMinted event ✅ + │ └── when there is no uri associated with the minted tokenId + │ └── it should set uri for minted tokenId equal to `_uri` ✅ + │ └── it should emit MetadataUpdate event ✅ + │ + └── when `_tokenId` is not type(uint256).max + ├── when `_tokenId` is not less than nextTokenIdToMint + │ └── it should revert ✅ + └── when `_tokenId` is less than nextTokenIdToMint + ├── when `_to` address is an EOA + │ └── it should mint tokenId equal to current value of `nextTokenIdToMint` ✅ + │ └── it should increment `nextTokenIdToMint` by 1 ✅ + │ └── it should mint the `_amount` number of tokens to the `_to` address ✅ + │ └── it should emit TokensMinted event ✅ + └── when `_to` address is a contract + ├── when `_to` address is non ERC1155Receiver implementer + │ └── it should revert ✅ + └── when `_to` address implements ERC1155Receiver + └── it should mint tokenId equal to current value of `nextTokenIdToMint` ✅ + └── it should increment `nextTokenIdToMint` by 1 ✅ + └── it should mint the `_amount` number of tokens to the `_to` address ✅ + └── it should emit TokensMinted event ✅ + diff --git a/src/test/tokenerc1155-BTT/mint-with-signature/mintWithSignature.t.sol b/src/test/tokenerc1155-BTT/mint-with-signature/mintWithSignature.t.sol new file mode 100644 index 000000000..0452e427f --- /dev/null +++ b/src/test/tokenerc1155-BTT/mint-with-signature/mintWithSignature.t.sol @@ -0,0 +1,903 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { IPlatformFee } from "contracts/extension/interface/IPlatformFee.sol"; + +contract MyTokenERC1155 is TokenERC1155 { + function setMintedURI(MintRequest calldata _req, bytes calldata _signature) external { + verifyRequest(_req, _signature); + } +} + +contract ERC1155ReceiverCompliant is IERC1155Receiver { + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes calldata data + ) external view virtual override returns (bytes4) { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external returns (bytes4) { + return this.onERC1155BatchReceived.selector; + } + + function supportsInterface(bytes4 interfaceId) external view returns (bool) {} +} + +contract ReentrantContract { + fallback() external payable { + TokenERC1155.MintRequest memory _mintrequest; + bytes memory _signature; + MyTokenERC1155(msg.sender).mintWithSignature(_mintrequest, _signature); + } +} + +contract TokenERC1155Test_MintWithSignature is BaseTest { + address public implementation; + address public proxy; + address public caller; + address public recipient; + string public uri; + + address private defaultFeeRecipient; + + MyTokenERC1155 internal tokenContract; + ERC1155ReceiverCompliant internal erc1155ReceiverContract; + + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + TokenERC1155.MintRequest _mintrequest; + + event MetadataUpdate(uint256 _tokenId); + event TokensMinted(address indexed mintedTo, uint256 indexed tokenIdMinted, string uri); + event TokensMintedWithSignature( + address indexed signer, + address indexed mintedTo, + uint256 indexed tokenIdMinted, + TokenERC1155.MintRequest mintRequest + ); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + erc1155ReceiverContract = new ERC1155ReceiverCompliant(); + caller = getActor(1); + recipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + defaultFeeRecipient = tokenContract.DEFAULT_FEE_RECIPIENT(); + + typehashMintRequest = keccak256( + "MintRequest(address to,address royaltyRecipient,uint256 royaltyBps,address primarySaleRecipient,uint256 tokenId,string uri,uint256 quantity,uint256 pricePerToken,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + nameHash = keccak256(bytes("TokenERC1155")); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256( + abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(tokenContract)) + ); + + // construct default mintrequest + _mintrequest.to = address(0x1234); + _mintrequest.royaltyRecipient = royaltyRecipient; + _mintrequest.royaltyBps = royaltyBps; + _mintrequest.primarySaleRecipient = saleRecipient; + _mintrequest.tokenId = type(uint256).max; + _mintrequest.uri = "ipfs://"; + _mintrequest.quantity = 100; + _mintrequest.pricePerToken = 0; + _mintrequest.currency = address(0); + _mintrequest.validityStartTimestamp = 0; + _mintrequest.validityEndTimestamp = 2000; + _mintrequest.uid = bytes32(0); + + erc20.mint(deployer, 1_000 ether); + vm.deal(deployer, 1_000 ether); + erc20.mint(caller, 1_000 ether); + vm.deal(caller, 1_000 ether); + + vm.startPrank(deployer); + erc20.approve(address(tokenContract), type(uint256).max); + vm.stopPrank(); + + vm.startPrank(caller); + erc20.approve(address(tokenContract), type(uint256).max); + vm.stopPrank(); + } + + function signMintRequest( + TokenERC1155.MintRequest memory _request, + uint256 _privateKey + ) internal view returns (bytes memory) { + bytes memory encodedRequest = bytes.concat( + abi.encode( + typehashMintRequest, + _request.to, + _request.royaltyRecipient, + _request.royaltyBps, + _request.primarySaleRecipient, + _request.tokenId, + keccak256(bytes(_request.uri)) + ), + abi.encode( + _request.quantity, + _request.pricePerToken, + _request.currency, + _request.validityStartTimestamp, + _request.validityEndTimestamp, + _request.uid + ) + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, typedDataHash); + bytes memory sig = abi.encodePacked(r, s, v); + + return sig; + } + + // ================== + // ======= Assume _req.tokenId input is type(uint256).max and platform fee type is Bps + // ================== + + function test_mintWithSignature_notMinterRole() public { + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(caller); + vm.expectRevert("invalid signature"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenMinterRole() { + vm.prank(deployer); + tokenContract.grantRole(keccak256("MINTER_ROLE"), signer); + _; + } + + function test_mintWithSignature_invalidUID() public whenMinterRole { + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // set state with this mintrequest and signature, marking the UID as used + tokenContract.setMintedURI(_mintrequest, _signature); + + // pass the same UID mintrequest again + vm.prank(caller); + vm.expectRevert("invalid signature"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenUidNotUsed() { + _; + } + + function test_mintWithSignature_invalidStartTimestamp() public whenMinterRole whenUidNotUsed { + _mintrequest.validityStartTimestamp = uint128(block.timestamp + 1); + + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("request expired"); + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenValidStartTimestamp() { + _; + } + + function test_mintWithSignature_invalidEndTimestamp() public whenMinterRole whenUidNotUsed whenValidStartTimestamp { + _mintrequest.validityEndTimestamp = uint128(block.timestamp - 1); + + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("request expired"); + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenValidEndTimestamp() { + _; + } + + function test_mintWithSignature_recipientAddressZero() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + { + _mintrequest.to = address(0); + + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("recipient undefined"); + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenRecipientAddressNotZero() { + _; + } + + function test_mintWithSignature_zeroQuantity() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + { + _mintrequest.quantity = 0; + + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("zero quantity"); + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenNotZeroQuantity() { + _mintrequest.quantity = 100; + _; + } + + // ================== + // ======= Test branch: when mint price is zero + // ================== + + function test_mintWithSignature_zeroPrice_msgValueNonZero() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + { + _mintrequest.pricePerToken = 0; + + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("!Value"); + vm.prank(caller); + tokenContract.mintWithSignature{ value: 1 }(_mintrequest, _signature); + } + + modifier whenMsgValueZero() { + _; + } + + function test_mintWithSignature_zeroPrice_EOA() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.pricePerToken = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + + // check state after + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.balanceOf(_mintrequest.to, _tokenIdToMint), _mintrequest.quantity); + assertEq(tokenContract.uri(_tokenIdToMint), _mintrequest.uri); + assertEq(tokenContract.totalSupply(_tokenIdToMint), _mintrequest.quantity); + } + + function test_mintWithSignature_zeroPrice_EOA_MetadataUpdateEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.pricePerToken = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(false, false, false, true); + emit MetadataUpdate(_tokenIdToMint); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_mintWithSignature_zeroPrice_EOA_TokensMintedWithSignatureEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.pricePerToken = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(true, true, false, true); + emit TokensMintedWithSignature(signer, _mintrequest.to, _tokenIdToMint, _mintrequest); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_mintWithSignature_zeroPrice_nonERC1155ReceiverContract() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.pricePerToken = 0; + _mintrequest.to = address(this); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // mint + vm.prank(caller); + vm.expectRevert(); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenERC1155Receiver() { + _mintrequest.to = address(erc1155ReceiverContract); + _; + } + + function test_mintWithSignature_zeroPrice_contract() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + whenERC1155Receiver + { + _mintrequest.pricePerToken = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + + // check state after + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.balanceOf(_mintrequest.to, _tokenIdToMint), _mintrequest.quantity); + assertEq(tokenContract.uri(_tokenIdToMint), _mintrequest.uri); + assertEq(tokenContract.totalSupply(_tokenIdToMint), _mintrequest.quantity); + } + + function test_mintWithSignature_zeroPrice_contract_MetadataUpdateEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + whenERC1155Receiver + { + _mintrequest.pricePerToken = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(false, false, false, true); + emit MetadataUpdate(_tokenIdToMint); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_mintWithSignature_zeroPrice_contract_TokensMintedWithSignatureEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + whenERC1155Receiver + { + _mintrequest.pricePerToken = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(true, true, true, true); + emit TokensMintedWithSignature(signer, _mintrequest.to, _tokenIdToMint, _mintrequest); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + // ================== + // ======= Test branch: when mint price is not zero + // ================== + + function test_mintWithSignature_nonZeroPrice_nativeToken_incorrectMsgValue() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + { + _mintrequest.pricePerToken = 10; + _mintrequest.currency = NATIVE_TOKEN; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 incorrectTotalPrice = (_mintrequest.pricePerToken * _mintrequest.quantity) + 1; + + vm.expectRevert("must send total price."); + vm.prank(caller); + tokenContract.mintWithSignature{ value: incorrectTotalPrice }(_mintrequest, _signature); + } + + modifier whenCorrectMsgValue() { + _; + } + + function test_mintWithSignature_nonZeroPrice_nativeToken() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenCorrectMsgValue + { + _mintrequest.pricePerToken = 10; + _mintrequest.currency = NATIVE_TOKEN; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + uint256 totalPrice = (_mintrequest.pricePerToken * _mintrequest.quantity); + + // mint + vm.prank(caller); + tokenContract.mintWithSignature{ value: totalPrice }(_mintrequest, _signature); + + // check state after + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.balanceOf(_mintrequest.to, _tokenIdToMint), _mintrequest.quantity); + assertEq(tokenContract.uri(_tokenIdToMint), _mintrequest.uri); + assertEq(tokenContract.totalSupply(_tokenIdToMint), _mintrequest.quantity); + + uint256 _platformFee = (totalPrice * platformFeeBps) / 10_000; + uint256 defaultFee = (totalPrice * 100) / 10_000; + uint256 _saleProceeds = totalPrice - _platformFee - defaultFee; + assertEq(caller.balance, 1000 ether - totalPrice); + assertEq(tokenContract.platformFeeRecipient().balance, _platformFee); + assertEq(tokenContract.primarySaleRecipient().balance, _saleProceeds); + assertEq(defaultFeeRecipient.balance, defaultFee); + } + + function test_mintWithSignature_nonZeroPrice_nativeToken_MetadataUpdateEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenCorrectMsgValue + { + _mintrequest.pricePerToken = 10; + _mintrequest.currency = NATIVE_TOKEN; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(false, false, false, true); + emit MetadataUpdate(_tokenIdToMint); + tokenContract.mintWithSignature{ value: _mintrequest.pricePerToken * _mintrequest.quantity }( + _mintrequest, + _signature + ); + } + + function test_mintWithSignature_nonZeroPrice_nativeToken_TokensMintedWithSignatureEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenCorrectMsgValue + { + _mintrequest.pricePerToken = 10; + _mintrequest.currency = NATIVE_TOKEN; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(true, true, true, true); + emit TokensMintedWithSignature(signer, _mintrequest.to, _tokenIdToMint, _mintrequest); + tokenContract.mintWithSignature{ value: _mintrequest.pricePerToken * _mintrequest.quantity }( + _mintrequest, + _signature + ); + } + + function test_mintWithSignature_nonZeroPrice_ERC20_nonZeroMsgValue() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + { + _mintrequest.pricePerToken = 10; + _mintrequest.currency = address(erc20); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("msg value not zero"); + vm.prank(caller); + tokenContract.mintWithSignature{ value: 1 }(_mintrequest, _signature); + } + + function test_mintWithSignature_nonZeroPrice_ERC20() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.pricePerToken = 10; + _mintrequest.currency = address(erc20); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + uint256 totalPrice = (_mintrequest.pricePerToken * _mintrequest.quantity); + + // mint + vm.prank(caller); + tokenContract.mintWithSignature{ value: 0 }(_mintrequest, _signature); + + // check state after + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.balanceOf(_mintrequest.to, _tokenIdToMint), _mintrequest.quantity); + assertEq(tokenContract.uri(_tokenIdToMint), _mintrequest.uri); + assertEq(tokenContract.totalSupply(_tokenIdToMint), _mintrequest.quantity); + + uint256 _platformFee = (totalPrice * platformFeeBps) / 10_000; + uint256 defaultFee = (totalPrice * 100) / 10_000; + uint256 _saleProceeds = totalPrice - _platformFee - defaultFee; + assertEq(erc20.balanceOf(caller), 1000 ether - totalPrice); + assertEq(erc20.balanceOf(tokenContract.platformFeeRecipient()), _platformFee); + assertEq(erc20.balanceOf(tokenContract.primarySaleRecipient()), _saleProceeds); + assertEq(erc20.balanceOf(defaultFeeRecipient), defaultFee); + } + + function test_mintWithSignature_nonZeroPrice_ERC20_MetadataUpdateEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.pricePerToken = 10; + _mintrequest.currency = address(erc20); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(false, false, false, true); + emit MetadataUpdate(_tokenIdToMint); + tokenContract.mintWithSignature{ value: 0 }(_mintrequest, _signature); + } + + function test_mintWithSignature_nonZeroPrice_ERC20_TokensMintedWithSignatureEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.pricePerToken = 10; + _mintrequest.currency = address(erc20); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(true, true, true, true); + emit TokensMintedWithSignature(signer, _mintrequest.to, _tokenIdToMint, _mintrequest); + tokenContract.mintWithSignature{ value: 0 }(_mintrequest, _signature); + } + + // ================== + // ======= Test branch: other cases + // ================== + + function test_mintWithSignature_nonZeroRoyaltyRecipient() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.pricePerToken = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + + (address _royaltyRecipient, uint16 _royaltyBps) = tokenContract.getRoyaltyInfoForToken(0); + assertEq(_royaltyRecipient, royaltyRecipient); + assertEq(_royaltyBps, royaltyBps); + } + + function test_mintWithSignature_royaltyRecipientZeroAddress() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.pricePerToken = 0; + _mintrequest.royaltyRecipient = address(0); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + + (address _royaltyRecipient, uint16 _royaltyBps) = tokenContract.getRoyaltyInfoForToken(0); + (address _defaultRoyaltyRecipient, uint16 _defaultRoyaltyBps) = tokenContract.getDefaultRoyaltyInfo(); + assertEq(_royaltyRecipient, _defaultRoyaltyRecipient); + assertEq(_royaltyBps, _defaultRoyaltyBps); + } + + function test_mintWithSignature_reentrantRecipientContract() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.pricePerToken = 0; + _mintrequest.to = address(new ReentrantContract()); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(caller); + vm.expectRevert("ReentrancyGuard: reentrant call"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_mintWithSignature_nonZeroPrice_flatFee_exceedsTotalPrice() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + vm.startPrank(deployer); + tokenContract.setPlatformFeeType(IPlatformFee.PlatformFeeType.Flat); + tokenContract.setFlatPlatformFeeInfo(platformFeeRecipient, 100 ether); + vm.stopPrank(); + + _mintrequest.pricePerToken = 10; + _mintrequest.currency = address(erc20); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + uint256 totalPrice = (_mintrequest.pricePerToken * _mintrequest.quantity); + + // mint + vm.prank(caller); + vm.expectRevert("price less than platform fee"); + tokenContract.mintWithSignature{ value: 0 }(_mintrequest, _signature); + } + + function test_mintWithSignature_nonZeroPrice_flatFee() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + vm.prank(deployer); + tokenContract.setPlatformFeeType(IPlatformFee.PlatformFeeType.Flat); + + _mintrequest.pricePerToken = 10; + _mintrequest.currency = address(erc20); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + uint256 totalPrice = (_mintrequest.pricePerToken * _mintrequest.quantity); + + // mint + vm.prank(caller); + tokenContract.mintWithSignature{ value: 0 }(_mintrequest, _signature); + + // check state after + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.balanceOf(_mintrequest.to, _tokenIdToMint), _mintrequest.quantity); + assertEq(tokenContract.uri(_tokenIdToMint), _mintrequest.uri); + assertEq(tokenContract.totalSupply(_tokenIdToMint), _mintrequest.quantity); + + (, uint256 _platformFee) = tokenContract.getFlatPlatformFeeInfo(); + uint256 defaultFee = (totalPrice * 100) / 10_000; + uint256 _saleProceeds = totalPrice - _platformFee - defaultFee; + assertEq(erc20.balanceOf(caller), 1000 ether - totalPrice); + assertEq(erc20.balanceOf(tokenContract.platformFeeRecipient()), _platformFee); + assertEq(erc20.balanceOf(tokenContract.primarySaleRecipient()), _saleProceeds); + assertEq(erc20.balanceOf(defaultFeeRecipient), defaultFee); + } + + modifier whenNotMaxTokenId() { + // pre-mint the first token (i.e. id 0), so that nextTokenIdToMint is 1, for this code path + vm.prank(deployer); + tokenContract.mintTo(deployer, type(uint256).max, "uri1", 10); + _; + } + + function test_mintWithSignature_nonZeroPrice_notMaxTokenId_invalidId() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + whenNotMaxTokenId + { + vm.prank(deployer); + tokenContract.setPlatformFeeType(IPlatformFee.PlatformFeeType.Flat); + + _mintrequest.pricePerToken = 10; + _mintrequest.currency = address(erc20); + _mintrequest.tokenId = 1; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(caller); + vm.expectRevert("invalid id"); + tokenContract.mintWithSignature{ value: 0 }(_mintrequest, _signature); + } + + modifier whenValidId() { + _; + } + + function test_mintWithSignature_nonZeroPrice_notMaxTokenId() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + whenNotMaxTokenId + whenValidId + { + vm.prank(deployer); + tokenContract.setPlatformFeeType(IPlatformFee.PlatformFeeType.Flat); + + _mintrequest.pricePerToken = 10; + _mintrequest.currency = address(erc20); + _mintrequest.tokenId = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint() - 1; + + uint256 totalPrice = (_mintrequest.pricePerToken * _mintrequest.quantity); + + // mint + vm.prank(caller); + tokenContract.mintWithSignature{ value: 0 }(_mintrequest, _signature); + + // check state after + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.balanceOf(_mintrequest.to, _tokenIdToMint), _mintrequest.quantity); + assertEq(tokenContract.uri(_tokenIdToMint), "uri1"); + assertEq(tokenContract.totalSupply(_tokenIdToMint), _mintrequest.quantity + 10); + } +} diff --git a/src/test/tokenerc1155-BTT/mint-with-signature/mintWithSignature.tree b/src/test/tokenerc1155-BTT/mint-with-signature/mintWithSignature.tree new file mode 100644 index 000000000..115264aec --- /dev/null +++ b/src/test/tokenerc1155-BTT/mint-with-signature/mintWithSignature.tree @@ -0,0 +1,102 @@ +mintWithSignature(MintRequest calldata _req, bytes calldata _signature) +// assuming _req.tokenId input is type(uint256).max and platform fee type is Bps +├── when signer doesn't have MINTER_ROLE +│ └── it should revert ✅ +└── when signer has MINTER_ROLE + └── when `_req.uid` has already been used + │ └── it should revert ✅ + └── when `_req.uid` has not been used + └── when `_req.validityStartTimestamp` is greater than block timestamp + │ └── it should revert ✅ + └── when `_req.validityStartTimestamp` is less than or equal to block timestamp + └── when `_req.validityEndTimestamp` is less than block timestamp + │ └── it should revert ✅ + └── when `_req.validityEndTimestamp` is greater than or equal to block timestamp + └── when `_req.to` is address(0) + │ └── it should revert ✅ + └── when `_req.to` is not address(0) + ├── when `_req.quantity` is zero + │ └── it should revert ✅ + └── when `_req.quantity` is not zero + │ + │ // case: price is zero + └── when `_req.pricePerToken` is zero + │ └── when msg.value is not zero + │ │ └── it should revert ✅ + │ └── when msg.value is zero + │ ├── when `_req.to` address is an EOA + │ │ └── it should mint tokenId equal to current value of `nextTokenIdToMint` ✅ + │ │ └── it should increment `nextTokenIdToMint` by 1 ✅ + │ │ └── it should mint the `_req.quantity` number of tokens to the `_req.to` address ✅ + │ │ └── it should increment totalSupply of tokenId by `_req.quantity` ✅ + │ │ └── it should set `_req.uid` as minted ✅ + │ │ └── it should set uri for minted tokenId equal to `_req.uri` ✅ + │ │ └── it should emit MetadataUpdate event ✅ + │ │ └── it should emit TokensMintedWithSignature event ✅ + │ └── when `_to` address is a contract + │ ├── when `_to` address is non ERC1155Receiver implementer + │ │ └── it should revert ✅ + │ └── when `_to` address implements ERC1155Receiver + │ └── it should mint tokenId equal to current value of `nextTokenIdToMint` ✅ + │ └── it should increment `nextTokenIdToMint` by 1 ✅ + │ └── it should mint the `_req.quantity` number of tokens to the `_req.to` address ✅ + │ └── it should increment totalSupply of tokenId by `_req.quantity` ✅ + │ └── it should set `_req.uid` as minted ✅ + │ └── it should set uri for minted tokenId equal to `_uri` ✅ + │ └── it should emit MetadataUpdate event ✅ + │ └── it should emit TokensMintedWithSignature event ✅ + │ + │ // case: price is not zero + └── when `_req.pricePerToken` is not zero + └── when currency is native token + │ └── when msg.value is not equal to total price + │ │ └── it should revert ✅ + │ └── when msg.value is equal to total price + │ └── it should mint tokenId equal to current value of `nextTokenIdToMint` ✅ + │ └── it should increment `nextTokenIdToMint` by 1 ✅ + │ └── it should mint the `_req.quantity` number of tokens to the `_req.to` address ✅ + │ └── it should increment totalSupply of tokenId by `_req.quantity` ✅ + │ └── it should set `_req.uid` as minted ✅ + │ └── it should set uri for minted tokenId equal to `_uri` ✅ + │ └── (transfer to sale recipient) ✅ + │ └── (transfer to fee recipient) ✅ + │ └── it should emit MetadataUpdate event ✅ + │ └── it should emit TokensMintedWithSignature event ✅ + └── when currency is some ERC20 token + └── when msg.value is not zero + │ └── it should revert ✅ + └── when msg.value is zero + └── it should mint tokenId equal to current value of `nextTokenIdToMint` ✅ + └── it should increment `nextTokenIdToMint` by 1 ✅ + └── it should mint the `_req.quantity` number of tokens to the `_req.to` address ✅ + └── it should increment totalSupply of tokenId by `_req.quantity` ✅ + └── it should set `_req.uid` as minted ✅ + └── it should set uri for minted tokenId equal to `_uri` ✅ + └── (transfer to sale recipient) ✅ + └── (transfer to fee recipient) ✅ + └── it should emit MetadataUpdate event ✅ + └── it should emit TokensMintedWithSignature event ✅ + +// other cases + +├── when `_req.royaltyRecipient` is not address(0) + │ └── it should set royaltyInfoForToken ✅ + └── when `_req.royaltyRecipient` is address(0) + └── it should use default royalty info ✅ + +├── when reentrant call + └── it should revert ✅ + +├── when platformFeeType is flat + └── when total price is less than platform fee + │ └── it should revert ✅ + └── when total price is greater than or equal to platform fee + └── (transfer to sale recipient) ✅ + └── (transfer to fee recipient) ✅ + +├── when tokenId input is greater than or equal to nextTokenIdToMint + └── it should revert ✅ +├── when tokenId input is less than nextTokenIdToMint + └── it should mint ✅ + + diff --git a/src/test/tokenerc1155-BTT/other-functions/other.t.sol b/src/test/tokenerc1155-BTT/other-functions/other.t.sol new file mode 100644 index 000000000..c95c15dab --- /dev/null +++ b/src/test/tokenerc1155-BTT/other-functions/other.t.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; +import { IStaking1155 } from "contracts/extension/interface/IStaking1155.sol"; +import { IERC2981 } from "contracts/eip/interface/IERC2981.sol"; + +import "@openzeppelin/contracts-upgradeable/access/IAccessControlEnumerableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/IAccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/introspection/IERC165Upgradeable.sol"; +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 { + function canSetMetadata() public view returns (bool) { + return _canSetMetadata(); + } + + function canFreezeMetadata() public view returns (bool) { + return _canFreezeMetadata(); + } + + function beforeTokenTransfer( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) external { + _beforeTokenTransfer(operator, from, to, ids, amounts, data); + } + + function setTotalSupply(uint256 _tokenId, uint256 _totalSupply) external { + totalSupply[_tokenId] = _totalSupply; + } +} + +contract TokenERC1155Test_OtherFunctions is BaseTest { + address public implementation; + address public proxy; + + MyTokenERC1155 public tokenContract; + address internal caller; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + caller = getActor(3); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + } + + function test_contractType() public { + assertEq(tokenContract.contractType(), bytes32("TokenERC1155")); + } + + function test_contractVersion() public { + assertEq(tokenContract.contractVersion(), uint8(1)); + } + + function test_beforeTokenTransfer_restricted_notTransferRole() public { + uint256[] memory ids; + uint256[] memory amounts; + + vm.prank(deployer); + tokenContract.revokeRole(keccak256("TRANSFER_ROLE"), address(0)); + vm.expectRevert("restricted to TRANSFER_ROLE holders."); + tokenContract.beforeTokenTransfer(caller, caller, address(0x123), ids, amounts, ""); + } + + modifier whenTransferRole() { + vm.prank(deployer); + tokenContract.grantRole(keccak256("TRANSFER_ROLE"), caller); + _; + } + + function test_beforeTokenTransfer_restricted() public whenTransferRole { + uint256[] memory ids; + uint256[] memory amounts; + tokenContract.beforeTokenTransfer(caller, caller, address(0x123), ids, amounts, ""); + } + + function test_beforeTokenTransfer_restricted_fromZero() public whenTransferRole { + uint256[] memory ids = new uint256[](1); + uint256[] memory amounts = new uint256[](1); + uint256 _initialSupply = 100; + + ids[0] = 1; + amounts[0] = 10; + tokenContract.setTotalSupply(ids[0], _initialSupply); // mock set supply + + tokenContract.beforeTokenTransfer(caller, address(0), address(0x123), ids, amounts, ""); + + assertEq(tokenContract.totalSupply(ids[0]), amounts[0] + _initialSupply); + } + + function test_beforeTokenTransfer_restricted_toZero() public whenTransferRole { + uint256[] memory ids = new uint256[](1); + uint256[] memory amounts = new uint256[](1); + uint256 _initialSupply = 100; + + ids[0] = 1; + amounts[0] = 10; + tokenContract.setTotalSupply(ids[0], _initialSupply); // mock set supply + + tokenContract.beforeTokenTransfer(caller, caller, address(0), ids, amounts, ""); + + assertEq(tokenContract.totalSupply(ids[0]), _initialSupply - amounts[0]); + } + + function test_canSetMetadata_notMetadataRole() public { + assertFalse(tokenContract.canSetMetadata()); + } + + modifier whenMetadataRoleRole() { + _; + } + + function test_canSetMetadata() public whenMetadataRoleRole { + vm.prank(deployer); + assertTrue(tokenContract.canSetMetadata()); + } + + function test_canFreezeMetadata_notMetadataRole() public { + assertFalse(tokenContract.canFreezeMetadata()); + } + + function test_canFreezeMetadata() public whenMetadataRoleRole { + vm.prank(deployer); + assertTrue(tokenContract.canFreezeMetadata()); + } + + function test_supportsInterface() public { + assertTrue(tokenContract.supportsInterface(type(IERC2981).interfaceId)); + assertTrue(tokenContract.supportsInterface(type(IERC165).interfaceId)); + assertTrue(tokenContract.supportsInterface(type(IERC165Upgradeable).interfaceId)); + assertTrue(tokenContract.supportsInterface(type(IAccessControlEnumerableUpgradeable).interfaceId)); + assertTrue(tokenContract.supportsInterface(type(IAccessControlUpgradeable).interfaceId)); + assertTrue(tokenContract.supportsInterface(type(IERC1155Upgradeable).interfaceId)); + + // false for other not supported interfaces + assertFalse(tokenContract.supportsInterface(type(IStaking1155).interfaceId)); + } +} diff --git a/src/test/tokenerc1155-BTT/other-functions/other.tree b/src/test/tokenerc1155-BTT/other-functions/other.tree new file mode 100644 index 000000000..6af7d78cf --- /dev/null +++ b/src/test/tokenerc1155-BTT/other-functions/other.tree @@ -0,0 +1,37 @@ +contractType() +├── it should return bytes32("TokenERC1155") ✅ + +contractVersion() +├── it should return uint8(1) ✅ + +_beforeTokenTransfers( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data +) +├── when transfers are restricted (i.e. address(0) doesn't have transfer role, or from-to addresses are not address(0) + └── when from and to don't have transfer role + │ └── it should revert ✅ + └── when from is address(0) + │ └── it should increase totalSupply of `ids` by `amounts` ✅ + └── when to is address(0) + └── it should decrease totalSupply of `ids` by `amounts` ✅ + +_canSetMetadata() +├── when the caller doesn't have METADATA_ROLE +│ └── it should revert ✅ +└── when the caller has METADATA_ROLE + └── it should return true ✅ + +_canFreezeMetadata() +├── when the caller doesn't have METADATA_ROLE +│ └── it should revert ✅ +└── when the caller has METADATA_ROLE + └── it should return true ✅ + +supportsInterface(bytes4 interfaceId) +├── it should return true for supported interface ✅ +├── it should return false for not supported interface ✅ diff --git a/src/test/tokenerc1155-BTT/owner/owner.t.sol b/src/test/tokenerc1155-BTT/owner/owner.t.sol new file mode 100644 index 000000000..0615f32c4 --- /dev/null +++ b/src/test/tokenerc1155-BTT/owner/owner.t.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract TokenERC1155Test_Owner is BaseTest { + address public implementation; + address public proxy; + + MyTokenERC1155 internal tokenContract; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + } + + function test_owner() public { + assertEq(tokenContract.owner(), deployer); + } + + function test_owner_notDefaultAdmin() public { + vm.prank(deployer); + tokenContract.renounceRole(bytes32(0x00), deployer); + + assertEq(tokenContract.owner(), address(0)); + } +} diff --git a/src/test/tokenerc1155-BTT/owner/owner.tree b/src/test/tokenerc1155-BTT/owner/owner.tree new file mode 100644 index 000000000..576cfcb91 --- /dev/null +++ b/src/test/tokenerc1155-BTT/owner/owner.tree @@ -0,0 +1,6 @@ +owner() +├── when private variable `_owner` DEFAULT_ADMIN_ROLE +│ └── it should return `_owner` ✅ +└── when private variable `_owner` doesn't have DEFAULT_ADMIN_ROLE + └── it should return address(0) ✅ + diff --git a/src/test/tokenerc1155-BTT/set-contract-uri/setContractURI.t.sol b/src/test/tokenerc1155-BTT/set-contract-uri/setContractURI.t.sol new file mode 100644 index 000000000..4f3739103 --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-contract-uri/setContractURI.t.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract TokenERC1155Test_SetContractURI is BaseTest { + address public implementation; + address public proxy; + address internal caller; + string internal _contractURI; + + MyTokenERC1155 internal tokenContract; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + + caller = getActor(1); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + _contractURI = "ipfs://contracturi"; + } + + function test_setContractURI_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(caller), 20), + " is missing role ", + Strings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setContractURI(_contractURI); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setContractURI_empty() public whenCallerAuthorized { + vm.prank(address(caller)); + tokenContract.setContractURI(""); + + // get contract uri + assertEq(tokenContract.contractURI(), ""); + } + + function test_setContractURI_notEmpty() public whenCallerAuthorized { + vm.prank(address(caller)); + tokenContract.setContractURI(_contractURI); + + // get contract uri + assertEq(tokenContract.contractURI(), _contractURI); + } +} diff --git a/src/test/tokenerc1155-BTT/set-contract-uri/setContractURI.tree b/src/test/tokenerc1155-BTT/set-contract-uri/setContractURI.tree new file mode 100644 index 000000000..8fc480b19 --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-contract-uri/setContractURI.tree @@ -0,0 +1,8 @@ +setContractURI(string calldata _uri) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── when `uri` is empty + │ └── it should update contract URI to empty string ✅ + └── when `uri` is not empty + └── it should update contract URI to `_uri` ✅ \ No newline at end of file diff --git a/src/test/tokenerc1155-BTT/set-default-royalty-info/setDefaultRoyaltyInfo.t.sol b/src/test/tokenerc1155-BTT/set-default-royalty-info/setDefaultRoyaltyInfo.t.sol new file mode 100644 index 000000000..552d9d75b --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-default-royalty-info/setDefaultRoyaltyInfo.t.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract TokenERC1155Test_SetDefaultRoyaltyInfo is BaseTest { + address public implementation; + address public proxy; + address internal caller; + address internal defaultRoyaltyRecipient; + uint256 internal defaultRoyaltyBps; + + MyTokenERC1155 internal tokenContract; + + event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + + caller = getActor(1); + defaultRoyaltyRecipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + } + + function test_setDefaultRoyaltyInfo_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(caller), 20), + " is missing role ", + Strings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setDefaultRoyaltyInfo_exceedMaxBps() public whenCallerAuthorized { + defaultRoyaltyBps = 10_001; + vm.prank(address(caller)); + vm.expectRevert("exceed royalty bps"); + tokenContract.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } + + modifier whenNotExceedMaxBps() { + defaultRoyaltyBps = 500; + _; + } + + function test_setDefaultRoyaltyInfo() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + tokenContract.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + + // get default royalty info + (address _recipient, uint16 _royaltyBps) = tokenContract.getDefaultRoyaltyInfo(); + assertEq(_recipient, defaultRoyaltyRecipient); + assertEq(_royaltyBps, uint16(defaultRoyaltyBps)); + + // get royalty info for token + uint256 tokenId = 0; + (_recipient, _royaltyBps) = tokenContract.getRoyaltyInfoForToken(tokenId); + assertEq(_recipient, defaultRoyaltyRecipient); + assertEq(_royaltyBps, uint16(defaultRoyaltyBps)); + + // royaltyInfo - ERC2981 + uint256 salePrice = 1000; + (address _royaltyRecipient, uint256 _royaltyAmount) = tokenContract.royaltyInfo(tokenId, salePrice); + assertEq(_royaltyRecipient, defaultRoyaltyRecipient); + assertEq(_royaltyAmount, (salePrice * defaultRoyaltyBps) / 10_000); + } + + function test_setDefaultRoyaltyInfo_event() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + vm.expectEmit(true, false, false, true); + emit DefaultRoyalty(defaultRoyaltyRecipient, defaultRoyaltyBps); + tokenContract.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } +} diff --git a/src/test/tokenerc1155-BTT/set-default-royalty-info/setDefaultRoyaltyInfo.tree b/src/test/tokenerc1155-BTT/set-default-royalty-info/setDefaultRoyaltyInfo.tree new file mode 100644 index 000000000..78a4312de --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-default-royalty-info/setDefaultRoyaltyInfo.tree @@ -0,0 +1,11 @@ +setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + ├── when royalty bps input is greater than MAX_BPS + │ └── it should revert ✅ + └── when royalty bps input is less than or equal to MAX_BPS + └── it should update default royalty recipient ✅ + └── it should update default royalty bps ✅ + └── it should calculate royalty amount for a token-id based on default royalty info ✅ + └── it should emit DefaultRoyalty event ✅ \ No newline at end of file diff --git a/src/test/tokenerc1155-BTT/set-flat-platform-fee-info/setFlatPlatformFeeInfo.t.sol b/src/test/tokenerc1155-BTT/set-flat-platform-fee-info/setFlatPlatformFeeInfo.t.sol new file mode 100644 index 000000000..380d65921 --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-flat-platform-fee-info/setFlatPlatformFeeInfo.t.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract TokenERC1155Test_SetFlatPlatformFeeInfo is BaseTest { + address public implementation; + address public proxy; + address internal caller; + address internal _platformFeeRecipient; + uint256 internal _flatFee; + + MyTokenERC1155 internal tokenContract; + + event FlatPlatformFeeUpdated(address platformFeeRecipient, uint256 flatFee); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + + caller = getActor(1); + _platformFeeRecipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + _flatFee = 25; + } + + function test_setFlatPlatformFeeInfo_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(caller), 20), + " is missing role ", + Strings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setFlatPlatformFeeInfo(_platformFeeRecipient, _flatFee); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setFlatPlatformFeeInfo() public whenCallerAuthorized { + vm.prank(address(caller)); + tokenContract.setFlatPlatformFeeInfo(_platformFeeRecipient, _flatFee); + + // get platform fee info + (address _recipient, uint256 _fee) = tokenContract.getFlatPlatformFeeInfo(); + assertEq(_recipient, _platformFeeRecipient); + assertEq(_fee, _flatFee); + assertEq(tokenContract.platformFeeRecipient(), _platformFeeRecipient); + } + + function test_setFlatPlatformFeeInfo_event() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectEmit(false, false, false, true); + emit FlatPlatformFeeUpdated(_platformFeeRecipient, _flatFee); + tokenContract.setFlatPlatformFeeInfo(_platformFeeRecipient, _flatFee); + } +} diff --git a/src/test/tokenerc1155-BTT/set-flat-platform-fee-info/setFlatPlatformFeeInfo.tree b/src/test/tokenerc1155-BTT/set-flat-platform-fee-info/setFlatPlatformFeeInfo.tree new file mode 100644 index 000000000..95bfe1f2d --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-flat-platform-fee-info/setFlatPlatformFeeInfo.tree @@ -0,0 +1,8 @@ +setFlatPlatformFeeInfo(address _platformFeeRecipient, uint256 _flatFee) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── when `_platformFeeBps` is less than or equal to MAX_BPS + └── it should update platform fee recipient ✅ + └── it should update flatPlatformFee ✅ + └── it should emit FlatPlatformFeeUpdated event ✅ \ No newline at end of file diff --git a/src/test/tokenerc1155-BTT/set-owner/setOwner.t.sol b/src/test/tokenerc1155-BTT/set-owner/setOwner.t.sol new file mode 100644 index 000000000..00dd0f2ef --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-owner/setOwner.t.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract TokenERC1155Test_SetOwner is BaseTest { + address public implementation; + address public proxy; + address internal caller; + address internal _newOwner; + + MyTokenERC1155 internal tokenContract; + + event OwnerUpdated(address indexed prevOwner, address indexed newOwner); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + + caller = getActor(1); + _newOwner = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + } + + function test_setOwner_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(caller), 20), + " is missing role ", + Strings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setOwner(_newOwner); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setOwner_newOwnerNotAdmin() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectRevert("new owner not module admin."); + tokenContract.setOwner(_newOwner); + } + + modifier whenNewOwnerIsAnAdmin() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), _newOwner); + _; + } + + function test_setOwner() public whenCallerAuthorized whenNewOwnerIsAnAdmin { + vm.prank(address(caller)); + tokenContract.setOwner(_newOwner); + + assertEq(tokenContract.owner(), _newOwner); + } + + function test_setOwner_event() public whenCallerAuthorized whenNewOwnerIsAnAdmin { + vm.prank(address(caller)); + vm.expectEmit(true, true, false, false); + emit OwnerUpdated(deployer, _newOwner); + tokenContract.setOwner(_newOwner); + } +} diff --git a/src/test/tokenerc1155-BTT/set-owner/setOwner.tree b/src/test/tokenerc1155-BTT/set-owner/setOwner.tree new file mode 100644 index 000000000..964e97cac --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-owner/setOwner.tree @@ -0,0 +1,9 @@ +setOwner(address _newOwner) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── when incoming `_owner` doesn't have DEFAULT_ADMIN_ROLE + │ └── it should revert ✅ + └── when incoming `_owner` has DEFAULT_ADMIN_ROLE + └── it should update owner ✅ + └── it should emit OwnerUpdated event ✅ \ No newline at end of file diff --git a/src/test/tokenerc1155-BTT/set-platform-fee-info/setPlatformFeeInfo.t.sol b/src/test/tokenerc1155-BTT/set-platform-fee-info/setPlatformFeeInfo.t.sol new file mode 100644 index 000000000..e52402a03 --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-platform-fee-info/setPlatformFeeInfo.t.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract TokenERC1155Test_SetPlatformFeeInfo is BaseTest { + address public implementation; + address public proxy; + address internal caller; + address internal _platformFeeRecipient; + uint256 internal _platformFeeBps; + + MyTokenERC1155 internal tokenContract; + + event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + + caller = getActor(1); + _platformFeeRecipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + } + + function test_setPlatformFeeInfo_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(caller), 20), + " is missing role ", + Strings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setPlatformFeeInfo_exceedMaxBps() public whenCallerAuthorized { + _platformFeeBps = 10_001; + vm.prank(address(caller)); + vm.expectRevert("exceeds MAX_BPS"); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } + + modifier whenNotExceedMaxBps() { + _platformFeeBps = 500; + _; + } + + function test_setPlatformFeeInfo() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + + // get platform fee info + (address _recipient, uint16 _bps) = tokenContract.getPlatformFeeInfo(); + assertEq(_recipient, _platformFeeRecipient); + assertEq(_bps, uint16(_platformFeeBps)); + assertEq(tokenContract.platformFeeRecipient(), _platformFeeRecipient); + } + + function test_setPlatformFeeInfo_event() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + vm.expectEmit(true, false, false, true); + emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } +} diff --git a/src/test/tokenerc1155-BTT/set-platform-fee-info/setPlatformFeeInfo.tree b/src/test/tokenerc1155-BTT/set-platform-fee-info/setPlatformFeeInfo.tree new file mode 100644 index 000000000..dcef9965e --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-platform-fee-info/setPlatformFeeInfo.tree @@ -0,0 +1,10 @@ +setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + ├── when `_platformFeeBps` is greater than MAX_BPS + │ └── it should revert ✅ + └── when `_platformFeeBps` is less than or equal to MAX_BPS + └── it should update platform fee recipient ✅ + └── it should update platform fee bps ✅ + └── it should emit PlatformFeeInfoUpdated event ✅ \ No newline at end of file diff --git a/src/test/tokenerc1155-BTT/set-platform-fee-type/setPlatformFeeType.t.sol b/src/test/tokenerc1155-BTT/set-platform-fee-type/setPlatformFeeType.t.sol new file mode 100644 index 000000000..db96dd310 --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-platform-fee-type/setPlatformFeeType.t.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { IPlatformFee } from "contracts/extension/interface/IPlatformFee.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract TokenERC1155Test_SetPlatformFeeType is BaseTest { + address public implementation; + address public proxy; + address internal caller; + IPlatformFee.PlatformFeeType internal _newFeeType; + + MyTokenERC1155 internal tokenContract; + + event PlatformFeeTypeUpdated(IPlatformFee.PlatformFeeType feeType); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + + caller = getActor(1); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + _newFeeType = IPlatformFee.PlatformFeeType.Flat; + } + + function test_setPlatformFeeType_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(caller), 20), + " is missing role ", + Strings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setPlatformFeeType(_newFeeType); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setPlatformFeeType() public whenCallerAuthorized { + vm.prank(address(caller)); + tokenContract.setPlatformFeeType(_newFeeType); + + assertEq(uint8(tokenContract.getPlatformFeeType()), uint8(_newFeeType)); + } + + function test_setPlatformFeeType_event() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectEmit(false, false, false, true); + emit PlatformFeeTypeUpdated(_newFeeType); + tokenContract.setPlatformFeeType(_newFeeType); + } +} diff --git a/src/test/tokenerc1155-BTT/set-platform-fee-type/setPlatformFeeType.tree b/src/test/tokenerc1155-BTT/set-platform-fee-type/setPlatformFeeType.tree new file mode 100644 index 000000000..e25a6bd4c --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-platform-fee-type/setPlatformFeeType.tree @@ -0,0 +1,6 @@ +setPlatformFeeType(PlatformFeeType _feeType) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── it should update platformFeeType ✅ + └── it should emit PlatformFeeTypeUpdated event ✅ \ No newline at end of file diff --git a/src/test/tokenerc1155-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.t.sol b/src/test/tokenerc1155-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.t.sol new file mode 100644 index 000000000..2f838241e --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.t.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract TokenERC1155Test_SetPrimarySaleRecipient is BaseTest { + address public implementation; + address public proxy; + address internal caller; + address internal _primarySaleRecipient; + + MyTokenERC1155 internal tokenContract; + + event PrimarySaleRecipientUpdated(address indexed recipient); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + + caller = getActor(1); + _primarySaleRecipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + } + + function test_setPrimarySaleRecipient_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(caller), 20), + " is missing role ", + Strings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setPrimarySaleRecipient() public whenCallerAuthorized { + vm.prank(address(caller)); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + + // get primary sale recipient info + assertEq(tokenContract.primarySaleRecipient(), _primarySaleRecipient); + } + + function test_setPrimarySaleRecipient_event() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectEmit(true, false, false, false); + emit PrimarySaleRecipientUpdated(_primarySaleRecipient); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + } +} diff --git a/src/test/tokenerc1155-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.tree b/src/test/tokenerc1155-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.tree new file mode 100644 index 000000000..230035a07 --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.tree @@ -0,0 +1,6 @@ +setPrimarySaleRecipient(address _saleRecipient) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── it should update primary sale recipient ✅ + └── it should emit PrimarySaleRecipientUpdated event ✅ \ No newline at end of file diff --git a/src/test/tokenerc1155-BTT/set-royalty-info-for-token/setRoyaltyInfoForToken.t.sol b/src/test/tokenerc1155-BTT/set-royalty-info-for-token/setRoyaltyInfoForToken.t.sol new file mode 100644 index 000000000..051dd7918 --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-royalty-info-for-token/setRoyaltyInfoForToken.t.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract TokenERC1155Test_SetRoyaltyInfoForToken is BaseTest { + address public implementation; + address public proxy; + address internal caller; + address internal defaultRoyaltyRecipient; + uint256 internal defaultRoyaltyBps; + + MyTokenERC1155 internal tokenContract; + + address internal royaltyRecipientForToken; + uint256 internal royaltyBpsForToken; + uint256 internal tokenId; + + event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, uint256 royaltyBps); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + + caller = getActor(1); + defaultRoyaltyRecipient = getActor(2); + royaltyRecipientForToken = getActor(3); + defaultRoyaltyBps = 500; + tokenId = 1; + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + + vm.prank(deployer); + tokenContract.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } + + function test_setRoyaltyInfoForToken_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(caller), 20), + " is missing role ", + Strings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setRoyaltyInfoForToken_exceedMaxBps() public whenCallerAuthorized { + royaltyBpsForToken = 10_001; + vm.prank(address(caller)); + vm.expectRevert("exceed royalty bps"); + tokenContract.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + } + + modifier whenNotExceedMaxBps() { + royaltyBpsForToken = 1000; + _; + } + + function test_setRoyaltyInfoForToken() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + tokenContract.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + + // get default royalty info + (address _defaultRecipient, uint16 _defaultRoyaltyBps) = tokenContract.getDefaultRoyaltyInfo(); + assertEq(_defaultRecipient, defaultRoyaltyRecipient); + assertEq(_defaultRoyaltyBps, uint16(defaultRoyaltyBps)); + + // get royalty info for token + (address _royaltyRecipientForToken, uint16 _royaltyBpsForToken) = tokenContract.getRoyaltyInfoForToken(tokenId); + assertEq(_royaltyRecipientForToken, royaltyRecipientForToken); + assertEq(_royaltyBpsForToken, uint16(royaltyBpsForToken)); + + // royaltyInfo - ERC2981: calculate for default + uint256 salePrice = 1000; + (address _royaltyRecipient, uint256 _royaltyAmount) = tokenContract.royaltyInfo(0, salePrice); + assertEq(_royaltyRecipient, defaultRoyaltyRecipient); + assertEq(_royaltyAmount, (salePrice * defaultRoyaltyBps) / 10_000); + + // royaltyInfo - ERC2981: calculate for specific tokenId we set the royalty info for + (_royaltyRecipient, _royaltyAmount) = tokenContract.royaltyInfo(tokenId, salePrice); + assertEq(_royaltyRecipient, royaltyRecipientForToken); + assertEq(_royaltyAmount, (salePrice * royaltyBpsForToken) / 10_000); + } + + function test_setRoyaltyInfoForToken_event() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + vm.expectEmit(true, true, false, true); + emit RoyaltyForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + tokenContract.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + } +} diff --git a/src/test/tokenerc1155-BTT/set-royalty-info-for-token/setRoyaltyInfoForToken.tree b/src/test/tokenerc1155-BTT/set-royalty-info-for-token/setRoyaltyInfoForToken.tree new file mode 100644 index 000000000..cada076de --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-royalty-info-for-token/setRoyaltyInfoForToken.tree @@ -0,0 +1,15 @@ +function setRoyaltyInfoForToken( + uint256 _tokenId, + address _recipient, + uint256 _bps +) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + ├── when royalty bps input is greater than MAX_BPS + │ └── it should revert ✅ + └── when royalty bps input is less than or equal to MAX_BPS + └── it should update default royalty recipient ✅ + └── it should update default royalty bps ✅ + └── it should calculate royalty amount for a token-id based on default royalty info ✅ + └── it should emit RoyaltyForToken event ✅ \ No newline at end of file diff --git a/src/test/tokenerc1155-BTT/uri/tokenURI.t.sol b/src/test/tokenerc1155-BTT/uri/tokenURI.t.sol new file mode 100644 index 000000000..1a2feb0a2 --- /dev/null +++ b/src/test/tokenerc1155-BTT/uri/tokenURI.t.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract TokenERC1155Test_Uri is BaseTest { + address public implementation; + address public proxy; + + MyTokenERC1155 internal tokenContract; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + } + + function test_uri() public { + uint256 _tokenId = 1; + string memory _uri = "ipfs://uri/1"; + + vm.prank(deployer); + tokenContract.setTokenURI(_tokenId, _uri); + + assertEq(tokenContract.uri(_tokenId), _uri); + } +} diff --git a/src/test/tokenerc1155-BTT/uri/tokenURI.tree b/src/test/tokenerc1155-BTT/uri/tokenURI.tree new file mode 100644 index 000000000..2df0b55ed --- /dev/null +++ b/src/test/tokenerc1155-BTT/uri/tokenURI.tree @@ -0,0 +1,3 @@ +uri(uint256 _tokenId) +├── it should return uri associated with the given `_tokenId` ✅ + diff --git a/src/test/tokenerc1155-BTT/verify/verify.t.sol b/src/test/tokenerc1155-BTT/verify/verify.t.sol new file mode 100644 index 000000000..f584f57a8 --- /dev/null +++ b/src/test/tokenerc1155-BTT/verify/verify.t.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 { + function setMintedURI(MintRequest calldata _req, bytes calldata _signature) external { + verifyRequest(_req, _signature); + } +} + +contract TokenERC1155Test_Verify is BaseTest { + address public implementation; + address public proxy; + + MyTokenERC1155 internal tokenContract; + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + TokenERC1155.MintRequest _mintrequest; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + + typehashMintRequest = keccak256( + "MintRequest(address to,address royaltyRecipient,uint256 royaltyBps,address primarySaleRecipient,uint256 tokenId,string uri,uint256 quantity,uint256 pricePerToken,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + nameHash = keccak256(bytes("TokenERC1155")); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256( + abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(tokenContract)) + ); + + // construct default mintrequest + _mintrequest.to = address(0x1234); + _mintrequest.royaltyRecipient = royaltyRecipient; + _mintrequest.royaltyBps = royaltyBps; + _mintrequest.primarySaleRecipient = saleRecipient; + _mintrequest.tokenId = type(uint256).max; + _mintrequest.uri = "ipfs://"; + _mintrequest.quantity = 100; + _mintrequest.pricePerToken = 0; + _mintrequest.currency = address(0); + _mintrequest.validityStartTimestamp = 0; + _mintrequest.validityEndTimestamp = 2000; + _mintrequest.uid = bytes32(0); + } + + function signMintRequest( + TokenERC1155.MintRequest memory _request, + uint256 _privateKey + ) internal view returns (bytes memory) { + bytes memory encodedRequest = bytes.concat( + abi.encode( + typehashMintRequest, + _request.to, + _request.royaltyRecipient, + _request.royaltyBps, + _request.primarySaleRecipient, + _request.tokenId, + keccak256(bytes(_request.uri)) + ), + abi.encode( + _request.quantity, + _request.pricePerToken, + _request.currency, + _request.validityStartTimestamp, + _request.validityEndTimestamp, + _request.uid + ) + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, typedDataHash); + bytes memory sig = abi.encodePacked(r, s, v); + + return sig; + } + + function test_verify_notMinterRole() public { + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + (bool _isValid, address _recoveredSigner) = tokenContract.verify(_mintrequest, _signature); + + assertFalse(_isValid); + assertEq(_recoveredSigner, signer); + } + + modifier whenMinterRole() { + vm.prank(deployer); + tokenContract.grantRole(keccak256("MINTER_ROLE"), signer); + _; + } + + function test_verify_invalidUID() public whenMinterRole { + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // set state with this mintrequest and signature, marking the UID as used + tokenContract.setMintedURI(_mintrequest, _signature); + + // pass the same UID mintrequest again + (bool _isValid, address _recoveredSigner) = tokenContract.verify(_mintrequest, _signature); + + assertFalse(_isValid); + assertEq(_recoveredSigner, signer); + } + + modifier whenUidNotUsed() { + _; + } + + function test_verify() public whenMinterRole whenUidNotUsed { + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + (bool _isValid, address _recoveredSigner) = tokenContract.verify(_mintrequest, _signature); + + assertTrue(_isValid); + assertEq(_recoveredSigner, signer); + } +} diff --git a/src/test/tokenerc1155-BTT/verify/verify.tree b/src/test/tokenerc1155-BTT/verify/verify.tree new file mode 100644 index 000000000..c160faa0f --- /dev/null +++ b/src/test/tokenerc1155-BTT/verify/verify.tree @@ -0,0 +1,12 @@ +verify(MintRequest calldata _req, bytes calldata _signature) +├── when signer doesn't have MINTER_ROLE +│ └── it should return false ✅ +│ └── it should return recovered signer equal to the actual signer of the request ✅ +└── when signer has MINTER_ROLE + └── when `_req.uid` has already been used + │ └── it should return false ✅ + │ └── it should return recovered signer equal to the actual signer of the request ✅ + └── when `_req.uid` has not been used + └── it should return true ✅ + └── it should return recovered signer equal to the actual signer of the request ✅ + diff --git a/src/test/tokenerc20-BTT/initialize/initialize.t.sol b/src/test/tokenerc20-BTT/initialize/initialize.t.sol new file mode 100644 index 000000000..6dae4df15 --- /dev/null +++ b/src/test/tokenerc20-BTT/initialize/initialize.t.sol @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC20 is TokenERC20 { + function eip712NameHash() external view returns (bytes32) { + return _EIP712NameHash(); + } + + function eip712VersionHash() external view returns (bytes32) { + return _EIP712VersionHash(); + } +} + +contract TokenERC20Test_Initialize is BaseTest { + address public implementation; + address public proxy; + + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC20()); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ) + ); + } + + function test_initialize_initializingImplementation() public { + vm.expectRevert("Initializable: contract is already initialized"); + TokenERC20(implementation).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ); + } + + modifier whenNotImplementation() { + _; + } + + function test_initialize_proxyAlreadyInitialized() public whenNotImplementation { + vm.expectRevert("Initializable: contract is already initialized"); + MyTokenERC20(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ); + } + + modifier whenProxyNotInitialized() { + proxy = address(new TWProxy(implementation, "")); + _; + } + + function test_initialize_exceedsMaxBps() public whenNotImplementation whenProxyNotInitialized { + vm.expectRevert("exceeds MAX_BPS"); + MyTokenERC20(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + uint128(MAX_BPS) + 1 // platformFeeBps greater than MAX_BPS + ); + } + + modifier whenPlatformFeeBpsWithinMaxBps() { + _; + } + + function test_initialize() public whenNotImplementation whenProxyNotInitialized whenPlatformFeeBpsWithinMaxBps { + MyTokenERC20(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ); + + // check state + MyTokenERC20 tokenContract = MyTokenERC20(proxy); + + assertEq(tokenContract.eip712NameHash(), keccak256(bytes(NAME))); + assertEq(tokenContract.eip712VersionHash(), keccak256(bytes("1"))); + + address[] memory _trustedForwarders = forwarders(); + for (uint256 i = 0; i < _trustedForwarders.length; i++) { + assertTrue(tokenContract.isTrustedForwarder(_trustedForwarders[i])); + } + + assertEq(tokenContract.name(), NAME); + assertEq(tokenContract.symbol(), SYMBOL); + assertEq(tokenContract.contractURI(), CONTRACT_URI); + + (address _platformFeeRecipient, uint16 _platformFeeBps) = tokenContract.getPlatformFeeInfo(); + assertEq(_platformFeeBps, platformFeeBps); + assertEq(_platformFeeRecipient, platformFeeRecipient); + + assertEq(tokenContract.primarySaleRecipient(), saleRecipient); + + assertTrue(tokenContract.hasRole(bytes32(0x00), deployer)); + assertTrue(tokenContract.hasRole(keccak256("TRANSFER_ROLE"), deployer)); + assertTrue(tokenContract.hasRole(keccak256("TRANSFER_ROLE"), address(0))); + assertTrue(tokenContract.hasRole(keccak256("MINTER_ROLE"), deployer)); + } + + function test_initialize_event_RoleGranted_DefaultAdmin() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _defaultAdminRole = bytes32(0x00); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_defaultAdminRole, deployer, deployer); + MyTokenERC20(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ); + } + + function test_initialize_event_RoleGranted_MinterRole() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _minterRole = keccak256("MINTER_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_minterRole, deployer, deployer); + MyTokenERC20(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ); + } + + function test_initialize_event_RoleGranted_TransferRole() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _transferRole = keccak256("TRANSFER_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_transferRole, deployer, deployer); + MyTokenERC20(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ); + } + + function test_initialize_event_RoleGranted_TransferRole_AddressZero() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _transferRole = keccak256("TRANSFER_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_transferRole, address(0), deployer); + MyTokenERC20(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ); + } +} diff --git a/src/test/tokenerc20-BTT/initialize/initialize.tree b/src/test/tokenerc20-BTT/initialize/initialize.tree new file mode 100644 index 000000000..a3ead7790 --- /dev/null +++ b/src/test/tokenerc20-BTT/initialize/initialize.tree @@ -0,0 +1,34 @@ +initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _primarySaleRecipient, + address _platformFeeRecipient + uint256 _platformFeeBps, +) +├── when initializing the implementation contract (not proxy) +│ └── it should revert ✅ +└── when it is a proxy to the implementation + └── when it is already initialized + │ └── it should revert ✅ + └── when it is not initialized + └── when platformFeeBps is greater than MAX_BPS + │ └── it should revert ✅ + └── when platformFeeBps is less than or equal to MAX_BPS + └── it should correctly set EIP712 name hash and version hash ✅ + └── it should set trustedForwarder mapping to true for all addresses in `_trustedForwarders` ✅ + └── it should set _name and _symbol to `_name` and `_symbol` param values respectively ✅ + └── it should set contractURI to `_contractURI` param value ✅ + └── it should set platformFeeRecipient and platformFeeBps as `_platformFeeRecipient` and `_platformFeeBps` respectively ✅ + └── it should set primary sale recipient as `_saleRecipient` param value ✅ + └── it should grant 0x00 (DEFAULT_ADMIN_ROLE) to `_defaultAdmin` address ✅ + └── it should emit RoleGranted event ✅ + └── it should grant MINTER_ROLE to `_defaultAdmin` address ✅ + └── it should emit RoleGranted event ✅ + └── it should grant TRANSFER_ROLE to `_defaultAdmin` address ✅ + └── it should emit RoleGranted event ✅ + └── it should grant TRANSFER_ROLE to address(0) ✅ + └── it should emit RoleGranted event ✅ + diff --git a/src/test/tokenerc20-BTT/mint-to/mintTo.t.sol b/src/test/tokenerc20-BTT/mint-to/mintTo.t.sol new file mode 100644 index 000000000..ad5095640 --- /dev/null +++ b/src/test/tokenerc20-BTT/mint-to/mintTo.t.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC20 is TokenERC20 {} + +contract TokenERC20Test_MintTo is BaseTest { + address public implementation; + address public proxy; + address public caller; + address public recipient; + uint256 public amount; + + MyTokenERC20 internal tokenContract; + + event TokensMinted(address indexed mintedTo, uint256 quantityMinted); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC20()); + caller = getActor(1); + recipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ) + ); + + tokenContract = MyTokenERC20(proxy); + amount = 100; + } + + function test_mintTo_notMinterRole() public { + vm.prank(caller); + vm.expectRevert("not minter."); + tokenContract.mintTo(recipient, amount); + } + + modifier whenMinterRole() { + vm.prank(deployer); + tokenContract.grantRole(keccak256("MINTER_ROLE"), caller); + _; + } + + function test_mintTo() public whenMinterRole { + // mint + vm.prank(caller); + tokenContract.mintTo(recipient, amount); + + // check state after + assertEq(tokenContract.balanceOf(recipient), amount); + } + + function test_mintTo_TokensMintedEvent() public whenMinterRole { + vm.prank(caller); + vm.expectEmit(true, false, false, true); + emit TokensMinted(recipient, amount); + tokenContract.mintTo(recipient, amount); + } +} diff --git a/src/test/tokenerc20-BTT/mint-to/mintTo.tree b/src/test/tokenerc20-BTT/mint-to/mintTo.tree new file mode 100644 index 000000000..33bb14c7e --- /dev/null +++ b/src/test/tokenerc20-BTT/mint-to/mintTo.tree @@ -0,0 +1,7 @@ +mintTo(address to, uint256 amount) +├── when caller doesn't have MINTER_ROLE + │ └── it should revert ✅ + └── when caller has MINTER_ROLE + └── it should mint `amount` to `to` ✅ + └── it should emit TokensMinted event ✅ + diff --git a/src/test/tokenerc20-BTT/mint-with-signature/mintWithSignature.t.sol b/src/test/tokenerc20-BTT/mint-with-signature/mintWithSignature.t.sol new file mode 100644 index 000000000..bbe988eb8 --- /dev/null +++ b/src/test/tokenerc20-BTT/mint-with-signature/mintWithSignature.t.sol @@ -0,0 +1,449 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC20 is TokenERC20 { + function setMintedUID(MintRequest calldata _req, bytes calldata _signature) external { + verifyRequest(_req, _signature); + } +} + +contract ReentrantContract { + fallback() external payable { + TokenERC20.MintRequest memory _mintrequest; + bytes memory _signature; + MyTokenERC20(msg.sender).mintWithSignature(_mintrequest, _signature); + } +} + +contract TokenERC20Test_MintWithSignature is BaseTest { + address public implementation; + address public proxy; + address public caller; + address public recipient; + + MyTokenERC20 internal tokenContract; + + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + TokenERC20.MintRequest _mintrequest; + + event TokensMintedWithSignature( + address indexed signer, + address indexed mintedTo, + TokenERC20.MintRequest mintRequest + ); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC20()); + caller = getActor(1); + recipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ) + ); + + tokenContract = MyTokenERC20(proxy); + + typehashMintRequest = keccak256( + "MintRequest(address to,address primarySaleRecipient,uint256 quantity,uint256 price,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + nameHash = keccak256(bytes(NAME)); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256( + abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(tokenContract)) + ); + + // construct default mintrequest + _mintrequest.to = recipient; + _mintrequest.primarySaleRecipient = saleRecipient; + _mintrequest.quantity = 100; + _mintrequest.price = 0; + _mintrequest.currency = address(0); + _mintrequest.validityStartTimestamp = 0; + _mintrequest.validityEndTimestamp = 2000; + _mintrequest.uid = bytes32(0); + + erc20.mint(deployer, 1_000 ether); + vm.deal(deployer, 1_000 ether); + erc20.mint(caller, 1_000 ether); + vm.deal(caller, 1_000 ether); + + vm.startPrank(deployer); + erc20.approve(address(tokenContract), type(uint256).max); + vm.stopPrank(); + + vm.startPrank(caller); + erc20.approve(address(tokenContract), type(uint256).max); + vm.stopPrank(); + } + + function signMintRequest( + TokenERC20.MintRequest memory _request, + uint256 _privateKey + ) internal view returns (bytes memory) { + bytes memory encodedRequest = abi.encode( + typehashMintRequest, + _request.to, + _request.primarySaleRecipient, + _request.quantity, + _request.price, + _request.currency, + _request.validityStartTimestamp, + _request.validityEndTimestamp, + _request.uid + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, typedDataHash); + bytes memory sig = abi.encodePacked(r, s, v); + + return sig; + } + + function test_mintWithSignature_notMinterRole() public { + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(caller); + vm.expectRevert("invalid signature"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenMinterRole() { + vm.prank(deployer); + tokenContract.grantRole(keccak256("MINTER_ROLE"), signer); + _; + } + + function test_mintWithSignature_invalidUID() public whenMinterRole { + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // set state with this mintrequest and signature, marking the UID as used + tokenContract.setMintedUID(_mintrequest, _signature); + + // pass the same UID mintrequest again + vm.prank(caller); + vm.expectRevert("invalid signature"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenUidNotUsed() { + _; + } + + function test_mintWithSignature_invalidStartTimestamp() public whenMinterRole whenUidNotUsed { + _mintrequest.validityStartTimestamp = uint128(block.timestamp + 1); + + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("request expired"); + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenValidStartTimestamp() { + _; + } + + function test_mintWithSignature_invalidEndTimestamp() public whenMinterRole whenUidNotUsed whenValidStartTimestamp { + _mintrequest.validityEndTimestamp = uint128(block.timestamp - 1); + + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("request expired"); + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenValidEndTimestamp() { + _; + } + + function test_mintWithSignature_recipientAddressZero() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + { + _mintrequest.to = address(0); + + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("recipient undefined"); + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenRecipientAddressNotZero() { + _; + } + + function test_mintWithSignature_zeroQuantity() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + { + _mintrequest.quantity = 0; + + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("zero quantity"); + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenNotZeroQuantity() { + _mintrequest.quantity = 100; + _; + } + + // ================== + // ======= Test branch: when mint price is zero + // ================== + + function test_mintWithSignature_zeroPrice_msgValueNonZero() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + { + _mintrequest.price = 0; + + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("!Value"); + vm.prank(caller); + tokenContract.mintWithSignature{ value: 1 }(_mintrequest, _signature); + } + + modifier whenMsgValueZero() { + _; + } + + function test_mintWithSignature_zeroPrice() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.price = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // mint + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + + // check state after + assertEq(tokenContract.balanceOf(recipient), _mintrequest.quantity); + } + + function test_mintWithSignature_zeroPrice_TokensMintedWithSignatureEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.price = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(caller); + vm.expectEmit(true, true, false, true); + emit TokensMintedWithSignature(signer, _mintrequest.to, _mintrequest); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + // ================== + // ======= Test branch: when mint price is not zero + // ================== + + function test_mintWithSignature_nonZeroPrice_nativeToken_incorrectMsgValue() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + { + _mintrequest.price = 10; + _mintrequest.currency = NATIVE_TOKEN; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 incorrectTotalPrice = (_mintrequest.price) + 1; + + vm.expectRevert("must send total price."); + vm.prank(caller); + tokenContract.mintWithSignature{ value: incorrectTotalPrice }(_mintrequest, _signature); + } + + modifier whenCorrectMsgValue() { + _; + } + + function test_mintWithSignature_nonZeroPrice_nativeToken() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenCorrectMsgValue + { + _mintrequest.price = 10; + _mintrequest.currency = NATIVE_TOKEN; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // mint + vm.prank(caller); + tokenContract.mintWithSignature{ value: _mintrequest.price }(_mintrequest, _signature); + + // check state after + assertEq(tokenContract.balanceOf(recipient), _mintrequest.quantity); + + uint256 _platformFee = (_mintrequest.price * platformFeeBps) / 10_000; + uint256 _saleProceeds = _mintrequest.price - _platformFee; + assertEq(caller.balance, 1000 ether - _mintrequest.price); + + (address _platformFeeRecipient, ) = tokenContract.getPlatformFeeInfo(); + assertEq(_platformFeeRecipient.balance, _platformFee); + assertEq(tokenContract.primarySaleRecipient().balance, _saleProceeds); + } + + function test_mintWithSignature_nonZeroPrice_nativeToken_TokensMintedWithSignatureEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenCorrectMsgValue + { + _mintrequest.price = 10; + _mintrequest.currency = NATIVE_TOKEN; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(caller); + vm.expectEmit(true, true, false, true); + emit TokensMintedWithSignature(signer, _mintrequest.to, _mintrequest); + tokenContract.mintWithSignature{ value: _mintrequest.price }(_mintrequest, _signature); + } + + function test_mintWithSignature_nonZeroPrice_ERC20_nonZeroMsgValue() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + { + _mintrequest.price = 10; + _mintrequest.currency = address(erc20); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("msg value not zero"); + vm.prank(caller); + tokenContract.mintWithSignature{ value: 1 }(_mintrequest, _signature); + } + + function test_mintWithSignature_nonZeroPrice_ERC20() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.price = 10; + _mintrequest.currency = address(erc20); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // mint + vm.prank(caller); + tokenContract.mintWithSignature{ value: 0 }(_mintrequest, _signature); + + // check state after + assertEq(tokenContract.balanceOf(recipient), _mintrequest.quantity); + + uint256 _platformFee = (_mintrequest.price * platformFeeBps) / 10_000; + uint256 _saleProceeds = _mintrequest.price - _platformFee; + assertEq(erc20.balanceOf(caller), 1000 ether - _mintrequest.price); + (address _platformFeeRecipient, ) = tokenContract.getPlatformFeeInfo(); + assertEq(erc20.balanceOf(_platformFeeRecipient), _platformFee); + assertEq(erc20.balanceOf(tokenContract.primarySaleRecipient()), _saleProceeds); + } + + function test_mintWithSignature_nonZeroPrice_ERC20_TokensMintedWithSignatureEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.price = 10; + _mintrequest.currency = address(erc20); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(caller); + vm.expectEmit(true, true, false, true); + emit TokensMintedWithSignature(signer, _mintrequest.to, _mintrequest); + tokenContract.mintWithSignature{ value: 0 }(_mintrequest, _signature); + } + + // ================== + // ======= Test branch: other cases + // ================== +} diff --git a/src/test/tokenerc20-BTT/mint-with-signature/mintWithSignature.tree b/src/test/tokenerc20-BTT/mint-with-signature/mintWithSignature.tree new file mode 100644 index 000000000..d7cfcdcab --- /dev/null +++ b/src/test/tokenerc20-BTT/mint-with-signature/mintWithSignature.tree @@ -0,0 +1,51 @@ +mintWithSignature(MintRequest calldata _req, bytes calldata _signature) +├── when signer doesn't have MINTER_ROLE +│ └── it should revert ✅ +└── when signer has MINTER_ROLE + └── when `_req.uid` has already been used + │ └── it should revert ✅ + └── when `_req.uid` has not been used + └── when `_req.validityStartTimestamp` is greater than block timestamp + │ └── it should revert ✅ + └── when `_req.validityStartTimestamp` is less than or equal to block timestamp + └── when `_req.validityEndTimestamp` is less than block timestamp + │ └── it should revert ✅ + └── when `_req.validityEndTimestamp` is greater than or equal to block timestamp + └── when `_req.to` is address(0) + │ └── it should revert ✅ + └── when `_req.to` is not address(0) + ├── when `_req.quantity` is zero + │ └── it should revert ✅ + └── when `_req.quantity` is not zero + │ + │ // case: price is zero + └── when `_req.price` is zero + │ └── when msg.value is not zero + │ │ └── it should revert ✅ + │ └── when msg.value is zero + │ └── it should mint `amount` to `to` ✅ + │ └── it should emit TokensMintedWithSignature event ✅ + │ + │ // case: price is not zero + └── when `_req.price` is not zero + └── when currency is native token + │ └── when msg.value is not equal to total price + │ │ └── it should revert ✅ + │ └── when msg.value is equal to total price + │ └── it should mint `amount` to `to` ✅ + │ └── (transfer to sale recipient) ✅ + │ └── (transfer to fee recipient) ✅ + │ └── it should emit TokensMintedWithSignature event ✅ + └── when currency is some ERC20 token + └── when msg.value is not zero + │ └── it should revert ✅ + └── when msg.value is zero + └── it should mint `amount` to `to` ✅ + └── (transfer to sale recipient) ✅ + └── (transfer to fee recipient) ✅ + └── it should emit TokensMintedWithSignature event ✅ + +// other cases + + + diff --git a/src/test/tokenerc20-BTT/other-functions/other.t.sol b/src/test/tokenerc20-BTT/other-functions/other.t.sol new file mode 100644 index 000000000..70b62669e --- /dev/null +++ b/src/test/tokenerc20-BTT/other-functions/other.t.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; +import { IStaking20 } from "contracts/extension/interface/IStaking20.sol"; + +import "@openzeppelin/contracts-upgradeable/access/IAccessControlEnumerableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/IAccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/introspection/IERC165Upgradeable.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC20 is TokenERC20 { + function beforeTokenTransfer(address from, address to, uint256 amount) external { + _beforeTokenTransfer(from, to, amount); + } + + function mint(address account, uint256 amount) external { + _mint(account, amount); + } + + function burn(address account, uint256 amount) external { + _burn(account, amount); + } +} + +contract TokenERC20Test_OtherFunctions is BaseTest { + address public implementation; + address public proxy; + + MyTokenERC20 public tokenContract; + address internal caller; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC20()); + caller = getActor(3); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ) + ); + + tokenContract = MyTokenERC20(proxy); + } + + function test_contractType() public { + assertEq(tokenContract.contractType(), bytes32("TokenERC20")); + } + + function test_contractVersion() public { + assertEq(tokenContract.contractVersion(), uint8(1)); + } + + function test_beforeTokenTransfer_restricted_notTransferRole() public { + vm.prank(deployer); + tokenContract.revokeRole(keccak256("TRANSFER_ROLE"), address(0)); + vm.expectRevert("transfers restricted."); + tokenContract.beforeTokenTransfer(caller, address(0x123), 100); + } + + modifier whenTransferRole() { + vm.prank(deployer); + tokenContract.grantRole(keccak256("TRANSFER_ROLE"), caller); + _; + } + + function test_beforeTokenTransfer_restricted() public whenTransferRole { + tokenContract.beforeTokenTransfer(caller, address(0x123), 100); + } + + function test_mint() public { + tokenContract.mint(caller, 100); + assertEq(tokenContract.balanceOf(caller), 100); + } + + function test_burn() public { + tokenContract.mint(caller, 100); + assertEq(tokenContract.balanceOf(caller), 100); + + tokenContract.burn(caller, 60); + assertEq(tokenContract.balanceOf(caller), 40); + } +} diff --git a/src/test/tokenerc20-BTT/other-functions/other.tree b/src/test/tokenerc20-BTT/other-functions/other.tree new file mode 100644 index 000000000..57a1466a8 --- /dev/null +++ b/src/test/tokenerc20-BTT/other-functions/other.tree @@ -0,0 +1,20 @@ +contractType() +├── it should return bytes32("TokenERC20") ✅ + +contractVersion() +├── it should return uint8(1) ✅ + +_beforeTokenTransfers( + address from, + address to, + uint256 amount +) +├── when transfers are restricted (i.e. address(0) doesn't have transfer role, or from-to addresses are not address(0) + └── when from and to don't have transfer role + │ └── it should revert ✅ + +_mint(address account, uint256 amount) +├── it should mint amount to account ✅ + +_burn(address account, uint256 amount) +├── it should mint amount from account ✅ diff --git a/src/test/tokenerc20-BTT/set-contract-uri/setContractURI.t.sol b/src/test/tokenerc20-BTT/set-contract-uri/setContractURI.t.sol new file mode 100644 index 000000000..faccf3f9b --- /dev/null +++ b/src/test/tokenerc20-BTT/set-contract-uri/setContractURI.t.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC20 is TokenERC20 {} + +contract TokenERC20Test_SetContractURI is BaseTest { + address public implementation; + address public proxy; + address internal caller; + string internal _contractURI; + + MyTokenERC20 internal tokenContract; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC20()); + + caller = getActor(1); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ) + ); + + tokenContract = MyTokenERC20(proxy); + _contractURI = "ipfs://contracturi"; + } + + function test_setContractURI_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(caller), 20), + " is missing role ", + Strings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setContractURI(_contractURI); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setContractURI_empty() public whenCallerAuthorized { + vm.prank(address(caller)); + tokenContract.setContractURI(""); + + // get contract uri + assertEq(tokenContract.contractURI(), ""); + } + + function test_setContractURI_notEmpty() public whenCallerAuthorized { + vm.prank(address(caller)); + tokenContract.setContractURI(_contractURI); + + // get contract uri + assertEq(tokenContract.contractURI(), _contractURI); + } +} diff --git a/src/test/tokenerc20-BTT/set-contract-uri/setContractURI.tree b/src/test/tokenerc20-BTT/set-contract-uri/setContractURI.tree new file mode 100644 index 000000000..8fc480b19 --- /dev/null +++ b/src/test/tokenerc20-BTT/set-contract-uri/setContractURI.tree @@ -0,0 +1,8 @@ +setContractURI(string calldata _uri) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── when `uri` is empty + │ └── it should update contract URI to empty string ✅ + └── when `uri` is not empty + └── it should update contract URI to `_uri` ✅ \ No newline at end of file diff --git a/src/test/tokenerc20-BTT/set-platform-fee-info/setPlatformFeeInfo.t.sol b/src/test/tokenerc20-BTT/set-platform-fee-info/setPlatformFeeInfo.t.sol new file mode 100644 index 000000000..d2a14a7f1 --- /dev/null +++ b/src/test/tokenerc20-BTT/set-platform-fee-info/setPlatformFeeInfo.t.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC20 is TokenERC20 {} + +contract TokenERC20Test_SetPlatformFeeInfo is BaseTest { + address public implementation; + address public proxy; + address internal caller; + address internal _platformFeeRecipient; + uint256 internal _platformFeeBps; + + MyTokenERC20 internal tokenContract; + + event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC20()); + + caller = getActor(1); + _platformFeeRecipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ) + ); + + tokenContract = MyTokenERC20(proxy); + } + + function test_setPlatformFeeInfo_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(caller), 20), + " is missing role ", + Strings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setPlatformFeeInfo_exceedMaxBps() public whenCallerAuthorized { + _platformFeeBps = 10_001; + vm.prank(address(caller)); + vm.expectRevert("exceeds MAX_BPS"); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } + + modifier whenNotExceedMaxBps() { + _platformFeeBps = 500; + _; + } + + function test_setPlatformFeeInfo() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + + // get platform fee info + (address _recipient, uint16 _bps) = tokenContract.getPlatformFeeInfo(); + assertEq(_recipient, _platformFeeRecipient); + assertEq(_bps, uint16(_platformFeeBps)); + } + + function test_setPlatformFeeInfo_event() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + vm.expectEmit(true, false, false, true); + emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } +} diff --git a/src/test/tokenerc20-BTT/set-platform-fee-info/setPlatformFeeInfo.tree b/src/test/tokenerc20-BTT/set-platform-fee-info/setPlatformFeeInfo.tree new file mode 100644 index 000000000..dcef9965e --- /dev/null +++ b/src/test/tokenerc20-BTT/set-platform-fee-info/setPlatformFeeInfo.tree @@ -0,0 +1,10 @@ +setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + ├── when `_platformFeeBps` is greater than MAX_BPS + │ └── it should revert ✅ + └── when `_platformFeeBps` is less than or equal to MAX_BPS + └── it should update platform fee recipient ✅ + └── it should update platform fee bps ✅ + └── it should emit PlatformFeeInfoUpdated event ✅ \ No newline at end of file diff --git a/src/test/tokenerc20-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.t.sol b/src/test/tokenerc20-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.t.sol new file mode 100644 index 000000000..070f9bfaf --- /dev/null +++ b/src/test/tokenerc20-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.t.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC20 is TokenERC20 {} + +contract TokenERC20Test_SetPrimarySaleRecipient is BaseTest { + address public implementation; + address public proxy; + address internal caller; + address internal _primarySaleRecipient; + + MyTokenERC20 internal tokenContract; + + event PrimarySaleRecipientUpdated(address indexed recipient); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC20()); + + caller = getActor(1); + _primarySaleRecipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ) + ); + + tokenContract = MyTokenERC20(proxy); + } + + function test_setPrimarySaleRecipient_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(caller), 20), + " is missing role ", + Strings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setPrimarySaleRecipient() public whenCallerAuthorized { + vm.prank(address(caller)); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + + // get primary sale recipient info + assertEq(tokenContract.primarySaleRecipient(), _primarySaleRecipient); + } + + function test_setPrimarySaleRecipient_event() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectEmit(true, false, false, false); + emit PrimarySaleRecipientUpdated(_primarySaleRecipient); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + } +} diff --git a/src/test/tokenerc20-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.tree b/src/test/tokenerc20-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.tree new file mode 100644 index 000000000..230035a07 --- /dev/null +++ b/src/test/tokenerc20-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.tree @@ -0,0 +1,6 @@ +setPrimarySaleRecipient(address _saleRecipient) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── it should update primary sale recipient ✅ + └── it should emit PrimarySaleRecipientUpdated event ✅ \ No newline at end of file diff --git a/src/test/tokenerc20-BTT/verify/verify.t.sol b/src/test/tokenerc20-BTT/verify/verify.t.sol new file mode 100644 index 000000000..fb54c7868 --- /dev/null +++ b/src/test/tokenerc20-BTT/verify/verify.t.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC20 is TokenERC20 { + function setMintedUID(MintRequest calldata _req, bytes calldata _signature) external { + verifyRequest(_req, _signature); + } +} + +contract TokenERC20Test_Verify is BaseTest { + address public implementation; + address public proxy; + + MyTokenERC20 internal tokenContract; + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + TokenERC20.MintRequest _mintrequest; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC20()); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ) + ); + + tokenContract = MyTokenERC20(proxy); + + typehashMintRequest = keccak256( + "MintRequest(address to,address primarySaleRecipient,uint256 quantity,uint256 price,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + nameHash = keccak256(bytes(NAME)); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256( + abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(tokenContract)) + ); + + // construct default mintrequest + _mintrequest.to = address(123); + _mintrequest.primarySaleRecipient = saleRecipient; + _mintrequest.quantity = 100; + _mintrequest.price = 0; + _mintrequest.currency = address(0); + _mintrequest.validityStartTimestamp = 0; + _mintrequest.validityEndTimestamp = 2000; + _mintrequest.uid = bytes32(0); + } + + function signMintRequest( + TokenERC20.MintRequest memory _request, + uint256 _privateKey + ) internal view returns (bytes memory) { + bytes memory encodedRequest = abi.encode( + typehashMintRequest, + _request.to, + _request.primarySaleRecipient, + _request.quantity, + _request.price, + _request.currency, + _request.validityStartTimestamp, + _request.validityEndTimestamp, + _request.uid + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, typedDataHash); + bytes memory sig = abi.encodePacked(r, s, v); + + return sig; + } + + function test_verify_notMinterRole() public { + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + (bool _isValid, address _recoveredSigner) = tokenContract.verify(_mintrequest, _signature); + + assertFalse(_isValid); + assertEq(_recoveredSigner, signer); + } + + modifier whenMinterRole() { + vm.prank(deployer); + tokenContract.grantRole(keccak256("MINTER_ROLE"), signer); + _; + } + + function test_verify_invalidUID() public whenMinterRole { + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // set state with this mintrequest and signature, marking the UID as used + tokenContract.setMintedUID(_mintrequest, _signature); + + // pass the same UID mintrequest again + (bool _isValid, address _recoveredSigner) = tokenContract.verify(_mintrequest, _signature); + + assertFalse(_isValid); + assertEq(_recoveredSigner, signer); + } + + modifier whenUidNotUsed() { + _; + } + + function test_verify() public whenMinterRole whenUidNotUsed { + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + (bool _isValid, address _recoveredSigner) = tokenContract.verify(_mintrequest, _signature); + + assertTrue(_isValid); + assertEq(_recoveredSigner, signer); + } +} diff --git a/src/test/tokenerc20-BTT/verify/verify.tree b/src/test/tokenerc20-BTT/verify/verify.tree new file mode 100644 index 000000000..c160faa0f --- /dev/null +++ b/src/test/tokenerc20-BTT/verify/verify.tree @@ -0,0 +1,12 @@ +verify(MintRequest calldata _req, bytes calldata _signature) +├── when signer doesn't have MINTER_ROLE +│ └── it should return false ✅ +│ └── it should return recovered signer equal to the actual signer of the request ✅ +└── when signer has MINTER_ROLE + └── when `_req.uid` has already been used + │ └── it should return false ✅ + │ └── it should return recovered signer equal to the actual signer of the request ✅ + └── when `_req.uid` has not been used + └── it should return true ✅ + └── it should return recovered signer equal to the actual signer of the request ✅ + diff --git a/src/test/tokenerc721-BTT/burn/burn.t.sol b/src/test/tokenerc721-BTT/burn/burn.t.sol new file mode 100644 index 000000000..01fa7b43c --- /dev/null +++ b/src/test/tokenerc721-BTT/burn/burn.t.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC721 is TokenERC721 {} + +contract TokenERC721Test_Burn is BaseTest { + address public implementation; + address public proxy; + address public caller; + address public recipient; + string public uri; + + MyTokenERC721 internal tokenContract; + + event MetadataUpdate(uint256 _tokenId); + event TokensMinted(address indexed mintedTo, uint256 indexed tokenIdMinted, string uri); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC721()); + caller = getActor(1); + recipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC721(proxy); + uri = "uri"; + + vm.prank(deployer); + tokenContract.grantRole(keccak256("MINTER_ROLE"), caller); + } + + function test_burn_whenNotOwnerNorApproved() public { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + uint256 _tokenId = tokenContract.mintTo(recipient, uri); + + // burn + vm.expectRevert("ERC721Burnable: caller is not owner nor approved"); + tokenContract.burn(_tokenId); + } + + function test_burn_whenOwner() public { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + uint256 _tokenId = tokenContract.mintTo(recipient, uri); + + // burn + vm.prank(recipient); + tokenContract.burn(_tokenId); + + vm.expectRevert(); // checking non-existent token, because burned + tokenContract.ownerOf(_tokenId); + } + + function test_burn_whenApproved() public { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + uint256 _tokenId = tokenContract.mintTo(recipient, uri); + + vm.prank(recipient); + tokenContract.setApprovalForAll(caller, true); + + // burn + vm.prank(caller); + tokenContract.burn(_tokenId); + + vm.expectRevert(); // checking non-existent token, because burned + tokenContract.ownerOf(_tokenId); + } +} diff --git a/src/test/tokenerc721-BTT/burn/burn.tree b/src/test/tokenerc721-BTT/burn/burn.tree new file mode 100644 index 000000000..0a6e2ff43 --- /dev/null +++ b/src/test/tokenerc721-BTT/burn/burn.tree @@ -0,0 +1,8 @@ +burn(uint256 tokenId) +├── when the caller isn't the owner of `tokenId` or token not approved to caller +│ └── it should revert ✅ +└── when the caller owns `tokenId` +│ └── it should burn the token ✅ +└── when the `tokenId` is approved to caller + └── it should burn the token ✅ + diff --git a/src/test/tokenerc721-BTT/initialize/initialize.t.sol b/src/test/tokenerc721-BTT/initialize/initialize.t.sol new file mode 100644 index 000000000..e16c27cf7 --- /dev/null +++ b/src/test/tokenerc721-BTT/initialize/initialize.t.sol @@ -0,0 +1,311 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC721 is TokenERC721 { + function eip712NameHash() external view returns (bytes32) { + return _EIP712NameHash(); + } + + function eip712VersionHash() external view returns (bytes32) { + return _EIP712VersionHash(); + } +} + +contract TokenERC721Test_Initialize is BaseTest { + address public implementation; + address public proxy; + + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC721()); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + } + + function test_initialize_initializingImplementation() public { + vm.expectRevert("Initializable: contract is already initialized"); + TokenERC721(implementation).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + modifier whenNotImplementation() { + _; + } + + function test_initialize_proxyAlreadyInitialized() public whenNotImplementation { + vm.expectRevert("Initializable: contract is already initialized"); + MyTokenERC721(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + modifier whenProxyNotInitialized() { + proxy = address(new TWProxy(implementation, "")); + _; + } + + function test_initialize_exceedsMaxBps() public whenNotImplementation whenProxyNotInitialized { + vm.expectRevert("exceeds MAX_BPS"); + MyTokenERC721(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + uint128(MAX_BPS) + 1, // platformFeeBps greater than MAX_BPS + platformFeeRecipient + ); + } + + modifier whenPlatformFeeBpsWithinMaxBps() { + _; + } + + function test_initialize() public whenNotImplementation whenProxyNotInitialized whenPlatformFeeBpsWithinMaxBps { + MyTokenERC721(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + + // check state + MyTokenERC721 tokenContract = MyTokenERC721(proxy); + + assertEq(tokenContract.eip712NameHash(), keccak256(bytes("TokenERC721"))); + assertEq(tokenContract.eip712VersionHash(), keccak256(bytes("1"))); + + address[] memory _trustedForwarders = forwarders(); + for (uint256 i = 0; i < _trustedForwarders.length; i++) { + assertTrue(tokenContract.isTrustedForwarder(_trustedForwarders[i])); + } + + assertEq(tokenContract.name(), NAME); + assertEq(tokenContract.symbol(), SYMBOL); + assertEq(tokenContract.contractURI(), CONTRACT_URI); + + (address _platformFeeRecipient, uint16 _platformFeeBps) = tokenContract.getPlatformFeeInfo(); + assertEq(_platformFeeBps, platformFeeBps); + assertEq(_platformFeeRecipient, platformFeeRecipient); + assertEq(tokenContract.platformFeeRecipient(), platformFeeRecipient); + + (address _royaltyRecipient, uint16 _royaltyBps) = tokenContract.getDefaultRoyaltyInfo(); + (address _royaltyRecipientForToken, uint16 _royaltyBpsForToken) = tokenContract.getRoyaltyInfoForToken(1); // random tokenId + assertEq(_royaltyBps, royaltyBps); + assertEq(_royaltyRecipient, royaltyRecipient); + assertEq(_royaltyRecipient, _royaltyRecipientForToken); + assertEq(_royaltyBps, _royaltyBpsForToken); + + assertEq(tokenContract.primarySaleRecipient(), saleRecipient); + + assertEq(tokenContract.owner(), deployer); + assertTrue(tokenContract.hasRole(bytes32(0x00), deployer)); + assertTrue(tokenContract.hasRole(keccak256("TRANSFER_ROLE"), deployer)); + assertTrue(tokenContract.hasRole(keccak256("TRANSFER_ROLE"), address(0))); + assertTrue(tokenContract.hasRole(keccak256("MINTER_ROLE"), deployer)); + assertTrue(tokenContract.hasRole(keccak256("METADATA_ROLE"), deployer)); + assertEq(tokenContract.getRoleAdmin(keccak256("METADATA_ROLE")), keccak256("METADATA_ROLE")); + } + + function test_initialize_event_RoleGranted_DefaultAdmin() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _defaultAdminRole = bytes32(0x00); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_defaultAdminRole, deployer, deployer); + MyTokenERC721(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleGranted_MinterRole() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _minterRole = keccak256("MINTER_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_minterRole, deployer, deployer); + MyTokenERC721(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleGranted_TransferRole() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _transferRole = keccak256("TRANSFER_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_transferRole, deployer, deployer); + MyTokenERC721(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleGranted_TransferRole_AddressZero() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _transferRole = keccak256("TRANSFER_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_transferRole, address(0), deployer); + MyTokenERC721(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleGranted_MetadataRole() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _metadataRole = keccak256("METADATA_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_metadataRole, deployer, deployer); + MyTokenERC721(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleAdminChanged_MetadataRole() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _metadataRole = keccak256("METADATA_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleAdminChanged(_metadataRole, bytes32(0x00), _metadataRole); + MyTokenERC721(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } +} diff --git a/src/test/tokenerc721-BTT/initialize/initialize.tree b/src/test/tokenerc721-BTT/initialize/initialize.tree new file mode 100644 index 000000000..041a9e248 --- /dev/null +++ b/src/test/tokenerc721-BTT/initialize/initialize.tree @@ -0,0 +1,42 @@ +initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _royaltyRecipient, + uint128 _royaltyBps, + uint128 _platformFeeBps, + address _platformFeeRecipient +) +├── when initializing the implementation contract (not proxy) +│ └── it should revert ✅ +└── when it is a proxy to the implementation + └── when it is already initialized + │ └── it should revert ✅ + └── when it is not initialized + └── when platformFeeBps is greater than MAX_BPS + │ └── it should revert ✅ + └── when platformFeeBps is less than or equal to MAX_BPS + └── it should correctly set EIP712 name hash and version hash ✅ + └── it should set trustedForwarder mapping to true for all addresses in `_trustedForwarders` ✅ + └── it should set _name and _symbol to `_name` and `_symbol` param values respectively ✅ + └── it should set contractURI to `_contractURI` param value ✅ + └── it should set platformFeeRecipient and platformFeeBps as `_platformFeeRecipient` and `_platformFeeBps` respectively ✅ + └── it should set royaltyRecipient and royaltyBps as `_royaltyRecipient` and `_royaltyBps` respectively ✅ + └── it should set primary sale recipient as `_saleRecipient` param value ✅ + └── it should set _owner to `_defaultAdmin` param value ✅ + └── it should grant 0x00 (DEFAULT_ADMIN_ROLE) to `_defaultAdmin` address ✅ + └── it should emit RoleGranted event ✅ + └── it should grant MINTER_ROLE to `_defaultAdmin` address ✅ + └── it should emit RoleGranted event ✅ + └── it should grant TRANSFER_ROLE to `_defaultAdmin` address ✅ + └── it should emit RoleGranted event ✅ + └── it should grant TRANSFER_ROLE to address(0) ✅ + └── it should emit RoleGranted event ✅ + └── it should grant METADATA_ROLE to `_defaultAdmin` address ✅ + └── it should emit RoleGranted event ✅ + └── it should set METADATA_ROLE as role admin for METADATA_ROLE ✅ + └── it should emit RoleAdminChanged event ✅ + diff --git a/src/test/tokenerc721-BTT/mint-to/mintTo.t.sol b/src/test/tokenerc721-BTT/mint-to/mintTo.t.sol new file mode 100644 index 000000000..4c9900e4c --- /dev/null +++ b/src/test/tokenerc721-BTT/mint-to/mintTo.t.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC721 is TokenERC721 {} + +contract ERC721ReceiverCompliant is IERC721Receiver { + function onERC721Received( + address, + address, + uint256, + bytes calldata + ) external view virtual override returns (bytes4) { + return this.onERC721Received.selector; + } +} + +contract TokenERC721Test_MintTo is BaseTest { + address public implementation; + address public proxy; + address public caller; + address public recipient; + string public uri; + + MyTokenERC721 internal tokenContract; + ERC721ReceiverCompliant internal erc721ReceiverContract; + + event MetadataUpdate(uint256 _tokenId); + event TokensMinted(address indexed mintedTo, uint256 indexed tokenIdMinted, string uri); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC721()); + erc721ReceiverContract = new ERC721ReceiverCompliant(); + caller = getActor(1); + recipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC721(proxy); + } + + function test_mintTo_notMinterRole() public { + vm.prank(caller); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(caller), 20), + " is missing role ", + Strings.toHexString(uint256(keccak256("MINTER_ROLE")), 32) + ) + ); + tokenContract.mintTo(recipient, uri); + } + + modifier whenMinterRole() { + vm.prank(deployer); + tokenContract.grantRole(keccak256("MINTER_ROLE"), caller); + _; + } + + function test_mintTo_emptyUri() public whenMinterRole { + vm.prank(caller); + vm.expectRevert("empty uri."); + tokenContract.mintTo(recipient, uri); + } + + modifier whenNotEmptyUri() { + uri = "ipfs://uri/1"; + _; + } + + // ================== + // ======= Test branch: recipient EOA + // ================== + + function test_mintTo_EOA() public whenMinterRole whenNotEmptyUri { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + uint256 _tokenId = tokenContract.mintTo(recipient, uri); + + // check state after + assertEq(_tokenId, _tokenIdToMint); + assertEq(tokenContract.tokenURI(_tokenId), uri); + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.ownerOf(_tokenId), recipient); + } + + function test_mintTo_EOA_MetadataUpdateEvent() public whenMinterRole whenNotEmptyUri { + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(false, false, false, true); + emit MetadataUpdate(_tokenIdToMint); + tokenContract.mintTo(recipient, uri); + } + + function test_mintTo_EOA_TokensMintedEvent() public whenMinterRole whenNotEmptyUri { + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(true, true, false, true); + emit TokensMinted(recipient, _tokenIdToMint, uri); + tokenContract.mintTo(recipient, uri); + } + + // ================== + // ======= Test branch: recipient is a contract + // ================== + + function test_mintTo_nonERC721ReceiverContract() public whenMinterRole whenNotEmptyUri { + recipient = address(this); + vm.prank(caller); + vm.expectRevert(); + uint256 _tokenId = tokenContract.mintTo(recipient, uri); + } + + modifier whenERC721Receiver() { + recipient = address(erc721ReceiverContract); + _; + } + + function test_mintTo_contract() public whenMinterRole whenNotEmptyUri whenERC721Receiver { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + uint256 _tokenId = tokenContract.mintTo(recipient, uri); + + // check state after + assertEq(_tokenId, _tokenIdToMint); + assertEq(tokenContract.tokenURI(_tokenId), uri); + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.ownerOf(_tokenId), recipient); + } + + function test_mintTo_contract_MetadataUpdateEvent() public whenMinterRole whenNotEmptyUri whenERC721Receiver { + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(false, false, false, true); + emit MetadataUpdate(_tokenIdToMint); + tokenContract.mintTo(recipient, uri); + } + + function test_mintTo_contract_TokensMintedEvent() public whenMinterRole whenNotEmptyUri whenERC721Receiver { + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(true, true, false, true); + emit TokensMinted(recipient, _tokenIdToMint, uri); + tokenContract.mintTo(recipient, uri); + } +} diff --git a/src/test/tokenerc721-BTT/mint-to/mintTo.tree b/src/test/tokenerc721-BTT/mint-to/mintTo.tree new file mode 100644 index 000000000..408dd48c9 --- /dev/null +++ b/src/test/tokenerc721-BTT/mint-to/mintTo.tree @@ -0,0 +1,25 @@ +mintTo(address _to, string calldata _uri) +├── when caller doesn't have MINTER_ROLE + │ └── it should revert ✅ + └── when caller has MINTER_ROLE + ├── when `_uri` is empty i.e. length is zero + │ └── it should revert ✅ + └── when `_uri` is not empty + ├── when `_to` address is an EOA + │ └── it should mint tokenId equal to current value of `nextTokenIdToMint` ✅ + │ └── it should set tokenURI for minted tokenId equal to `_uri` ✅ + │ └── it should increment `nextTokenIdToMint` by 1 ✅ + │ └── it should mint the tokenId to the `_to` address ✅ + │ └── it should emit MetadataUpdate event ✅ + │ └── it should emit TokensMinted event ✅ + └── when `_to` address is a contract + ├── when `_to` address is non ERC721Receiver implementer + │ └── it should revert ✅ + └── when `_to` address implements ERC721Receiver + └── it should mint tokenId equal to current value of `nextTokenIdToMint` ✅ + └── it should set tokenURI for minted tokenId equal to `_uri` ✅ + └── it should increment `nextTokenIdToMint` by 1 ✅ + └── it should mint the tokenId to the `_to` address ✅ + └── it should emit MetadataUpdate event ✅ + └── it should emit TokensMinted event ✅ + diff --git a/src/test/tokenerc721-BTT/mint-with-signature/mintWithSignature.t.sol b/src/test/tokenerc721-BTT/mint-with-signature/mintWithSignature.t.sol new file mode 100644 index 000000000..4ad653f7b --- /dev/null +++ b/src/test/tokenerc721-BTT/mint-with-signature/mintWithSignature.t.sol @@ -0,0 +1,717 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC721 is TokenERC721 { + function setMintedURI(MintRequest calldata _req, bytes calldata _signature) external { + verifyRequest(_req, _signature); + } +} + +contract ERC721ReceiverCompliant is IERC721Receiver { + function onERC721Received( + address, + address, + uint256, + bytes calldata + ) external view virtual override returns (bytes4) { + return this.onERC721Received.selector; + } +} + +contract ReentrantContract { + fallback() external payable { + TokenERC721.MintRequest memory _mintrequest; + bytes memory _signature; + MyTokenERC721(msg.sender).mintWithSignature(_mintrequest, _signature); + } +} + +contract TokenERC721Test_MintWithSignature is BaseTest { + address public implementation; + address public proxy; + address public caller; + address public recipient; + string public uri; + + MyTokenERC721 internal tokenContract; + ERC721ReceiverCompliant internal erc721ReceiverContract; + + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + TokenERC721.MintRequest _mintrequest; + + event MetadataUpdate(uint256 _tokenId); + event TokensMinted(address indexed mintedTo, uint256 indexed tokenIdMinted, string uri); + event TokensMintedWithSignature( + address indexed signer, + address indexed mintedTo, + uint256 indexed tokenIdMinted, + TokenERC721.MintRequest mintRequest + ); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC721()); + erc721ReceiverContract = new ERC721ReceiverCompliant(); + caller = getActor(1); + recipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC721(proxy); + + typehashMintRequest = keccak256( + "MintRequest(address to,address royaltyRecipient,uint256 royaltyBps,address primarySaleRecipient,string uri,uint256 price,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + nameHash = keccak256(bytes("TokenERC721")); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256( + abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(tokenContract)) + ); + + // construct default mintrequest + _mintrequest.to = address(0x1234); + _mintrequest.royaltyRecipient = royaltyRecipient; + _mintrequest.royaltyBps = royaltyBps; + _mintrequest.primarySaleRecipient = saleRecipient; + _mintrequest.uri = "ipfs://"; + _mintrequest.price = 0; + _mintrequest.currency = address(0); + _mintrequest.validityStartTimestamp = 0; + _mintrequest.validityEndTimestamp = 2000; + _mintrequest.uid = bytes32(0); + + erc20.mint(deployer, 1_000 ether); + vm.deal(deployer, 1_000 ether); + erc20.mint(caller, 1_000 ether); + vm.deal(caller, 1_000 ether); + + vm.startPrank(deployer); + erc20.approve(address(tokenContract), type(uint256).max); + vm.stopPrank(); + + vm.startPrank(caller); + erc20.approve(address(tokenContract), type(uint256).max); + vm.stopPrank(); + } + + function signMintRequest( + TokenERC721.MintRequest memory _request, + uint256 _privateKey + ) internal view returns (bytes memory) { + bytes memory encodedRequest = abi.encode( + typehashMintRequest, + _request.to, + _request.royaltyRecipient, + _request.royaltyBps, + _request.primarySaleRecipient, + keccak256(bytes(_request.uri)), + _request.price, + _request.currency, + _request.validityStartTimestamp, + _request.validityEndTimestamp, + _request.uid + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, typedDataHash); + bytes memory sig = abi.encodePacked(r, s, v); + + return sig; + } + + function test_mintWithSignature_notMinterRole() public { + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(caller); + vm.expectRevert("invalid signature"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenMinterRole() { + vm.prank(deployer); + tokenContract.grantRole(keccak256("MINTER_ROLE"), signer); + _; + } + + function test_mintWithSignature_invalidUID() public whenMinterRole { + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // set state with this mintrequest and signature, marking the UID as used + tokenContract.setMintedURI(_mintrequest, _signature); + + // pass the same UID mintrequest again + vm.prank(caller); + vm.expectRevert("invalid signature"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenUidNotUsed() { + _; + } + + function test_mintWithSignature_invalidStartTimestamp() public whenMinterRole whenUidNotUsed { + _mintrequest.validityStartTimestamp = uint128(block.timestamp + 1); + + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("request expired"); + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenValidStartTimestamp() { + _; + } + + function test_mintWithSignature_invalidEndTimestamp() public whenMinterRole whenUidNotUsed whenValidStartTimestamp { + _mintrequest.validityEndTimestamp = uint128(block.timestamp - 1); + + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("request expired"); + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenValidEndTimestamp() { + _; + } + + function test_mintWithSignature_recipientAddressZero() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + { + _mintrequest.to = address(0); + + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("recipient undefined"); + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenRecipientAddressNotZero() { + _; + } + + function test_mintWithSignature_emptyUri() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + { + _mintrequest.uri = ""; + + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("empty uri."); + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenNotEmptyUri() { + _; + } + + // ================== + // ======= Test branch: when mint price is zero + // ================== + + function test_mintWithSignature_zeroPrice_msgValueNonZero() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotEmptyUri + { + _mintrequest.price = 0; + + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("!Value"); + vm.prank(caller); + tokenContract.mintWithSignature{ value: 1 }(_mintrequest, _signature); + } + + modifier whenMsgValueZero() { + _; + } + + function test_mintWithSignature_zeroPrice_EOA() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotEmptyUri + whenMsgValueZero + { + _mintrequest.price = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + uint256 _tokenId = tokenContract.mintWithSignature(_mintrequest, _signature); + + // check state after + assertEq(_tokenId, _tokenIdToMint); + assertEq(tokenContract.tokenURI(_tokenId), _mintrequest.uri); + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.ownerOf(_tokenId), _mintrequest.to); + } + + function test_mintWithSignature_zeroPrice_EOA_MetadataUpdateEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotEmptyUri + whenMsgValueZero + { + _mintrequest.price = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(false, false, false, true); + emit MetadataUpdate(_tokenIdToMint); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_mintWithSignature_zeroPrice_EOA_TokensMintedWithSignatureEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotEmptyUri + whenMsgValueZero + { + _mintrequest.price = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(true, true, false, true); + emit TokensMintedWithSignature(signer, _mintrequest.to, _tokenIdToMint, _mintrequest); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_mintWithSignature_zeroPrice_nonERC721ReceiverContract() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotEmptyUri + whenMsgValueZero + { + _mintrequest.price = 0; + _mintrequest.to = address(this); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // mint + vm.prank(caller); + vm.expectRevert(); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenERC721Receiver() { + _mintrequest.to = address(erc721ReceiverContract); + _; + } + + function test_mintWithSignature_zeroPrice_contract() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotEmptyUri + whenMsgValueZero + whenERC721Receiver + { + _mintrequest.price = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + uint256 _tokenId = tokenContract.mintWithSignature(_mintrequest, _signature); + + // check state after + assertEq(_tokenId, _tokenIdToMint); + assertEq(tokenContract.tokenURI(_tokenId), _mintrequest.uri); + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.ownerOf(_tokenId), _mintrequest.to); + } + + function test_mintWithSignature_zeroPrice_contract_MetadataUpdateEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotEmptyUri + whenMsgValueZero + whenERC721Receiver + { + _mintrequest.price = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(false, false, false, true); + emit MetadataUpdate(_tokenIdToMint); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_mintWithSignature_zeroPrice_contract_TokensMintedWithSignatureEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotEmptyUri + whenMsgValueZero + whenERC721Receiver + { + _mintrequest.price = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(true, true, true, true); + emit TokensMintedWithSignature(signer, _mintrequest.to, _tokenIdToMint, _mintrequest); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + // ================== + // ======= Test branch: when mint price is not zero + // ================== + + function test_mintWithSignature_nonZeroPrice_nativeToken_incorrectMsgValue() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotEmptyUri + { + _mintrequest.price = 10; + _mintrequest.currency = NATIVE_TOKEN; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 incorrectTotalPrice = (_mintrequest.price) + 1; + + vm.expectRevert("must send total price."); + vm.prank(caller); + tokenContract.mintWithSignature{ value: incorrectTotalPrice }(_mintrequest, _signature); + } + + modifier whenCorrectMsgValue() { + _; + } + + function test_mintWithSignature_nonZeroPrice_nativeToken() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotEmptyUri + whenCorrectMsgValue + { + _mintrequest.price = 10; + _mintrequest.currency = NATIVE_TOKEN; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + uint256 _tokenId = tokenContract.mintWithSignature{ value: _mintrequest.price }(_mintrequest, _signature); + + // check state after + assertEq(_tokenId, _tokenIdToMint); + assertEq(tokenContract.tokenURI(_tokenId), _mintrequest.uri); + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.ownerOf(_tokenId), _mintrequest.to); + + uint256 _platformFee = (_mintrequest.price * platformFeeBps) / 10_000; + uint256 _saleProceeds = _mintrequest.price - _platformFee; + assertEq(caller.balance, 1000 ether - _mintrequest.price); + assertEq(tokenContract.platformFeeRecipient().balance, _platformFee); + assertEq(tokenContract.primarySaleRecipient().balance, _saleProceeds); + } + + function test_mintWithSignature_nonZeroPrice_nativeToken_MetadataUpdateEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotEmptyUri + whenCorrectMsgValue + { + _mintrequest.price = 10; + _mintrequest.currency = NATIVE_TOKEN; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(false, false, false, true); + emit MetadataUpdate(_tokenIdToMint); + tokenContract.mintWithSignature{ value: _mintrequest.price }(_mintrequest, _signature); + } + + function test_mintWithSignature_nonZeroPrice_nativeToken_TokensMintedWithSignatureEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotEmptyUri + whenCorrectMsgValue + { + _mintrequest.price = 10; + _mintrequest.currency = NATIVE_TOKEN; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(true, true, true, true); + emit TokensMintedWithSignature(signer, _mintrequest.to, _tokenIdToMint, _mintrequest); + tokenContract.mintWithSignature{ value: _mintrequest.price }(_mintrequest, _signature); + } + + function test_mintWithSignature_nonZeroPrice_ERC20_nonZeroMsgValue() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotEmptyUri + { + _mintrequest.price = 10; + _mintrequest.currency = address(erc20); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("msg value not zero"); + vm.prank(caller); + tokenContract.mintWithSignature{ value: 1 }(_mintrequest, _signature); + } + + function test_mintWithSignature_nonZeroPrice_ERC20() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotEmptyUri + whenMsgValueZero + { + _mintrequest.price = 10; + _mintrequest.currency = address(erc20); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + uint256 _tokenId = tokenContract.mintWithSignature{ value: 0 }(_mintrequest, _signature); + + // check state after + assertEq(_tokenId, _tokenIdToMint); + assertEq(tokenContract.tokenURI(_tokenId), _mintrequest.uri); + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.ownerOf(_tokenId), _mintrequest.to); + + uint256 _platformFee = (_mintrequest.price * platformFeeBps) / 10_000; + uint256 _saleProceeds = _mintrequest.price - _platformFee; + assertEq(erc20.balanceOf(caller), 1000 ether - _mintrequest.price); + assertEq(erc20.balanceOf(tokenContract.platformFeeRecipient()), _platformFee); + assertEq(erc20.balanceOf(tokenContract.primarySaleRecipient()), _saleProceeds); + } + + function test_mintWithSignature_nonZeroPrice_ERC20_MetadataUpdateEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotEmptyUri + whenMsgValueZero + { + _mintrequest.price = 10; + _mintrequest.currency = address(erc20); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(false, false, false, true); + emit MetadataUpdate(_tokenIdToMint); + tokenContract.mintWithSignature{ value: 0 }(_mintrequest, _signature); + } + + function test_mintWithSignature_nonZeroPrice_ERC20_TokensMintedWithSignatureEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotEmptyUri + whenMsgValueZero + { + _mintrequest.price = 10; + _mintrequest.currency = address(erc20); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(true, true, true, true); + emit TokensMintedWithSignature(signer, _mintrequest.to, _tokenIdToMint, _mintrequest); + tokenContract.mintWithSignature{ value: 0 }(_mintrequest, _signature); + } + + // ================== + // ======= Test branch: other cases + // ================== + + function test_mintWithSignature_nonZeroRoyaltyRecipient() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotEmptyUri + whenMsgValueZero + { + _mintrequest.price = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(caller); + uint256 _tokenId = tokenContract.mintWithSignature(_mintrequest, _signature); + + (address _royaltyRecipient, uint16 _royaltyBps) = tokenContract.getRoyaltyInfoForToken(_tokenId); + assertEq(_royaltyRecipient, royaltyRecipient); + assertEq(_royaltyBps, royaltyBps); + } + + function test_mintWithSignature_royaltyRecipientZeroAddress() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotEmptyUri + whenMsgValueZero + { + _mintrequest.price = 0; + _mintrequest.royaltyRecipient = address(0); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(caller); + uint256 _tokenId = tokenContract.mintWithSignature(_mintrequest, _signature); + + (address _royaltyRecipient, uint16 _royaltyBps) = tokenContract.getRoyaltyInfoForToken(_tokenId); + (address _defaultRoyaltyRecipient, uint16 _defaultRoyaltyBps) = tokenContract.getDefaultRoyaltyInfo(); + assertEq(_royaltyRecipient, _defaultRoyaltyRecipient); + assertEq(_royaltyBps, _defaultRoyaltyBps); + } + + function test_mintWithSignature_reentrantRecipientContract() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotEmptyUri + whenMsgValueZero + { + _mintrequest.price = 0; + _mintrequest.to = address(new ReentrantContract()); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(caller); + vm.expectRevert("ReentrancyGuard: reentrant call"); + uint256 _tokenId = tokenContract.mintWithSignature(_mintrequest, _signature); + } +} diff --git a/src/test/tokenerc721-BTT/mint-with-signature/mintWithSignature.tree b/src/test/tokenerc721-BTT/mint-with-signature/mintWithSignature.tree new file mode 100644 index 000000000..347c0f674 --- /dev/null +++ b/src/test/tokenerc721-BTT/mint-with-signature/mintWithSignature.tree @@ -0,0 +1,85 @@ +mintWithSignature(MintRequest calldata _req, bytes calldata _signature) +├── when signer doesn't have MINTER_ROLE +│ └── it should revert ✅ +└── when signer has MINTER_ROLE + └── when `_req.uid` has already been used + │ └── it should revert ✅ + └── when `_req.uid` has not been used + └── when `_req.validityStartTimestamp` is greater than block timestamp + │ └── it should revert ✅ + └── when `_req.validityStartTimestamp` is less than or equal to block timestamp + └── when `_req.validityEndTimestamp` is less than block timestamp + │ └── it should revert ✅ + └── when `_req.validityEndTimestamp` is greater than or equal to block timestamp + └── when `_req.to` is address(0) + │ └── it should revert ✅ + └── when `_req.to` is not address(0) + ├── when `_req.uri` is empty i.e. length is zero + │ └── it should revert ✅ + └── when `_req.uri` is not empty + │ + │ // case: price is zero + └── when `_req.price` is zero + │ └── when msg.value is not zero + │ │ └── it should revert ✅ + │ └── when msg.value is zero + │ ├── when `_req.to` address is an EOA + │ │ └── it should mint tokenId equal to current value of `nextTokenIdToMint` ✅ + │ │ └── it should set tokenURI for minted tokenId equal to `_req.uri` ✅ + │ │ └── it should increment `nextTokenIdToMint` by 1 ✅ + │ │ └── it should mint the tokenId to the `_req.to` address ✅ + │ │ └── it should set `_req.uid` as minted ✅ + │ │ └── it should emit MetadataUpdate event ✅ + │ │ └── it should emit TokensMintedWithSignature event ✅ + │ └── when `_to` address is a contract + │ ├── when `_to` address is non ERC721Receiver implementer + │ │ └── it should revert ✅ + │ └── when `_to` address implements ERC721Receiver + │ └── it should mint tokenId equal to current value of `nextTokenIdToMint` ✅ + │ └── it should set tokenURI for minted tokenId equal to `_uri` ✅ + │ └── it should increment `nextTokenIdToMint` by 1 ✅ + │ └── it should mint the tokenId to the `_to` address ✅ + │ └── it should set `_req.uid` as minted ✅ + │ └── it should emit MetadataUpdate event ✅ + │ └── it should emit TokensMintedWithSignature event ✅ + │ + │ // case: price is not zero + └── when `_req.price` is not zero + └── when currency is native token + │ └── when msg.value is not equal to total price + │ │ └── it should revert ✅ + │ └── when msg.value is equal to total price + │ └── it should mint tokenId equal to current value of `nextTokenIdToMint` ✅ + │ └── it should set tokenURI for minted tokenId equal to `_uri` ✅ + │ └── it should increment `nextTokenIdToMint` by 1 ✅ + │ └── it should mint the tokenId to the `_to` address ✅ + │ └── it should set `_req.uid` as minted ✅ + │ └── (transfer to sale recipient) ✅ + │ └── (transfer to fee recipient) ✅ + │ └── it should emit MetadataUpdate event ✅ + │ └── it should emit TokensMintedWithSignature event ✅ + └── when currency is some ERC20 token + └── when msg.value is not zero + │ └── it should revert ✅ + └── when msg.value is zero + └── it should mint tokenId equal to current value of `nextTokenIdToMint` ✅ + └── it should set tokenURI for minted tokenId equal to `_uri` ✅ + └── it should increment `nextTokenIdToMint` by 1 ✅ + └── it should mint the tokenId to the `_to` address ✅ + └── it should set `_req.uid` as minted ✅ + └── (transfer to sale recipient) ✅ + └── (transfer to fee recipient) ✅ + └── it should emit MetadataUpdate event ✅ + └── it should emit TokensMintedWithSignature event ✅ + +// other cases + +├── when `_req.royaltyRecipient` is not address(0) + │ └── it should set royaltyInfoForToken ✅ + └── when `_req.royaltyRecipient` is address(0) + └── it should use default royalty info ✅ + +├── when reentrant call + └── it should revert ✅ + + diff --git a/src/test/tokenerc721-BTT/other-functions/other.t.sol b/src/test/tokenerc721-BTT/other-functions/other.t.sol new file mode 100644 index 000000000..43a70d662 --- /dev/null +++ b/src/test/tokenerc721-BTT/other-functions/other.t.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; +import { IStaking721 } from "contracts/extension/interface/IStaking721.sol"; +import { IERC2981 } from "contracts/eip/interface/IERC2981.sol"; + +import "@openzeppelin/contracts-upgradeable/access/IAccessControlEnumerableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/IAccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/IERC721EnumerableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/introspection/IERC165Upgradeable.sol"; +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC721 is TokenERC721 { + function canSetMetadata() public view returns (bool) { + return _canSetMetadata(); + } + + function canFreezeMetadata() public view returns (bool) { + return _canFreezeMetadata(); + } +} + +contract TokenERC721Test_OtherFunctions is BaseTest { + address public implementation; + address public proxy; + + MyTokenERC721 public tokenContract; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC721()); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC721(proxy); + } + + function test_contractType() public { + assertEq(tokenContract.contractType(), bytes32("TokenERC721")); + } + + function test_contractVersion() public { + assertEq(tokenContract.contractVersion(), uint8(1)); + } + + function test_canSetMetadata_notMetadataRole() public { + assertFalse(tokenContract.canSetMetadata()); + } + + modifier whenMetadataRoleRole() { + _; + } + + function test_canSetMetadata() public whenMetadataRoleRole { + vm.prank(deployer); + assertTrue(tokenContract.canSetMetadata()); + } + + function test_canFreezeMetadata_notMetadataRole() public { + assertFalse(tokenContract.canFreezeMetadata()); + } + + function test_canFreezeMetadata() public whenMetadataRoleRole { + vm.prank(deployer); + assertTrue(tokenContract.canFreezeMetadata()); + } + + function test_supportsInterface() public { + assertTrue(tokenContract.supportsInterface(type(IERC2981).interfaceId)); + assertTrue(tokenContract.supportsInterface(type(IERC165).interfaceId)); + assertTrue(tokenContract.supportsInterface(type(IERC165Upgradeable).interfaceId)); + assertTrue(tokenContract.supportsInterface(type(IAccessControlEnumerableUpgradeable).interfaceId)); + assertTrue(tokenContract.supportsInterface(type(IAccessControlUpgradeable).interfaceId)); + assertTrue(tokenContract.supportsInterface(type(IERC721EnumerableUpgradeable).interfaceId)); + assertTrue(tokenContract.supportsInterface(type(IERC721Upgradeable).interfaceId)); + + // false for other not supported interfaces + assertFalse(tokenContract.supportsInterface(type(IStaking721).interfaceId)); + } +} diff --git a/src/test/tokenerc721-BTT/other-functions/other.tree b/src/test/tokenerc721-BTT/other-functions/other.tree new file mode 100644 index 000000000..c6611fa64 --- /dev/null +++ b/src/test/tokenerc721-BTT/other-functions/other.tree @@ -0,0 +1,31 @@ +contractType() +├── it should return bytes32("TokenERC721") ✅ + +contractVersion() +├── it should return uint8(1) ✅ + +_beforeTokenTransfers( + address from, + address to, + uint256 startTokenId, + uint256 quantity +) +├── when transfers are restricted (i.e. address(0) doesn't have transfer role, or from-to addresses are not address(0) +│ └── when from and to don't have transfer role +│ └── it should revert ✅ + +_canSetMetadata() +├── when the caller doesn't have METADATA_ROLE +│ └── it should revert ✅ +└── when the caller has METADATA_ROLE + └── it should return true ✅ + +_canFreezeMetadata() +├── when the caller doesn't have METADATA_ROLE +│ └── it should revert ✅ +└── when the caller has METADATA_ROLE + └── it should return true ✅ + +supportsInterface(bytes4 interfaceId) +├── it should return true for supported interface ✅ +├── it should return false for not supported interface ✅ diff --git a/src/test/tokenerc721-BTT/owner/owner.t.sol b/src/test/tokenerc721-BTT/owner/owner.t.sol new file mode 100644 index 000000000..1f9f88ddc --- /dev/null +++ b/src/test/tokenerc721-BTT/owner/owner.t.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC721 is TokenERC721 {} + +contract TokenERC721Test_Owner is BaseTest { + address public implementation; + address public proxy; + + MyTokenERC721 internal tokenContract; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC721()); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC721(proxy); + } + + function test_owner() public { + assertEq(tokenContract.owner(), deployer); + } + + function test_owner_notDefaultAdmin() public { + vm.prank(deployer); + tokenContract.renounceRole(bytes32(0x00), deployer); + + assertEq(tokenContract.owner(), address(0)); + } +} diff --git a/src/test/tokenerc721-BTT/owner/owner.tree b/src/test/tokenerc721-BTT/owner/owner.tree new file mode 100644 index 000000000..576cfcb91 --- /dev/null +++ b/src/test/tokenerc721-BTT/owner/owner.tree @@ -0,0 +1,6 @@ +owner() +├── when private variable `_owner` DEFAULT_ADMIN_ROLE +│ └── it should return `_owner` ✅ +└── when private variable `_owner` doesn't have DEFAULT_ADMIN_ROLE + └── it should return address(0) ✅ + diff --git a/src/test/tokenerc721-BTT/set-contract-uri/setContractURI.t.sol b/src/test/tokenerc721-BTT/set-contract-uri/setContractURI.t.sol new file mode 100644 index 000000000..1dc0d8855 --- /dev/null +++ b/src/test/tokenerc721-BTT/set-contract-uri/setContractURI.t.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC721 is TokenERC721 {} + +contract TokenERC721Test_SetContractURI is BaseTest { + address public implementation; + address public proxy; + address internal caller; + string internal _contractURI; + + MyTokenERC721 internal tokenContract; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC721()); + + caller = getActor(1); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC721(proxy); + _contractURI = "ipfs://contracturi"; + } + + function test_setContractURI_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(caller), 20), + " is missing role ", + Strings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setContractURI(_contractURI); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setContractURI_empty() public whenCallerAuthorized { + vm.prank(address(caller)); + tokenContract.setContractURI(""); + + // get contract uri + assertEq(tokenContract.contractURI(), ""); + } + + function test_setContractURI_notEmpty() public whenCallerAuthorized { + vm.prank(address(caller)); + tokenContract.setContractURI(_contractURI); + + // get contract uri + assertEq(tokenContract.contractURI(), _contractURI); + } +} diff --git a/src/test/tokenerc721-BTT/set-contract-uri/setContractURI.tree b/src/test/tokenerc721-BTT/set-contract-uri/setContractURI.tree new file mode 100644 index 000000000..8fc480b19 --- /dev/null +++ b/src/test/tokenerc721-BTT/set-contract-uri/setContractURI.tree @@ -0,0 +1,8 @@ +setContractURI(string calldata _uri) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── when `uri` is empty + │ └── it should update contract URI to empty string ✅ + └── when `uri` is not empty + └── it should update contract URI to `_uri` ✅ \ No newline at end of file diff --git a/src/test/tokenerc721-BTT/set-default-royalty-info/setDefaultRoyaltyInfo.t.sol b/src/test/tokenerc721-BTT/set-default-royalty-info/setDefaultRoyaltyInfo.t.sol new file mode 100644 index 000000000..d63175b76 --- /dev/null +++ b/src/test/tokenerc721-BTT/set-default-royalty-info/setDefaultRoyaltyInfo.t.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC721 is TokenERC721 {} + +contract TokenERC721Test_SetDefaultRoyaltyInfo is BaseTest { + address public implementation; + address public proxy; + address internal caller; + address internal defaultRoyaltyRecipient; + uint256 internal defaultRoyaltyBps; + + MyTokenERC721 internal tokenContract; + + event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC721()); + + caller = getActor(1); + defaultRoyaltyRecipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC721(proxy); + } + + function test_setDefaultRoyaltyInfo_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(caller), 20), + " is missing role ", + Strings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setDefaultRoyaltyInfo_exceedMaxBps() public whenCallerAuthorized { + defaultRoyaltyBps = 10_001; + vm.prank(address(caller)); + vm.expectRevert("exceed royalty bps"); + tokenContract.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } + + modifier whenNotExceedMaxBps() { + defaultRoyaltyBps = 500; + _; + } + + function test_setDefaultRoyaltyInfo() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + tokenContract.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + + // get default royalty info + (address _recipient, uint16 _royaltyBps) = tokenContract.getDefaultRoyaltyInfo(); + assertEq(_recipient, defaultRoyaltyRecipient); + assertEq(_royaltyBps, uint16(defaultRoyaltyBps)); + + // get royalty info for token + uint256 tokenId = 0; + (_recipient, _royaltyBps) = tokenContract.getRoyaltyInfoForToken(tokenId); + assertEq(_recipient, defaultRoyaltyRecipient); + assertEq(_royaltyBps, uint16(defaultRoyaltyBps)); + + // royaltyInfo - ERC2981 + uint256 salePrice = 1000; + (address _royaltyRecipient, uint256 _royaltyAmount) = tokenContract.royaltyInfo(tokenId, salePrice); + assertEq(_royaltyRecipient, defaultRoyaltyRecipient); + assertEq(_royaltyAmount, (salePrice * defaultRoyaltyBps) / 10_000); + } + + function test_setDefaultRoyaltyInfo_event() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + vm.expectEmit(true, false, false, true); + emit DefaultRoyalty(defaultRoyaltyRecipient, defaultRoyaltyBps); + tokenContract.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } +} diff --git a/src/test/tokenerc721-BTT/set-default-royalty-info/setDefaultRoyaltyInfo.tree b/src/test/tokenerc721-BTT/set-default-royalty-info/setDefaultRoyaltyInfo.tree new file mode 100644 index 000000000..78a4312de --- /dev/null +++ b/src/test/tokenerc721-BTT/set-default-royalty-info/setDefaultRoyaltyInfo.tree @@ -0,0 +1,11 @@ +setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + ├── when royalty bps input is greater than MAX_BPS + │ └── it should revert ✅ + └── when royalty bps input is less than or equal to MAX_BPS + └── it should update default royalty recipient ✅ + └── it should update default royalty bps ✅ + └── it should calculate royalty amount for a token-id based on default royalty info ✅ + └── it should emit DefaultRoyalty event ✅ \ No newline at end of file diff --git a/src/test/tokenerc721-BTT/set-owner/setOwner.t.sol b/src/test/tokenerc721-BTT/set-owner/setOwner.t.sol new file mode 100644 index 000000000..1ea57ba2c --- /dev/null +++ b/src/test/tokenerc721-BTT/set-owner/setOwner.t.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC721 is TokenERC721 {} + +contract TokenERC721Test_SetOwner is BaseTest { + address public implementation; + address public proxy; + address internal caller; + address internal _newOwner; + + MyTokenERC721 internal tokenContract; + + event OwnerUpdated(address indexed prevOwner, address indexed newOwner); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC721()); + + caller = getActor(1); + _newOwner = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC721(proxy); + } + + function test_setOwner_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(caller), 20), + " is missing role ", + Strings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setOwner(_newOwner); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setOwner_newOwnerNotAdmin() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectRevert("new owner not module admin."); + tokenContract.setOwner(_newOwner); + } + + modifier whenNewOwnerIsAnAdmin() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), _newOwner); + _; + } + + function test_setOwner() public whenCallerAuthorized whenNewOwnerIsAnAdmin { + vm.prank(address(caller)); + tokenContract.setOwner(_newOwner); + + assertEq(tokenContract.owner(), _newOwner); + } + + function test_setOwner_event() public whenCallerAuthorized whenNewOwnerIsAnAdmin { + vm.prank(address(caller)); + vm.expectEmit(true, true, false, false); + emit OwnerUpdated(deployer, _newOwner); + tokenContract.setOwner(_newOwner); + } +} diff --git a/src/test/tokenerc721-BTT/set-owner/setOwner.tree b/src/test/tokenerc721-BTT/set-owner/setOwner.tree new file mode 100644 index 000000000..964e97cac --- /dev/null +++ b/src/test/tokenerc721-BTT/set-owner/setOwner.tree @@ -0,0 +1,9 @@ +setOwner(address _newOwner) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── when incoming `_owner` doesn't have DEFAULT_ADMIN_ROLE + │ └── it should revert ✅ + └── when incoming `_owner` has DEFAULT_ADMIN_ROLE + └── it should update owner ✅ + └── it should emit OwnerUpdated event ✅ \ No newline at end of file diff --git a/src/test/tokenerc721-BTT/set-platform-fee-info/setPlatformFeeInfo.t.sol b/src/test/tokenerc721-BTT/set-platform-fee-info/setPlatformFeeInfo.t.sol new file mode 100644 index 000000000..e2a9b0ab4 --- /dev/null +++ b/src/test/tokenerc721-BTT/set-platform-fee-info/setPlatformFeeInfo.t.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC721 is TokenERC721 {} + +contract TokenERC721Test_SetPlatformFeeInfo is BaseTest { + address public implementation; + address public proxy; + address internal caller; + address internal _platformFeeRecipient; + uint256 internal _platformFeeBps; + + MyTokenERC721 internal tokenContract; + + event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC721()); + + caller = getActor(1); + _platformFeeRecipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC721(proxy); + } + + function test_setPlatformFeeInfo_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(caller), 20), + " is missing role ", + Strings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setPlatformFeeInfo_exceedMaxBps() public whenCallerAuthorized { + _platformFeeBps = 10_001; + vm.prank(address(caller)); + vm.expectRevert("exceeds MAX_BPS"); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } + + modifier whenNotExceedMaxBps() { + _platformFeeBps = 500; + _; + } + + function test_setPlatformFeeInfo() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + + // get platform fee info + (address _recipient, uint16 _bps) = tokenContract.getPlatformFeeInfo(); + assertEq(_recipient, _platformFeeRecipient); + assertEq(_bps, uint16(_platformFeeBps)); + assertEq(tokenContract.platformFeeRecipient(), _platformFeeRecipient); + } + + function test_setPlatformFeeInfo_event() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + vm.expectEmit(true, false, false, true); + emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } +} diff --git a/src/test/tokenerc721-BTT/set-platform-fee-info/setPlatformFeeInfo.tree b/src/test/tokenerc721-BTT/set-platform-fee-info/setPlatformFeeInfo.tree new file mode 100644 index 000000000..dcef9965e --- /dev/null +++ b/src/test/tokenerc721-BTT/set-platform-fee-info/setPlatformFeeInfo.tree @@ -0,0 +1,10 @@ +setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + ├── when `_platformFeeBps` is greater than MAX_BPS + │ └── it should revert ✅ + └── when `_platformFeeBps` is less than or equal to MAX_BPS + └── it should update platform fee recipient ✅ + └── it should update platform fee bps ✅ + └── it should emit PlatformFeeInfoUpdated event ✅ \ No newline at end of file diff --git a/src/test/tokenerc721-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.t.sol b/src/test/tokenerc721-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.t.sol new file mode 100644 index 000000000..325b801ac --- /dev/null +++ b/src/test/tokenerc721-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.t.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC721 is TokenERC721 {} + +contract TokenERC721Test_SetPrimarySaleRecipient is BaseTest { + address public implementation; + address public proxy; + address internal caller; + address internal _primarySaleRecipient; + + MyTokenERC721 internal tokenContract; + + event PrimarySaleRecipientUpdated(address indexed recipient); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC721()); + + caller = getActor(1); + _primarySaleRecipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC721(proxy); + } + + function test_setPrimarySaleRecipient_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(caller), 20), + " is missing role ", + Strings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setPrimarySaleRecipient() public whenCallerAuthorized { + vm.prank(address(caller)); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + + // get primary sale recipient info + assertEq(tokenContract.primarySaleRecipient(), _primarySaleRecipient); + } + + function test_setPrimarySaleRecipient_event() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectEmit(true, false, false, false); + emit PrimarySaleRecipientUpdated(_primarySaleRecipient); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + } +} diff --git a/src/test/tokenerc721-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.tree b/src/test/tokenerc721-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.tree new file mode 100644 index 000000000..230035a07 --- /dev/null +++ b/src/test/tokenerc721-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.tree @@ -0,0 +1,6 @@ +setPrimarySaleRecipient(address _saleRecipient) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── it should update primary sale recipient ✅ + └── it should emit PrimarySaleRecipientUpdated event ✅ \ No newline at end of file diff --git a/src/test/tokenerc721-BTT/set-royalty-info-for-token/setRoyaltyInfoForToken.t.sol b/src/test/tokenerc721-BTT/set-royalty-info-for-token/setRoyaltyInfoForToken.t.sol new file mode 100644 index 000000000..f5c5e3b83 --- /dev/null +++ b/src/test/tokenerc721-BTT/set-royalty-info-for-token/setRoyaltyInfoForToken.t.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC721 is TokenERC721 {} + +contract TokenERC721Test_SetRoyaltyInfoForToken is BaseTest { + address public implementation; + address public proxy; + address internal caller; + address internal defaultRoyaltyRecipient; + uint256 internal defaultRoyaltyBps; + + MyTokenERC721 internal tokenContract; + + address internal royaltyRecipientForToken; + uint256 internal royaltyBpsForToken; + uint256 internal tokenId; + + event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, uint256 royaltyBps); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC721()); + + caller = getActor(1); + defaultRoyaltyRecipient = getActor(2); + royaltyRecipientForToken = getActor(3); + defaultRoyaltyBps = 500; + tokenId = 1; + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC721(proxy); + + vm.prank(deployer); + tokenContract.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } + + function test_setRoyaltyInfoForToken_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(caller), 20), + " is missing role ", + Strings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setRoyaltyInfoForToken_exceedMaxBps() public whenCallerAuthorized { + royaltyBpsForToken = 10_001; + vm.prank(address(caller)); + vm.expectRevert("exceed royalty bps"); + tokenContract.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + } + + modifier whenNotExceedMaxBps() { + royaltyBpsForToken = 1000; + _; + } + + function test_setRoyaltyInfoForToken() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + tokenContract.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + + // get default royalty info + (address _defaultRecipient, uint16 _defaultRoyaltyBps) = tokenContract.getDefaultRoyaltyInfo(); + assertEq(_defaultRecipient, defaultRoyaltyRecipient); + assertEq(_defaultRoyaltyBps, uint16(defaultRoyaltyBps)); + + // get royalty info for token + (address _royaltyRecipientForToken, uint16 _royaltyBpsForToken) = tokenContract.getRoyaltyInfoForToken(tokenId); + assertEq(_royaltyRecipientForToken, royaltyRecipientForToken); + assertEq(_royaltyBpsForToken, uint16(royaltyBpsForToken)); + + // royaltyInfo - ERC2981: calculate for default + uint256 salePrice = 1000; + (address _royaltyRecipient, uint256 _royaltyAmount) = tokenContract.royaltyInfo(0, salePrice); + assertEq(_royaltyRecipient, defaultRoyaltyRecipient); + assertEq(_royaltyAmount, (salePrice * defaultRoyaltyBps) / 10_000); + + // royaltyInfo - ERC2981: calculate for specific tokenId we set the royalty info for + (_royaltyRecipient, _royaltyAmount) = tokenContract.royaltyInfo(tokenId, salePrice); + assertEq(_royaltyRecipient, royaltyRecipientForToken); + assertEq(_royaltyAmount, (salePrice * royaltyBpsForToken) / 10_000); + } + + function test_setRoyaltyInfoForToken_event() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + vm.expectEmit(true, true, false, true); + emit RoyaltyForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + tokenContract.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + } +} diff --git a/src/test/tokenerc721-BTT/set-royalty-info-for-token/setRoyaltyInfoForToken.tree b/src/test/tokenerc721-BTT/set-royalty-info-for-token/setRoyaltyInfoForToken.tree new file mode 100644 index 000000000..e28295634 --- /dev/null +++ b/src/test/tokenerc721-BTT/set-royalty-info-for-token/setRoyaltyInfoForToken.tree @@ -0,0 +1,15 @@ +function setRoyaltyInfoForToken( + uint256 _tokenId, + address _recipient, + uint256 _bps +) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + ├── when royalty bps input is greater than MAX_BPS + │ └── it should revert ✅ + └── when royalty bps input is less than or equal to MAX_BPS + └── it should update default royalty recipient ✅ + └── it should update default royalty bps ✅ + └── it should calculate royalty amount for a token-id based on default royalty info ✅ + └── it should emit DefaultRoyalty event ✅ \ No newline at end of file diff --git a/src/test/tokenerc721-BTT/token-uri/tokenURI.t.sol b/src/test/tokenerc721-BTT/token-uri/tokenURI.t.sol new file mode 100644 index 000000000..260048e0c --- /dev/null +++ b/src/test/tokenerc721-BTT/token-uri/tokenURI.t.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC721 is TokenERC721 {} + +contract TokenERC721Test_TokenURI is BaseTest { + address public implementation; + address public proxy; + + MyTokenERC721 internal tokenContract; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC721()); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC721(proxy); + } + + function test_tokenURI() public { + uint256 _tokenId = 1; + string memory _uri = "ipfs://uri/1"; + + vm.prank(deployer); + tokenContract.setTokenURI(_tokenId, _uri); + + assertEq(tokenContract.tokenURI(_tokenId), _uri); + } +} diff --git a/src/test/tokenerc721-BTT/token-uri/tokenURI.tree b/src/test/tokenerc721-BTT/token-uri/tokenURI.tree new file mode 100644 index 000000000..c97d2c9d1 --- /dev/null +++ b/src/test/tokenerc721-BTT/token-uri/tokenURI.tree @@ -0,0 +1,3 @@ +tokenURI(uint256 _tokenId) +├── it should return tokenURI associated with the given `_tokenId` ✅ + diff --git a/src/test/tokenerc721-BTT/verify/verify.t.sol b/src/test/tokenerc721-BTT/verify/verify.t.sol new file mode 100644 index 000000000..2d219b9f0 --- /dev/null +++ b/src/test/tokenerc721-BTT/verify/verify.t.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC721 is TokenERC721 { + function setMintedURI(MintRequest calldata _req, bytes calldata _signature) external { + verifyRequest(_req, _signature); + } +} + +contract TokenERC721Test_Verify is BaseTest { + address public implementation; + address public proxy; + + MyTokenERC721 internal tokenContract; + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + TokenERC721.MintRequest _mintrequest; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC721()); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC721(proxy); + + typehashMintRequest = keccak256( + "MintRequest(address to,address royaltyRecipient,uint256 royaltyBps,address primarySaleRecipient,string uri,uint256 price,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + nameHash = keccak256(bytes("TokenERC721")); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256( + abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(tokenContract)) + ); + + // construct default mintrequest + _mintrequest.to = address(0x1234); + _mintrequest.royaltyRecipient = royaltyRecipient; + _mintrequest.royaltyBps = royaltyBps; + _mintrequest.primarySaleRecipient = saleRecipient; + _mintrequest.uri = "ipfs://"; + _mintrequest.price = 0; + _mintrequest.currency = address(0); + _mintrequest.validityStartTimestamp = 0; + _mintrequest.validityEndTimestamp = 2000; + _mintrequest.uid = bytes32(0); + } + + function signMintRequest( + TokenERC721.MintRequest memory _request, + uint256 _privateKey + ) internal view returns (bytes memory) { + bytes memory encodedRequest = abi.encode( + typehashMintRequest, + _request.to, + _request.royaltyRecipient, + _request.royaltyBps, + _request.primarySaleRecipient, + keccak256(bytes(_request.uri)), + _request.price, + _request.currency, + _request.validityStartTimestamp, + _request.validityEndTimestamp, + _request.uid + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, typedDataHash); + bytes memory sig = abi.encodePacked(r, s, v); + + return sig; + } + + function test_verify_notMinterRole() public { + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + (bool _isValid, address _recoveredSigner) = tokenContract.verify(_mintrequest, _signature); + + assertFalse(_isValid); + assertEq(_recoveredSigner, signer); + } + + modifier whenMinterRole() { + vm.prank(deployer); + tokenContract.grantRole(keccak256("MINTER_ROLE"), signer); + _; + } + + function test_verify_invalidUID() public whenMinterRole { + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // set state with this mintrequest and signature, marking the UID as used + tokenContract.setMintedURI(_mintrequest, _signature); + + // pass the same UID mintrequest again + (bool _isValid, address _recoveredSigner) = tokenContract.verify(_mintrequest, _signature); + + assertFalse(_isValid); + assertEq(_recoveredSigner, signer); + } + + modifier whenUidNotUsed() { + _; + } + + function test_verify() public whenMinterRole whenUidNotUsed { + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + (bool _isValid, address _recoveredSigner) = tokenContract.verify(_mintrequest, _signature); + + assertTrue(_isValid); + assertEq(_recoveredSigner, signer); + } +} diff --git a/src/test/tokenerc721-BTT/verify/verify.tree b/src/test/tokenerc721-BTT/verify/verify.tree new file mode 100644 index 000000000..c160faa0f --- /dev/null +++ b/src/test/tokenerc721-BTT/verify/verify.tree @@ -0,0 +1,12 @@ +verify(MintRequest calldata _req, bytes calldata _signature) +├── when signer doesn't have MINTER_ROLE +│ └── it should return false ✅ +│ └── it should return recovered signer equal to the actual signer of the request ✅ +└── when signer has MINTER_ROLE + └── when `_req.uid` has already been used + │ └── it should return false ✅ + │ └── it should return recovered signer equal to the actual signer of the request ✅ + └── when `_req.uid` has not been used + └── it should return true ✅ + └── it should return recovered signer equal to the actual signer of the request ✅ + diff --git a/src/test/utils/BaseTest.sol b/src/test/utils/BaseTest.sol index ec7333c66..a26ff27ad 100644 --- a/src/test/utils/BaseTest.sol +++ b/src/test/utils/BaseTest.sol @@ -4,29 +4,51 @@ pragma solidity ^0.8.11; import "@std/Test.sol"; import "@ds-test/test.sol"; // import "./Console.sol"; -import "./Wallet.sol"; -import "../mocks/WETH9.sol"; -import "../mocks/MockERC20.sol"; -import "../mocks/MockERC721.sol"; -import "../mocks/MockERC1155.sol"; -import "contracts/Forwarder.sol"; -import "contracts/TWFee.sol"; -import "contracts/TWRegistry.sol"; -import "contracts/TWFactory.sol"; -import { Multiwrap } from "contracts/multiwrap/Multiwrap.sol"; -import { Pack } from "contracts/pack/Pack.sol"; -import { Split } from "contracts/Split.sol"; -import { DropERC20 } from "contracts/drop/DropERC20.sol"; -import { DropERC721 } from "contracts/drop/DropERC721.sol"; -import { DropERC1155 } from "contracts/drop/DropERC1155.sol"; -import { TokenERC20 } from "contracts/token/TokenERC20.sol"; -import { TokenERC721 } from "contracts/token/TokenERC721.sol"; -import { TokenERC1155 } from "contracts/token/TokenERC1155.sol"; -import { Marketplace } from "contracts/marketplace/Marketplace.sol"; -import { VoteERC20 } from "contracts/vote/VoteERC20.sol"; -import { SignatureDrop } from "contracts/signature-drop/SignatureDrop.sol"; -import { ContractPublisher } from "contracts/ContractPublisher.sol"; -import "contracts/mock/Mock.sol"; +import { Wallet } from "./Wallet.sol"; +import "./ChainlinkVRF.sol"; +import { WETH9 } from "../mocks/WETH9.sol"; +import { MockERC20, ERC20, IERC20 } from "../mocks/MockERC20.sol"; +import { MockERC721, IERC721 } from "../mocks/MockERC721.sol"; +import { MockERC1155, IERC1155 } from "../mocks/MockERC1155.sol"; +import { MockERC721NonBurnable } from "../mocks/MockERC721NonBurnable.sol"; +import { MockERC1155NonBurnable } from "../mocks/MockERC1155NonBurnable.sol"; +import { Forwarder } from "contracts/infra/forwarder/Forwarder.sol"; +import { ForwarderEOAOnly } from "contracts/infra/forwarder/ForwarderEOAOnly.sol"; +import { TWRegistry } from "contracts/infra/TWRegistry.sol"; +import { TWFactory } from "contracts/infra/TWFactory.sol"; +import { Multiwrap } from "contracts/prebuilts/multiwrap/Multiwrap.sol"; +import { Pack } from "contracts/prebuilts/pack/Pack.sol"; +import { PackVRFDirect } from "contracts/prebuilts/pack/PackVRFDirect.sol"; +import { Split } from "contracts/prebuilts/split/Split.sol"; +import { DropERC20 } from "contracts/prebuilts/drop/DropERC20.sol"; +import { DropERC721 } from "contracts/prebuilts/drop/DropERC721.sol"; +import { DropERC1155 } from "contracts/prebuilts/drop/DropERC1155.sol"; +import { TokenERC20 } from "contracts/prebuilts/token/TokenERC20.sol"; +import { TokenERC721 } from "contracts/prebuilts/token/TokenERC721.sol"; +import { TokenERC1155 } from "contracts/prebuilts/token/TokenERC1155.sol"; +import { Marketplace } from "contracts/prebuilts/marketplace-legacy/Marketplace.sol"; +import { VoteERC20 } from "contracts/prebuilts/vote/VoteERC20.sol"; +import { SignatureDrop } from "contracts/prebuilts/signature-drop/SignatureDrop.sol"; +import { ContractPublisher } from "contracts/infra/ContractPublisher.sol"; +import { IContractPublisher } from "contracts/infra/interface/IContractPublisher.sol"; +import { AirdropERC721 } from "contracts/prebuilts/unaudited/airdrop/AirdropERC721.sol"; +import { AirdropERC721Claimable } from "contracts/prebuilts/unaudited/airdrop/AirdropERC721Claimable.sol"; +import { AirdropERC20 } from "contracts/prebuilts/unaudited/airdrop/AirdropERC20.sol"; +import { AirdropERC20Claimable } from "contracts/prebuilts/unaudited/airdrop/AirdropERC20Claimable.sol"; +import { AirdropERC1155 } from "contracts/prebuilts/unaudited/airdrop/AirdropERC1155.sol"; +import { AirdropERC1155Claimable } from "contracts/prebuilts/unaudited/airdrop/AirdropERC1155Claimable.sol"; +import { NFTStake } from "contracts/prebuilts/staking/NFTStake.sol"; +import { EditionStake } from "contracts/prebuilts/staking/EditionStake.sol"; +import { TokenStake } from "contracts/prebuilts/staking/TokenStake.sol"; +import { Mock, MockContract } from "../mocks/Mock.sol"; +import { MockContractPublisher } from "../mocks/MockContractPublisher.sol"; +import { Permissions } from "contracts/extension/Permissions.sol"; +import { PermissionsEnumerable } from "contracts/extension/PermissionsEnumerable.sol"; +import { ERC1155Holder, IERC1155Receiver } from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; +import { ERC721Holder, IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; +import { Clones } from "@openzeppelin/contracts/proxy/Clones.sol"; +import { Strings } from "contracts/lib/Strings.sol"; +import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; abstract contract BaseTest is DSTest, Test { string public constant NAME = "NAME"; @@ -35,15 +57,21 @@ abstract contract BaseTest is DSTest, Test { address public constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; MockERC20 public erc20; + MockERC20 public erc20Aux; MockERC721 public erc721; MockERC1155 public erc1155; + MockERC721NonBurnable public erc721NonBurnable; + MockERC1155NonBurnable public erc1155NonBurnable; WETH9 public weth; address public forwarder; + address public eoaForwarder; address public registry; address public factory; address public fee; address public contractPublisher; + address public linkToken; + address public vrfV2Wrapper; address public factoryAdmin = address(0x10000); address public deployer = address(0x20000); @@ -57,6 +85,20 @@ abstract contract BaseTest is DSTest, Test { uint256 public privateKey = 1234; address public signer; + // airdrop-claimable inputs + uint256[] internal _airdropTokenIdsERC721; + bytes32 internal _airdropMerkleRootERC721; + + uint256[] internal _airdropTokenIdsERC1155; + uint256[] internal _airdropWalletClaimCountERC1155; + uint256[] internal _airdropAmountsERC1155; + bytes32[] internal _airdropMerkleRootERC1155; + + bytes32 internal _airdropMerkleRootERC20; + + Wallet internal airdropTokenOwner; + // airdrop-claimable inputs -- over + mapping(bytes32 => address) public contracts; function setUp() public virtual { @@ -66,19 +108,25 @@ abstract contract BaseTest is DSTest, Test { signer = vm.addr(privateKey); erc20 = new MockERC20(); + erc20Aux = new MockERC20(); erc721 = new MockERC721(); erc1155 = new MockERC1155(); + erc721NonBurnable = new MockERC721NonBurnable(); + erc1155NonBurnable = new MockERC1155NonBurnable(); weth = new WETH9(); forwarder = address(new Forwarder()); - registry = address(new TWRegistry(forwarder)); - factory = address(new TWFactory(forwarder, registry)); - contractPublisher = address(new ContractPublisher(forwarder)); + eoaForwarder = address(new ForwarderEOAOnly()); + registry = address(new TWRegistry(forwarders())); + factory = address(new TWFactory(forwarders(), registry)); + contractPublisher = address(new ContractPublisher(factoryAdmin, forwarders(), new MockContractPublisher())); + linkToken = address(new Link()); + vrfV2Wrapper = address(new VRFV2Wrapper()); TWRegistry(registry).grantRole(TWRegistry(registry).OPERATOR_ROLE(), factory); TWRegistry(registry).grantRole(TWRegistry(registry).OPERATOR_ROLE(), contractPublisher); - fee = address(new TWFee(forwarder, factory)); - TWFactory(factory).addImplementation(address(new TokenERC20(fee))); - TWFactory(factory).addImplementation(address(new TokenERC721(fee))); - TWFactory(factory).addImplementation(address(new TokenERC1155(fee))); + + TWFactory(factory).addImplementation(address(new TokenERC20())); + TWFactory(factory).addImplementation(address(new TokenERC721())); + TWFactory(factory).addImplementation(address(new TokenERC1155())); TWFactory(factory).addImplementation(address(new DropERC20())); TWFactory(factory).addImplementation(address(new MockContract(bytes32("DropERC721"), 1))); TWFactory(factory).addImplementation(address(new DropERC721())); @@ -87,29 +135,38 @@ abstract contract BaseTest is DSTest, Test { TWFactory(factory).addImplementation(address(new MockContract(bytes32("SignatureDrop"), 1))); TWFactory(factory).addImplementation(address(new SignatureDrop())); TWFactory(factory).addImplementation(address(new MockContract(bytes32("Marketplace"), 1))); - TWFactory(factory).addImplementation(address(new Marketplace(address(weth), fee))); - TWFactory(factory).addImplementation(address(new Split(fee))); + TWFactory(factory).addImplementation(address(new Marketplace(address(weth)))); + TWFactory(factory).addImplementation(address(new Split())); TWFactory(factory).addImplementation(address(new Multiwrap(address(weth)))); TWFactory(factory).addImplementation(address(new MockContract(bytes32("Pack"), 1))); + TWFactory(factory).addImplementation(address(new MockContract(bytes32("AirdropERC721"), 1))); + TWFactory(factory).addImplementation(address(new AirdropERC721())); + TWFactory(factory).addImplementation(address(new MockContract(bytes32("AirdropERC20"), 1))); + TWFactory(factory).addImplementation(address(new AirdropERC20())); + TWFactory(factory).addImplementation(address(new MockContract(bytes32("AirdropERC1155"), 1))); + TWFactory(factory).addImplementation(address(new AirdropERC1155())); + TWFactory(factory).addImplementation( + address(new PackVRFDirect(address(weth), eoaForwarder, linkToken, vrfV2Wrapper)) + ); TWFactory(factory).addImplementation(address(new Pack(address(weth)))); TWFactory(factory).addImplementation(address(new VoteERC20())); + TWFactory(factory).addImplementation(address(new MockContract(bytes32("NFTStake"), 1))); + TWFactory(factory).addImplementation(address(new NFTStake(address(weth)))); + TWFactory(factory).addImplementation(address(new MockContract(bytes32("EditionStake"), 1))); + TWFactory(factory).addImplementation(address(new EditionStake(address(weth)))); + TWFactory(factory).addImplementation(address(new MockContract(bytes32("TokenStake"), 1))); + TWFactory(factory).addImplementation(address(new TokenStake(address(weth)))); vm.stopPrank(); + // setup airdrop logic + setupAirdropClaimable(); + /// deploy proxy for tests deployContractProxy( "TokenERC20", abi.encodeCall( TokenERC20.initialize, - ( - deployer, - NAME, - SYMBOL, - CONTRACT_URI, - forwarders(), - saleRecipient, - platformFeeRecipient, - platformFeeBps - ) + (signer, NAME, SYMBOL, CONTRACT_URI, forwarders(), saleRecipient, platformFeeRecipient, platformFeeBps) ) ); deployContractProxy( @@ -117,7 +174,7 @@ abstract contract BaseTest is DSTest, Test { abi.encodeCall( TokenERC721.initialize, ( - deployer, + signer, NAME, SYMBOL, CONTRACT_URI, @@ -135,7 +192,7 @@ abstract contract BaseTest is DSTest, Test { abi.encodeCall( TokenERC1155.initialize, ( - deployer, + signer, NAME, SYMBOL, CONTRACT_URI, @@ -239,12 +296,54 @@ abstract contract BaseTest is DSTest, Test { (deployer, NAME, SYMBOL, CONTRACT_URI, forwarders(), royaltyRecipient, royaltyBps) ) ); + + deployContractProxy( + "PackVRFDirect", + abi.encodeCall( + PackVRFDirect.initialize, + (deployer, NAME, SYMBOL, CONTRACT_URI, forwarders(), royaltyRecipient, royaltyBps) + ) + ); + + deployContractProxy( + "AirdropERC721", + abi.encodeCall(AirdropERC721.initialize, (deployer, CONTRACT_URI, forwarders())) + ); + deployContractProxy( + "AirdropERC20", + abi.encodeCall(AirdropERC20.initialize, (deployer, CONTRACT_URI, forwarders())) + ); + deployContractProxy( + "AirdropERC1155", + abi.encodeCall(AirdropERC1155.initialize, (deployer, CONTRACT_URI, forwarders())) + ); + deployContractProxy( + "NFTStake", + abi.encodeCall( + NFTStake.initialize, + (deployer, CONTRACT_URI, forwarders(), address(erc20), address(erc721), 60, 1) + ) + ); + deployContractProxy( + "EditionStake", + abi.encodeCall( + EditionStake.initialize, + (deployer, CONTRACT_URI, forwarders(), address(erc20), address(erc1155), 60, 1) + ) + ); + deployContractProxy( + "TokenStake", + abi.encodeCall( + TokenStake.initialize, + (deployer, CONTRACT_URI, forwarders(), address(erc20), address(erc20Aux), 60, 3, 50) + ) + ); } - function deployContractProxy(string memory _contractType, bytes memory _initializer) - public - returns (address proxyAddress) - { + function deployContractProxy( + string memory _contractType, + bytes memory _initializer + ) public returns (address proxyAddress) { vm.startPrank(deployer); proxyAddress = TWFactory(factory).deployProxy(bytes32(bytes(_contractType)), _initializer); contracts[bytes32(bytes(_contractType))] = proxyAddress; @@ -263,22 +362,14 @@ abstract contract BaseTest is DSTest, Test { wallet = new Wallet(); } - function assertIsOwnerERC721( - address _token, - address _owner, - uint256[] memory _tokenIds - ) internal { + function assertIsOwnerERC721(address _token, address _owner, uint256[] memory _tokenIds) internal { for (uint256 i = 0; i < _tokenIds.length; i += 1) { bool isOwnerOfToken = MockERC721(_token).ownerOf(_tokenIds[i]) == _owner; assertTrue(isOwnerOfToken); } } - function assertIsNotOwnerERC721( - address _token, - address _owner, - uint256[] memory _tokenIds - ) internal { + function assertIsNotOwnerERC721(address _token, address _owner, uint256[] memory _tokenIds) internal { for (uint256 i = 0; i < _tokenIds.length; i += 1) { bool isOwnerOfToken = MockERC721(_token).ownerOf(_tokenIds[i]) == _owner; assertTrue(!isOwnerOfToken); @@ -311,19 +402,11 @@ abstract contract BaseTest is DSTest, Test { } } - function assertBalERC20Eq( - address _token, - address _owner, - uint256 _amount - ) internal { + function assertBalERC20Eq(address _token, address _owner, uint256 _amount) internal { assertEq(MockERC20(_token).balanceOf(_owner), _amount); } - function assertBalERC20Gte( - address _token, - address _owner, - uint256 _amount - ) internal { + function assertBalERC20Gte(address _token, address _owner, uint256 _amount) internal { assertTrue(MockERC20(_token).balanceOf(_owner) >= _amount); } @@ -332,4 +415,33 @@ abstract contract BaseTest is DSTest, Test { _forwarders[0] = forwarder; return _forwarders; } + + function setupAirdropClaimable() public { + string[] memory inputs = new string[](3); + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRootAirdrop.ts"; + inputs[2] = Strings.toString(uint256(5)); + + bytes memory result = vm.ffi(inputs); + bytes32 root = abi.decode(result, (bytes32)); + + airdropTokenOwner = getWallet(); + + // ERC721 + for (uint256 i = 0; i < 1000; i++) { + _airdropTokenIdsERC721.push(i); + } + _airdropMerkleRootERC721 = root; + + // ERC1155 + for (uint256 i = 0; i < 5; i++) { + _airdropTokenIdsERC1155.push(i); + _airdropAmountsERC1155.push(100); + _airdropWalletClaimCountERC1155.push(1); + _airdropMerkleRootERC1155.push(root); + } + + // ERC20 + _airdropMerkleRootERC20 = root; + } } diff --git a/src/test/utils/ChainlinkVRF.sol b/src/test/utils/ChainlinkVRF.sol new file mode 100644 index 000000000..cd5eaf76a --- /dev/null +++ b/src/test/utils/ChainlinkVRF.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.0; + +contract Link { + function transferAndCall(address, uint256, bytes calldata) external returns (bool) {} +} + +contract VRFV2Wrapper { + uint256 private nextId = 5; + + function lastRequestId() external view returns (uint256 id) { + id = nextId; + } + + function calculateRequestPrice(uint32 _callbackGasLimit) external pure returns (uint256) { + return _callbackGasLimit; + } +} diff --git a/src/test/utils/Console.sol b/src/test/utils/Console.sol index 753b533e0..c8e655ca6 100644 --- a/src/test/utils/Console.sol +++ b/src/test/utils/Console.sol @@ -249,2819 +249,1283 @@ library console { _sendLogPayload(abi.encodeWithSignature("log(address,address)", p0, p1)); } - function log( - uint256 p0, - uint256 p1, - uint256 p2 - ) internal view { + function log(uint256 p0, uint256 p1, uint256 p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,uint,uint)", p0, p1, p2)); } - function log( - uint256 p0, - uint256 p1, - string memory p2 - ) internal view { + function log(uint256 p0, uint256 p1, string memory p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,uint,string)", p0, p1, p2)); } - function log( - uint256 p0, - uint256 p1, - bool p2 - ) internal view { + function log(uint256 p0, uint256 p1, bool p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,uint,bool)", p0, p1, p2)); } - function log( - uint256 p0, - uint256 p1, - address p2 - ) internal view { + function log(uint256 p0, uint256 p1, address p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,uint,address)", p0, p1, p2)); } - function log( - uint256 p0, - string memory p1, - uint256 p2 - ) internal view { + function log(uint256 p0, string memory p1, uint256 p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,string,uint)", p0, p1, p2)); } - function log( - uint256 p0, - string memory p1, - string memory p2 - ) internal view { + function log(uint256 p0, string memory p1, string memory p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,string,string)", p0, p1, p2)); } - function log( - uint256 p0, - string memory p1, - bool p2 - ) internal view { + function log(uint256 p0, string memory p1, bool p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,string,bool)", p0, p1, p2)); } - function log( - uint256 p0, - string memory p1, - address p2 - ) internal view { + function log(uint256 p0, string memory p1, address p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,string,address)", p0, p1, p2)); } - function log( - uint256 p0, - bool p1, - uint256 p2 - ) internal view { + function log(uint256 p0, bool p1, uint256 p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,bool,uint)", p0, p1, p2)); } - function log( - uint256 p0, - bool p1, - string memory p2 - ) internal view { + function log(uint256 p0, bool p1, string memory p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,bool,string)", p0, p1, p2)); } - function log( - uint256 p0, - bool p1, - bool p2 - ) internal view { + function log(uint256 p0, bool p1, bool p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,bool,bool)", p0, p1, p2)); } - function log( - uint256 p0, - bool p1, - address p2 - ) internal view { + function log(uint256 p0, bool p1, address p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,bool,address)", p0, p1, p2)); } - function log( - uint256 p0, - address p1, - uint256 p2 - ) internal view { + function log(uint256 p0, address p1, uint256 p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,address,uint)", p0, p1, p2)); } - function log( - uint256 p0, - address p1, - string memory p2 - ) internal view { + function log(uint256 p0, address p1, string memory p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,address,string)", p0, p1, p2)); } - function log( - uint256 p0, - address p1, - bool p2 - ) internal view { + function log(uint256 p0, address p1, bool p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,address,bool)", p0, p1, p2)); } - function log( - uint256 p0, - address p1, - address p2 - ) internal view { + function log(uint256 p0, address p1, address p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,address,address)", p0, p1, p2)); } - function log( - string memory p0, - uint256 p1, - uint256 p2 - ) internal view { + function log(string memory p0, uint256 p1, uint256 p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,uint,uint)", p0, p1, p2)); } - function log( - string memory p0, - uint256 p1, - string memory p2 - ) internal view { + function log(string memory p0, uint256 p1, string memory p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,uint,string)", p0, p1, p2)); } - function log( - string memory p0, - uint256 p1, - bool p2 - ) internal view { + function log(string memory p0, uint256 p1, bool p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,uint,bool)", p0, p1, p2)); } - function log( - string memory p0, - uint256 p1, - address p2 - ) internal view { + function log(string memory p0, uint256 p1, address p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,uint,address)", p0, p1, p2)); } - function log( - string memory p0, - string memory p1, - uint256 p2 - ) internal view { + function log(string memory p0, string memory p1, uint256 p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,string,uint)", p0, p1, p2)); } - function log( - string memory p0, - string memory p1, - string memory p2 - ) internal view { + function log(string memory p0, string memory p1, string memory p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,string,string)", p0, p1, p2)); } - function log( - string memory p0, - string memory p1, - bool p2 - ) internal view { + function log(string memory p0, string memory p1, bool p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,string,bool)", p0, p1, p2)); } - function log( - string memory p0, - string memory p1, - address p2 - ) internal view { + function log(string memory p0, string memory p1, address p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,string,address)", p0, p1, p2)); } - function log( - string memory p0, - bool p1, - uint256 p2 - ) internal view { + function log(string memory p0, bool p1, uint256 p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,bool,uint)", p0, p1, p2)); } - function log( - string memory p0, - bool p1, - string memory p2 - ) internal view { + function log(string memory p0, bool p1, string memory p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,bool,string)", p0, p1, p2)); } - function log( - string memory p0, - bool p1, - bool p2 - ) internal view { + function log(string memory p0, bool p1, bool p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,bool,bool)", p0, p1, p2)); } - function log( - string memory p0, - bool p1, - address p2 - ) internal view { + function log(string memory p0, bool p1, address p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,bool,address)", p0, p1, p2)); } - function log( - string memory p0, - address p1, - uint256 p2 - ) internal view { + function log(string memory p0, address p1, uint256 p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,address,uint)", p0, p1, p2)); } - function log( - string memory p0, - address p1, - string memory p2 - ) internal view { + function log(string memory p0, address p1, string memory p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,address,string)", p0, p1, p2)); } - function log( - string memory p0, - address p1, - bool p2 - ) internal view { + function log(string memory p0, address p1, bool p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,address,bool)", p0, p1, p2)); } - function log( - string memory p0, - address p1, - address p2 - ) internal view { + function log(string memory p0, address p1, address p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,address,address)", p0, p1, p2)); } - function log( - bool p0, - uint256 p1, - uint256 p2 - ) internal view { + function log(bool p0, uint256 p1, uint256 p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,uint,uint)", p0, p1, p2)); } - function log( - bool p0, - uint256 p1, - string memory p2 - ) internal view { + function log(bool p0, uint256 p1, string memory p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,uint,string)", p0, p1, p2)); } - function log( - bool p0, - uint256 p1, - bool p2 - ) internal view { + function log(bool p0, uint256 p1, bool p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,uint,bool)", p0, p1, p2)); } - function log( - bool p0, - uint256 p1, - address p2 - ) internal view { + function log(bool p0, uint256 p1, address p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,uint,address)", p0, p1, p2)); } - function log( - bool p0, - string memory p1, - uint256 p2 - ) internal view { + function log(bool p0, string memory p1, uint256 p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,string,uint)", p0, p1, p2)); } - function log( - bool p0, - string memory p1, - string memory p2 - ) internal view { + function log(bool p0, string memory p1, string memory p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,string,string)", p0, p1, p2)); } - function log( - bool p0, - string memory p1, - bool p2 - ) internal view { + function log(bool p0, string memory p1, bool p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,string,bool)", p0, p1, p2)); } - function log( - bool p0, - string memory p1, - address p2 - ) internal view { + function log(bool p0, string memory p1, address p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,string,address)", p0, p1, p2)); } - function log( - bool p0, - bool p1, - uint256 p2 - ) internal view { + function log(bool p0, bool p1, uint256 p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,bool,uint)", p0, p1, p2)); } - function log( - bool p0, - bool p1, - string memory p2 - ) internal view { + function log(bool p0, bool p1, string memory p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,bool,string)", p0, p1, p2)); } - function log( - bool p0, - bool p1, - bool p2 - ) internal view { + function log(bool p0, bool p1, bool p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,bool,bool)", p0, p1, p2)); } - function log( - bool p0, - bool p1, - address p2 - ) internal view { + function log(bool p0, bool p1, address p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,bool,address)", p0, p1, p2)); } - function log( - bool p0, - address p1, - uint256 p2 - ) internal view { + function log(bool p0, address p1, uint256 p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,address,uint)", p0, p1, p2)); } - function log( - bool p0, - address p1, - string memory p2 - ) internal view { + function log(bool p0, address p1, string memory p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,address,string)", p0, p1, p2)); } - function log( - bool p0, - address p1, - bool p2 - ) internal view { + function log(bool p0, address p1, bool p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,address,bool)", p0, p1, p2)); } - function log( - bool p0, - address p1, - address p2 - ) internal view { + function log(bool p0, address p1, address p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,address,address)", p0, p1, p2)); } - function log( - address p0, - uint256 p1, - uint256 p2 - ) internal view { + function log(address p0, uint256 p1, uint256 p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,uint,uint)", p0, p1, p2)); } - function log( - address p0, - uint256 p1, - string memory p2 - ) internal view { + function log(address p0, uint256 p1, string memory p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,uint,string)", p0, p1, p2)); } - function log( - address p0, - uint256 p1, - bool p2 - ) internal view { + function log(address p0, uint256 p1, bool p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,uint,bool)", p0, p1, p2)); } - function log( - address p0, - uint256 p1, - address p2 - ) internal view { + function log(address p0, uint256 p1, address p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,uint,address)", p0, p1, p2)); } - function log( - address p0, - string memory p1, - uint256 p2 - ) internal view { + function log(address p0, string memory p1, uint256 p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,string,uint)", p0, p1, p2)); } - function log( - address p0, - string memory p1, - string memory p2 - ) internal view { + function log(address p0, string memory p1, string memory p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,string,string)", p0, p1, p2)); } - function log( - address p0, - string memory p1, - bool p2 - ) internal view { + function log(address p0, string memory p1, bool p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,string,bool)", p0, p1, p2)); } - function log( - address p0, - string memory p1, - address p2 - ) internal view { + function log(address p0, string memory p1, address p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,string,address)", p0, p1, p2)); } - function log( - address p0, - bool p1, - uint256 p2 - ) internal view { + function log(address p0, bool p1, uint256 p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,bool,uint)", p0, p1, p2)); } - function log( - address p0, - bool p1, - string memory p2 - ) internal view { + function log(address p0, bool p1, string memory p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,bool,string)", p0, p1, p2)); } - function log( - address p0, - bool p1, - bool p2 - ) internal view { + function log(address p0, bool p1, bool p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,bool,bool)", p0, p1, p2)); } - function log( - address p0, - bool p1, - address p2 - ) internal view { + function log(address p0, bool p1, address p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,bool,address)", p0, p1, p2)); } - function log( - address p0, - address p1, - uint256 p2 - ) internal view { + function log(address p0, address p1, uint256 p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,address,uint)", p0, p1, p2)); } - function log( - address p0, - address p1, - string memory p2 - ) internal view { + function log(address p0, address p1, string memory p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,address,string)", p0, p1, p2)); } - function log( - address p0, - address p1, - bool p2 - ) internal view { + function log(address p0, address p1, bool p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,address,bool)", p0, p1, p2)); } - function log( - address p0, - address p1, - address p2 - ) internal view { + function log(address p0, address p1, address p2) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,address,address)", p0, p1, p2)); } - function log( - uint256 p0, - uint256 p1, - uint256 p2, - uint256 p3 - ) internal view { + function log(uint256 p0, uint256 p1, uint256 p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,uint,uint,uint)", p0, p1, p2, p3)); } - function log( - uint256 p0, - uint256 p1, - uint256 p2, - string memory p3 - ) internal view { + function log(uint256 p0, uint256 p1, uint256 p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,uint,uint,string)", p0, p1, p2, p3)); } - function log( - uint256 p0, - uint256 p1, - uint256 p2, - bool p3 - ) internal view { + function log(uint256 p0, uint256 p1, uint256 p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,uint,uint,bool)", p0, p1, p2, p3)); } - function log( - uint256 p0, - uint256 p1, - uint256 p2, - address p3 - ) internal view { + function log(uint256 p0, uint256 p1, uint256 p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,uint,uint,address)", p0, p1, p2, p3)); } - function log( - uint256 p0, - uint256 p1, - string memory p2, - uint256 p3 - ) internal view { + function log(uint256 p0, uint256 p1, string memory p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,uint,string,uint)", p0, p1, p2, p3)); } - function log( - uint256 p0, - uint256 p1, - string memory p2, - string memory p3 - ) internal view { + function log(uint256 p0, uint256 p1, string memory p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,uint,string,string)", p0, p1, p2, p3)); } - function log( - uint256 p0, - uint256 p1, - string memory p2, - bool p3 - ) internal view { + function log(uint256 p0, uint256 p1, string memory p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,uint,string,bool)", p0, p1, p2, p3)); } - function log( - uint256 p0, - uint256 p1, - string memory p2, - address p3 - ) internal view { + function log(uint256 p0, uint256 p1, string memory p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,uint,string,address)", p0, p1, p2, p3)); } - function log( - uint256 p0, - uint256 p1, - bool p2, - uint256 p3 - ) internal view { + function log(uint256 p0, uint256 p1, bool p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,uint,bool,uint)", p0, p1, p2, p3)); } - function log( - uint256 p0, - uint256 p1, - bool p2, - string memory p3 - ) internal view { + function log(uint256 p0, uint256 p1, bool p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,uint,bool,string)", p0, p1, p2, p3)); } - function log( - uint256 p0, - uint256 p1, - bool p2, - bool p3 - ) internal view { + function log(uint256 p0, uint256 p1, bool p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,uint,bool,bool)", p0, p1, p2, p3)); } - function log( - uint256 p0, - uint256 p1, - bool p2, - address p3 - ) internal view { + function log(uint256 p0, uint256 p1, bool p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,uint,bool,address)", p0, p1, p2, p3)); } - function log( - uint256 p0, - uint256 p1, - address p2, - uint256 p3 - ) internal view { + function log(uint256 p0, uint256 p1, address p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,uint,address,uint)", p0, p1, p2, p3)); } - function log( - uint256 p0, - uint256 p1, - address p2, - string memory p3 - ) internal view { + function log(uint256 p0, uint256 p1, address p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,uint,address,string)", p0, p1, p2, p3)); } - function log( - uint256 p0, - uint256 p1, - address p2, - bool p3 - ) internal view { + function log(uint256 p0, uint256 p1, address p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,uint,address,bool)", p0, p1, p2, p3)); } - function log( - uint256 p0, - uint256 p1, - address p2, - address p3 - ) internal view { + function log(uint256 p0, uint256 p1, address p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,uint,address,address)", p0, p1, p2, p3)); } - function log( - uint256 p0, - string memory p1, - uint256 p2, - uint256 p3 - ) internal view { + function log(uint256 p0, string memory p1, uint256 p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,string,uint,uint)", p0, p1, p2, p3)); } - function log( - uint256 p0, - string memory p1, - uint256 p2, - string memory p3 - ) internal view { + function log(uint256 p0, string memory p1, uint256 p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,string,uint,string)", p0, p1, p2, p3)); } - function log( - uint256 p0, - string memory p1, - uint256 p2, - bool p3 - ) internal view { + function log(uint256 p0, string memory p1, uint256 p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,string,uint,bool)", p0, p1, p2, p3)); } - function log( - uint256 p0, - string memory p1, - uint256 p2, - address p3 - ) internal view { + function log(uint256 p0, string memory p1, uint256 p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,string,uint,address)", p0, p1, p2, p3)); } - function log( - uint256 p0, - string memory p1, - string memory p2, - uint256 p3 - ) internal view { + function log(uint256 p0, string memory p1, string memory p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,string,string,uint)", p0, p1, p2, p3)); } - function log( - uint256 p0, - string memory p1, - string memory p2, - string memory p3 - ) internal view { + function log(uint256 p0, string memory p1, string memory p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,string,string,string)", p0, p1, p2, p3)); } - function log( - uint256 p0, - string memory p1, - string memory p2, - bool p3 - ) internal view { + function log(uint256 p0, string memory p1, string memory p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,string,string,bool)", p0, p1, p2, p3)); } - function log( - uint256 p0, - string memory p1, - string memory p2, - address p3 - ) internal view { + function log(uint256 p0, string memory p1, string memory p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,string,string,address)", p0, p1, p2, p3)); } - function log( - uint256 p0, - string memory p1, - bool p2, - uint256 p3 - ) internal view { + function log(uint256 p0, string memory p1, bool p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,string,bool,uint)", p0, p1, p2, p3)); } - function log( - uint256 p0, - string memory p1, - bool p2, - string memory p3 - ) internal view { + function log(uint256 p0, string memory p1, bool p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,string,bool,string)", p0, p1, p2, p3)); } - function log( - uint256 p0, - string memory p1, - bool p2, - bool p3 - ) internal view { + function log(uint256 p0, string memory p1, bool p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,string,bool,bool)", p0, p1, p2, p3)); } - function log( - uint256 p0, - string memory p1, - bool p2, - address p3 - ) internal view { + function log(uint256 p0, string memory p1, bool p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,string,bool,address)", p0, p1, p2, p3)); } - function log( - uint256 p0, - string memory p1, - address p2, - uint256 p3 - ) internal view { + function log(uint256 p0, string memory p1, address p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,string,address,uint)", p0, p1, p2, p3)); } - function log( - uint256 p0, - string memory p1, - address p2, - string memory p3 - ) internal view { + function log(uint256 p0, string memory p1, address p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,string,address,string)", p0, p1, p2, p3)); } - function log( - uint256 p0, - string memory p1, - address p2, - bool p3 - ) internal view { + function log(uint256 p0, string memory p1, address p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,string,address,bool)", p0, p1, p2, p3)); } - function log( - uint256 p0, - string memory p1, - address p2, - address p3 - ) internal view { + function log(uint256 p0, string memory p1, address p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,string,address,address)", p0, p1, p2, p3)); } - function log( - uint256 p0, - bool p1, - uint256 p2, - uint256 p3 - ) internal view { + function log(uint256 p0, bool p1, uint256 p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,bool,uint,uint)", p0, p1, p2, p3)); } - function log( - uint256 p0, - bool p1, - uint256 p2, - string memory p3 - ) internal view { + function log(uint256 p0, bool p1, uint256 p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,bool,uint,string)", p0, p1, p2, p3)); } - function log( - uint256 p0, - bool p1, - uint256 p2, - bool p3 - ) internal view { + function log(uint256 p0, bool p1, uint256 p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,bool,uint,bool)", p0, p1, p2, p3)); } - function log( - uint256 p0, - bool p1, - uint256 p2, - address p3 - ) internal view { + function log(uint256 p0, bool p1, uint256 p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,bool,uint,address)", p0, p1, p2, p3)); } - function log( - uint256 p0, - bool p1, - string memory p2, - uint256 p3 - ) internal view { + function log(uint256 p0, bool p1, string memory p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,bool,string,uint)", p0, p1, p2, p3)); } - function log( - uint256 p0, - bool p1, - string memory p2, - string memory p3 - ) internal view { + function log(uint256 p0, bool p1, string memory p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,bool,string,string)", p0, p1, p2, p3)); } - function log( - uint256 p0, - bool p1, - string memory p2, - bool p3 - ) internal view { + function log(uint256 p0, bool p1, string memory p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,bool,string,bool)", p0, p1, p2, p3)); } - function log( - uint256 p0, - bool p1, - string memory p2, - address p3 - ) internal view { + function log(uint256 p0, bool p1, string memory p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,bool,string,address)", p0, p1, p2, p3)); } - function log( - uint256 p0, - bool p1, - bool p2, - uint256 p3 - ) internal view { + function log(uint256 p0, bool p1, bool p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,bool,bool,uint)", p0, p1, p2, p3)); } - function log( - uint256 p0, - bool p1, - bool p2, - string memory p3 - ) internal view { + function log(uint256 p0, bool p1, bool p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,bool,bool,string)", p0, p1, p2, p3)); } - function log( - uint256 p0, - bool p1, - bool p2, - bool p3 - ) internal view { + function log(uint256 p0, bool p1, bool p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,bool,bool,bool)", p0, p1, p2, p3)); } - function log( - uint256 p0, - bool p1, - bool p2, - address p3 - ) internal view { + function log(uint256 p0, bool p1, bool p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,bool,bool,address)", p0, p1, p2, p3)); } - function log( - uint256 p0, - bool p1, - address p2, - uint256 p3 - ) internal view { + function log(uint256 p0, bool p1, address p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,bool,address,uint)", p0, p1, p2, p3)); } - function log( - uint256 p0, - bool p1, - address p2, - string memory p3 - ) internal view { + function log(uint256 p0, bool p1, address p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,bool,address,string)", p0, p1, p2, p3)); } - function log( - uint256 p0, - bool p1, - address p2, - bool p3 - ) internal view { + function log(uint256 p0, bool p1, address p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,bool,address,bool)", p0, p1, p2, p3)); } - function log( - uint256 p0, - bool p1, - address p2, - address p3 - ) internal view { + function log(uint256 p0, bool p1, address p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,bool,address,address)", p0, p1, p2, p3)); } - function log( - uint256 p0, - address p1, - uint256 p2, - uint256 p3 - ) internal view { + function log(uint256 p0, address p1, uint256 p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,address,uint,uint)", p0, p1, p2, p3)); } - function log( - uint256 p0, - address p1, - uint256 p2, - string memory p3 - ) internal view { + function log(uint256 p0, address p1, uint256 p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,address,uint,string)", p0, p1, p2, p3)); } - function log( - uint256 p0, - address p1, - uint256 p2, - bool p3 - ) internal view { + function log(uint256 p0, address p1, uint256 p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,address,uint,bool)", p0, p1, p2, p3)); } - function log( - uint256 p0, - address p1, - uint256 p2, - address p3 - ) internal view { + function log(uint256 p0, address p1, uint256 p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,address,uint,address)", p0, p1, p2, p3)); } - function log( - uint256 p0, - address p1, - string memory p2, - uint256 p3 - ) internal view { + function log(uint256 p0, address p1, string memory p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,address,string,uint)", p0, p1, p2, p3)); } - function log( - uint256 p0, - address p1, - string memory p2, - string memory p3 - ) internal view { + function log(uint256 p0, address p1, string memory p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,address,string,string)", p0, p1, p2, p3)); } - function log( - uint256 p0, - address p1, - string memory p2, - bool p3 - ) internal view { + function log(uint256 p0, address p1, string memory p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,address,string,bool)", p0, p1, p2, p3)); } - function log( - uint256 p0, - address p1, - string memory p2, - address p3 - ) internal view { + function log(uint256 p0, address p1, string memory p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,address,string,address)", p0, p1, p2, p3)); } - function log( - uint256 p0, - address p1, - bool p2, - uint256 p3 - ) internal view { + function log(uint256 p0, address p1, bool p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,address,bool,uint)", p0, p1, p2, p3)); } - function log( - uint256 p0, - address p1, - bool p2, - string memory p3 - ) internal view { + function log(uint256 p0, address p1, bool p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,address,bool,string)", p0, p1, p2, p3)); } - function log( - uint256 p0, - address p1, - bool p2, - bool p3 - ) internal view { + function log(uint256 p0, address p1, bool p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,address,bool,bool)", p0, p1, p2, p3)); } - function log( - uint256 p0, - address p1, - bool p2, - address p3 - ) internal view { + function log(uint256 p0, address p1, bool p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,address,bool,address)", p0, p1, p2, p3)); } - function log( - uint256 p0, - address p1, - address p2, - uint256 p3 - ) internal view { + function log(uint256 p0, address p1, address p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,address,address,uint)", p0, p1, p2, p3)); } - function log( - uint256 p0, - address p1, - address p2, - string memory p3 - ) internal view { + function log(uint256 p0, address p1, address p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,address,address,string)", p0, p1, p2, p3)); } - function log( - uint256 p0, - address p1, - address p2, - bool p3 - ) internal view { + function log(uint256 p0, address p1, address p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,address,address,bool)", p0, p1, p2, p3)); } - function log( - uint256 p0, - address p1, - address p2, - address p3 - ) internal view { + function log(uint256 p0, address p1, address p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(uint,address,address,address)", p0, p1, p2, p3)); } - function log( - string memory p0, - uint256 p1, - uint256 p2, - uint256 p3 - ) internal view { + function log(string memory p0, uint256 p1, uint256 p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,uint,uint,uint)", p0, p1, p2, p3)); } - function log( - string memory p0, - uint256 p1, - uint256 p2, - string memory p3 - ) internal view { + function log(string memory p0, uint256 p1, uint256 p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,uint,uint,string)", p0, p1, p2, p3)); } - function log( - string memory p0, - uint256 p1, - uint256 p2, - bool p3 - ) internal view { + function log(string memory p0, uint256 p1, uint256 p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,uint,uint,bool)", p0, p1, p2, p3)); } - function log( - string memory p0, - uint256 p1, - uint256 p2, - address p3 - ) internal view { + function log(string memory p0, uint256 p1, uint256 p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,uint,uint,address)", p0, p1, p2, p3)); } - function log( - string memory p0, - uint256 p1, - string memory p2, - uint256 p3 - ) internal view { + function log(string memory p0, uint256 p1, string memory p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,uint,string,uint)", p0, p1, p2, p3)); } - function log( - string memory p0, - uint256 p1, - string memory p2, - string memory p3 - ) internal view { + function log(string memory p0, uint256 p1, string memory p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,uint,string,string)", p0, p1, p2, p3)); } - function log( - string memory p0, - uint256 p1, - string memory p2, - bool p3 - ) internal view { + function log(string memory p0, uint256 p1, string memory p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,uint,string,bool)", p0, p1, p2, p3)); } - function log( - string memory p0, - uint256 p1, - string memory p2, - address p3 - ) internal view { + function log(string memory p0, uint256 p1, string memory p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,uint,string,address)", p0, p1, p2, p3)); } - function log( - string memory p0, - uint256 p1, - bool p2, - uint256 p3 - ) internal view { + function log(string memory p0, uint256 p1, bool p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,uint,bool,uint)", p0, p1, p2, p3)); } - function log( - string memory p0, - uint256 p1, - bool p2, - string memory p3 - ) internal view { + function log(string memory p0, uint256 p1, bool p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,uint,bool,string)", p0, p1, p2, p3)); } - function log( - string memory p0, - uint256 p1, - bool p2, - bool p3 - ) internal view { + function log(string memory p0, uint256 p1, bool p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,uint,bool,bool)", p0, p1, p2, p3)); } - function log( - string memory p0, - uint256 p1, - bool p2, - address p3 - ) internal view { + function log(string memory p0, uint256 p1, bool p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,uint,bool,address)", p0, p1, p2, p3)); } - function log( - string memory p0, - uint256 p1, - address p2, - uint256 p3 - ) internal view { + function log(string memory p0, uint256 p1, address p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,uint,address,uint)", p0, p1, p2, p3)); } - function log( - string memory p0, - uint256 p1, - address p2, - string memory p3 - ) internal view { + function log(string memory p0, uint256 p1, address p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,uint,address,string)", p0, p1, p2, p3)); } - function log( - string memory p0, - uint256 p1, - address p2, - bool p3 - ) internal view { + function log(string memory p0, uint256 p1, address p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,uint,address,bool)", p0, p1, p2, p3)); } - function log( - string memory p0, - uint256 p1, - address p2, - address p3 - ) internal view { + function log(string memory p0, uint256 p1, address p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,uint,address,address)", p0, p1, p2, p3)); } - function log( - string memory p0, - string memory p1, - uint256 p2, - uint256 p3 - ) internal view { + function log(string memory p0, string memory p1, uint256 p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,string,uint,uint)", p0, p1, p2, p3)); } - function log( - string memory p0, - string memory p1, - uint256 p2, - string memory p3 - ) internal view { + function log(string memory p0, string memory p1, uint256 p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,string,uint,string)", p0, p1, p2, p3)); } - function log( - string memory p0, - string memory p1, - uint256 p2, - bool p3 - ) internal view { + function log(string memory p0, string memory p1, uint256 p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,string,uint,bool)", p0, p1, p2, p3)); } - function log( - string memory p0, - string memory p1, - uint256 p2, - address p3 - ) internal view { + function log(string memory p0, string memory p1, uint256 p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,string,uint,address)", p0, p1, p2, p3)); } - function log( - string memory p0, - string memory p1, - string memory p2, - uint256 p3 - ) internal view { + function log(string memory p0, string memory p1, string memory p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,string,string,uint)", p0, p1, p2, p3)); } - function log( - string memory p0, - string memory p1, - string memory p2, - string memory p3 - ) internal view { + function log(string memory p0, string memory p1, string memory p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,string,string,string)", p0, p1, p2, p3)); } - function log( - string memory p0, - string memory p1, - string memory p2, - bool p3 - ) internal view { + function log(string memory p0, string memory p1, string memory p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,string,string,bool)", p0, p1, p2, p3)); } - function log( - string memory p0, - string memory p1, - string memory p2, - address p3 - ) internal view { + function log(string memory p0, string memory p1, string memory p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,string,string,address)", p0, p1, p2, p3)); } - function log( - string memory p0, - string memory p1, - bool p2, - uint256 p3 - ) internal view { + function log(string memory p0, string memory p1, bool p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,string,bool,uint)", p0, p1, p2, p3)); } - function log( - string memory p0, - string memory p1, - bool p2, - string memory p3 - ) internal view { + function log(string memory p0, string memory p1, bool p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,string,bool,string)", p0, p1, p2, p3)); } - function log( - string memory p0, - string memory p1, - bool p2, - bool p3 - ) internal view { + function log(string memory p0, string memory p1, bool p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,string,bool,bool)", p0, p1, p2, p3)); } - function log( - string memory p0, - string memory p1, - bool p2, - address p3 - ) internal view { + function log(string memory p0, string memory p1, bool p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,string,bool,address)", p0, p1, p2, p3)); } - function log( - string memory p0, - string memory p1, - address p2, - uint256 p3 - ) internal view { + function log(string memory p0, string memory p1, address p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,string,address,uint)", p0, p1, p2, p3)); } - function log( - string memory p0, - string memory p1, - address p2, - string memory p3 - ) internal view { + function log(string memory p0, string memory p1, address p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,string,address,string)", p0, p1, p2, p3)); } - function log( - string memory p0, - string memory p1, - address p2, - bool p3 - ) internal view { + function log(string memory p0, string memory p1, address p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,string,address,bool)", p0, p1, p2, p3)); } - function log( - string memory p0, - string memory p1, - address p2, - address p3 - ) internal view { + function log(string memory p0, string memory p1, address p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,string,address,address)", p0, p1, p2, p3)); } - function log( - string memory p0, - bool p1, - uint256 p2, - uint256 p3 - ) internal view { + function log(string memory p0, bool p1, uint256 p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,bool,uint,uint)", p0, p1, p2, p3)); } - function log( - string memory p0, - bool p1, - uint256 p2, - string memory p3 - ) internal view { + function log(string memory p0, bool p1, uint256 p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,bool,uint,string)", p0, p1, p2, p3)); } - function log( - string memory p0, - bool p1, - uint256 p2, - bool p3 - ) internal view { + function log(string memory p0, bool p1, uint256 p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,bool,uint,bool)", p0, p1, p2, p3)); } - function log( - string memory p0, - bool p1, - uint256 p2, - address p3 - ) internal view { + function log(string memory p0, bool p1, uint256 p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,bool,uint,address)", p0, p1, p2, p3)); } - function log( - string memory p0, - bool p1, - string memory p2, - uint256 p3 - ) internal view { + function log(string memory p0, bool p1, string memory p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,bool,string,uint)", p0, p1, p2, p3)); } - function log( - string memory p0, - bool p1, - string memory p2, - string memory p3 - ) internal view { + function log(string memory p0, bool p1, string memory p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,bool,string,string)", p0, p1, p2, p3)); } - function log( - string memory p0, - bool p1, - string memory p2, - bool p3 - ) internal view { + function log(string memory p0, bool p1, string memory p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,bool,string,bool)", p0, p1, p2, p3)); } - function log( - string memory p0, - bool p1, - string memory p2, - address p3 - ) internal view { + function log(string memory p0, bool p1, string memory p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,bool,string,address)", p0, p1, p2, p3)); } - function log( - string memory p0, - bool p1, - bool p2, - uint256 p3 - ) internal view { + function log(string memory p0, bool p1, bool p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,bool,bool,uint)", p0, p1, p2, p3)); } - function log( - string memory p0, - bool p1, - bool p2, - string memory p3 - ) internal view { + function log(string memory p0, bool p1, bool p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,bool,bool,string)", p0, p1, p2, p3)); } - function log( - string memory p0, - bool p1, - bool p2, - bool p3 - ) internal view { + function log(string memory p0, bool p1, bool p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,bool,bool,bool)", p0, p1, p2, p3)); } - function log( - string memory p0, - bool p1, - bool p2, - address p3 - ) internal view { + function log(string memory p0, bool p1, bool p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,bool,bool,address)", p0, p1, p2, p3)); } - function log( - string memory p0, - bool p1, - address p2, - uint256 p3 - ) internal view { + function log(string memory p0, bool p1, address p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,bool,address,uint)", p0, p1, p2, p3)); } - function log( - string memory p0, - bool p1, - address p2, - string memory p3 - ) internal view { + function log(string memory p0, bool p1, address p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,bool,address,string)", p0, p1, p2, p3)); } - function log( - string memory p0, - bool p1, - address p2, - bool p3 - ) internal view { + function log(string memory p0, bool p1, address p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,bool,address,bool)", p0, p1, p2, p3)); } - function log( - string memory p0, - bool p1, - address p2, - address p3 - ) internal view { + function log(string memory p0, bool p1, address p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,bool,address,address)", p0, p1, p2, p3)); } - function log( - string memory p0, - address p1, - uint256 p2, - uint256 p3 - ) internal view { + function log(string memory p0, address p1, uint256 p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,address,uint,uint)", p0, p1, p2, p3)); } - function log( - string memory p0, - address p1, - uint256 p2, - string memory p3 - ) internal view { + function log(string memory p0, address p1, uint256 p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,address,uint,string)", p0, p1, p2, p3)); } - function log( - string memory p0, - address p1, - uint256 p2, - bool p3 - ) internal view { + function log(string memory p0, address p1, uint256 p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,address,uint,bool)", p0, p1, p2, p3)); } - function log( - string memory p0, - address p1, - uint256 p2, - address p3 - ) internal view { + function log(string memory p0, address p1, uint256 p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,address,uint,address)", p0, p1, p2, p3)); } - function log( - string memory p0, - address p1, - string memory p2, - uint256 p3 - ) internal view { + function log(string memory p0, address p1, string memory p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,address,string,uint)", p0, p1, p2, p3)); } - function log( - string memory p0, - address p1, - string memory p2, - string memory p3 - ) internal view { + function log(string memory p0, address p1, string memory p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,address,string,string)", p0, p1, p2, p3)); } - function log( - string memory p0, - address p1, - string memory p2, - bool p3 - ) internal view { + function log(string memory p0, address p1, string memory p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,address,string,bool)", p0, p1, p2, p3)); } - function log( - string memory p0, - address p1, - string memory p2, - address p3 - ) internal view { + function log(string memory p0, address p1, string memory p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,address,string,address)", p0, p1, p2, p3)); } - function log( - string memory p0, - address p1, - bool p2, - uint256 p3 - ) internal view { + function log(string memory p0, address p1, bool p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,address,bool,uint)", p0, p1, p2, p3)); } - function log( - string memory p0, - address p1, - bool p2, - string memory p3 - ) internal view { + function log(string memory p0, address p1, bool p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,address,bool,string)", p0, p1, p2, p3)); } - function log( - string memory p0, - address p1, - bool p2, - bool p3 - ) internal view { + function log(string memory p0, address p1, bool p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,address,bool,bool)", p0, p1, p2, p3)); } - function log( - string memory p0, - address p1, - bool p2, - address p3 - ) internal view { + function log(string memory p0, address p1, bool p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,address,bool,address)", p0, p1, p2, p3)); } - function log( - string memory p0, - address p1, - address p2, - uint256 p3 - ) internal view { + function log(string memory p0, address p1, address p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,address,address,uint)", p0, p1, p2, p3)); } - function log( - string memory p0, - address p1, - address p2, - string memory p3 - ) internal view { + function log(string memory p0, address p1, address p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,address,address,string)", p0, p1, p2, p3)); } - function log( - string memory p0, - address p1, - address p2, - bool p3 - ) internal view { + function log(string memory p0, address p1, address p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,address,address,bool)", p0, p1, p2, p3)); } - function log( - string memory p0, - address p1, - address p2, - address p3 - ) internal view { + function log(string memory p0, address p1, address p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(string,address,address,address)", p0, p1, p2, p3)); } - function log( - bool p0, - uint256 p1, - uint256 p2, - uint256 p3 - ) internal view { + function log(bool p0, uint256 p1, uint256 p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,uint,uint,uint)", p0, p1, p2, p3)); } - function log( - bool p0, - uint256 p1, - uint256 p2, - string memory p3 - ) internal view { + function log(bool p0, uint256 p1, uint256 p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,uint,uint,string)", p0, p1, p2, p3)); } - function log( - bool p0, - uint256 p1, - uint256 p2, - bool p3 - ) internal view { + function log(bool p0, uint256 p1, uint256 p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,uint,uint,bool)", p0, p1, p2, p3)); } - function log( - bool p0, - uint256 p1, - uint256 p2, - address p3 - ) internal view { + function log(bool p0, uint256 p1, uint256 p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,uint,uint,address)", p0, p1, p2, p3)); } - function log( - bool p0, - uint256 p1, - string memory p2, - uint256 p3 - ) internal view { + function log(bool p0, uint256 p1, string memory p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,uint,string,uint)", p0, p1, p2, p3)); } - function log( - bool p0, - uint256 p1, - string memory p2, - string memory p3 - ) internal view { + function log(bool p0, uint256 p1, string memory p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,uint,string,string)", p0, p1, p2, p3)); } - function log( - bool p0, - uint256 p1, - string memory p2, - bool p3 - ) internal view { + function log(bool p0, uint256 p1, string memory p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,uint,string,bool)", p0, p1, p2, p3)); } - function log( - bool p0, - uint256 p1, - string memory p2, - address p3 - ) internal view { + function log(bool p0, uint256 p1, string memory p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,uint,string,address)", p0, p1, p2, p3)); } - function log( - bool p0, - uint256 p1, - bool p2, - uint256 p3 - ) internal view { + function log(bool p0, uint256 p1, bool p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,uint,bool,uint)", p0, p1, p2, p3)); } - function log( - bool p0, - uint256 p1, - bool p2, - string memory p3 - ) internal view { + function log(bool p0, uint256 p1, bool p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,uint,bool,string)", p0, p1, p2, p3)); } - function log( - bool p0, - uint256 p1, - bool p2, - bool p3 - ) internal view { + function log(bool p0, uint256 p1, bool p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,uint,bool,bool)", p0, p1, p2, p3)); } - function log( - bool p0, - uint256 p1, - bool p2, - address p3 - ) internal view { + function log(bool p0, uint256 p1, bool p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,uint,bool,address)", p0, p1, p2, p3)); } - function log( - bool p0, - uint256 p1, - address p2, - uint256 p3 - ) internal view { + function log(bool p0, uint256 p1, address p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,uint,address,uint)", p0, p1, p2, p3)); } - function log( - bool p0, - uint256 p1, - address p2, - string memory p3 - ) internal view { + function log(bool p0, uint256 p1, address p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,uint,address,string)", p0, p1, p2, p3)); } - function log( - bool p0, - uint256 p1, - address p2, - bool p3 - ) internal view { + function log(bool p0, uint256 p1, address p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,uint,address,bool)", p0, p1, p2, p3)); } - function log( - bool p0, - uint256 p1, - address p2, - address p3 - ) internal view { + function log(bool p0, uint256 p1, address p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,uint,address,address)", p0, p1, p2, p3)); } - function log( - bool p0, - string memory p1, - uint256 p2, - uint256 p3 - ) internal view { + function log(bool p0, string memory p1, uint256 p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,string,uint,uint)", p0, p1, p2, p3)); } - function log( - bool p0, - string memory p1, - uint256 p2, - string memory p3 - ) internal view { + function log(bool p0, string memory p1, uint256 p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,string,uint,string)", p0, p1, p2, p3)); } - function log( - bool p0, - string memory p1, - uint256 p2, - bool p3 - ) internal view { + function log(bool p0, string memory p1, uint256 p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,string,uint,bool)", p0, p1, p2, p3)); } - function log( - bool p0, - string memory p1, - uint256 p2, - address p3 - ) internal view { + function log(bool p0, string memory p1, uint256 p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,string,uint,address)", p0, p1, p2, p3)); } - function log( - bool p0, - string memory p1, - string memory p2, - uint256 p3 - ) internal view { + function log(bool p0, string memory p1, string memory p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,string,string,uint)", p0, p1, p2, p3)); } - function log( - bool p0, - string memory p1, - string memory p2, - string memory p3 - ) internal view { + function log(bool p0, string memory p1, string memory p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,string,string,string)", p0, p1, p2, p3)); } - function log( - bool p0, - string memory p1, - string memory p2, - bool p3 - ) internal view { + function log(bool p0, string memory p1, string memory p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,string,string,bool)", p0, p1, p2, p3)); } - function log( - bool p0, - string memory p1, - string memory p2, - address p3 - ) internal view { + function log(bool p0, string memory p1, string memory p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,string,string,address)", p0, p1, p2, p3)); } - function log( - bool p0, - string memory p1, - bool p2, - uint256 p3 - ) internal view { + function log(bool p0, string memory p1, bool p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,string,bool,uint)", p0, p1, p2, p3)); } - function log( - bool p0, - string memory p1, - bool p2, - string memory p3 - ) internal view { + function log(bool p0, string memory p1, bool p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,string,bool,string)", p0, p1, p2, p3)); } - function log( - bool p0, - string memory p1, - bool p2, - bool p3 - ) internal view { + function log(bool p0, string memory p1, bool p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,string,bool,bool)", p0, p1, p2, p3)); } - function log( - bool p0, - string memory p1, - bool p2, - address p3 - ) internal view { + function log(bool p0, string memory p1, bool p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,string,bool,address)", p0, p1, p2, p3)); } - function log( - bool p0, - string memory p1, - address p2, - uint256 p3 - ) internal view { + function log(bool p0, string memory p1, address p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,string,address,uint)", p0, p1, p2, p3)); } - function log( - bool p0, - string memory p1, - address p2, - string memory p3 - ) internal view { + function log(bool p0, string memory p1, address p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,string,address,string)", p0, p1, p2, p3)); } - function log( - bool p0, - string memory p1, - address p2, - bool p3 - ) internal view { + function log(bool p0, string memory p1, address p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,string,address,bool)", p0, p1, p2, p3)); } - function log( - bool p0, - string memory p1, - address p2, - address p3 - ) internal view { + function log(bool p0, string memory p1, address p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,string,address,address)", p0, p1, p2, p3)); } - function log( - bool p0, - bool p1, - uint256 p2, - uint256 p3 - ) internal view { + function log(bool p0, bool p1, uint256 p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,bool,uint,uint)", p0, p1, p2, p3)); } - function log( - bool p0, - bool p1, - uint256 p2, - string memory p3 - ) internal view { + function log(bool p0, bool p1, uint256 p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,bool,uint,string)", p0, p1, p2, p3)); } - function log( - bool p0, - bool p1, - uint256 p2, - bool p3 - ) internal view { + function log(bool p0, bool p1, uint256 p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,bool,uint,bool)", p0, p1, p2, p3)); } - function log( - bool p0, - bool p1, - uint256 p2, - address p3 - ) internal view { + function log(bool p0, bool p1, uint256 p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,bool,uint,address)", p0, p1, p2, p3)); } - function log( - bool p0, - bool p1, - string memory p2, - uint256 p3 - ) internal view { + function log(bool p0, bool p1, string memory p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,bool,string,uint)", p0, p1, p2, p3)); } - function log( - bool p0, - bool p1, - string memory p2, - string memory p3 - ) internal view { + function log(bool p0, bool p1, string memory p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,bool,string,string)", p0, p1, p2, p3)); } - function log( - bool p0, - bool p1, - string memory p2, - bool p3 - ) internal view { + function log(bool p0, bool p1, string memory p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,bool,string,bool)", p0, p1, p2, p3)); } - function log( - bool p0, - bool p1, - string memory p2, - address p3 - ) internal view { + function log(bool p0, bool p1, string memory p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,bool,string,address)", p0, p1, p2, p3)); } - function log( - bool p0, - bool p1, - bool p2, - uint256 p3 - ) internal view { + function log(bool p0, bool p1, bool p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,bool,bool,uint)", p0, p1, p2, p3)); } - function log( - bool p0, - bool p1, - bool p2, - string memory p3 - ) internal view { + function log(bool p0, bool p1, bool p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,bool,bool,string)", p0, p1, p2, p3)); } - function log( - bool p0, - bool p1, - bool p2, - bool p3 - ) internal view { + function log(bool p0, bool p1, bool p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,bool,bool,bool)", p0, p1, p2, p3)); } - function log( - bool p0, - bool p1, - bool p2, - address p3 - ) internal view { + function log(bool p0, bool p1, bool p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,bool,bool,address)", p0, p1, p2, p3)); } - function log( - bool p0, - bool p1, - address p2, - uint256 p3 - ) internal view { + function log(bool p0, bool p1, address p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,bool,address,uint)", p0, p1, p2, p3)); } - function log( - bool p0, - bool p1, - address p2, - string memory p3 - ) internal view { + function log(bool p0, bool p1, address p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,bool,address,string)", p0, p1, p2, p3)); } - function log( - bool p0, - bool p1, - address p2, - bool p3 - ) internal view { + function log(bool p0, bool p1, address p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,bool,address,bool)", p0, p1, p2, p3)); } - function log( - bool p0, - bool p1, - address p2, - address p3 - ) internal view { + function log(bool p0, bool p1, address p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,bool,address,address)", p0, p1, p2, p3)); } - function log( - bool p0, - address p1, - uint256 p2, - uint256 p3 - ) internal view { + function log(bool p0, address p1, uint256 p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,address,uint,uint)", p0, p1, p2, p3)); } - function log( - bool p0, - address p1, - uint256 p2, - string memory p3 - ) internal view { + function log(bool p0, address p1, uint256 p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,address,uint,string)", p0, p1, p2, p3)); } - function log( - bool p0, - address p1, - uint256 p2, - bool p3 - ) internal view { + function log(bool p0, address p1, uint256 p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,address,uint,bool)", p0, p1, p2, p3)); } - function log( - bool p0, - address p1, - uint256 p2, - address p3 - ) internal view { + function log(bool p0, address p1, uint256 p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,address,uint,address)", p0, p1, p2, p3)); } - function log( - bool p0, - address p1, - string memory p2, - uint256 p3 - ) internal view { + function log(bool p0, address p1, string memory p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,address,string,uint)", p0, p1, p2, p3)); } - function log( - bool p0, - address p1, - string memory p2, - string memory p3 - ) internal view { + function log(bool p0, address p1, string memory p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,address,string,string)", p0, p1, p2, p3)); } - function log( - bool p0, - address p1, - string memory p2, - bool p3 - ) internal view { + function log(bool p0, address p1, string memory p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,address,string,bool)", p0, p1, p2, p3)); } - function log( - bool p0, - address p1, - string memory p2, - address p3 - ) internal view { + function log(bool p0, address p1, string memory p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,address,string,address)", p0, p1, p2, p3)); } - function log( - bool p0, - address p1, - bool p2, - uint256 p3 - ) internal view { + function log(bool p0, address p1, bool p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,address,bool,uint)", p0, p1, p2, p3)); } - function log( - bool p0, - address p1, - bool p2, - string memory p3 - ) internal view { + function log(bool p0, address p1, bool p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,address,bool,string)", p0, p1, p2, p3)); } - function log( - bool p0, - address p1, - bool p2, - bool p3 - ) internal view { + function log(bool p0, address p1, bool p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,address,bool,bool)", p0, p1, p2, p3)); } - function log( - bool p0, - address p1, - bool p2, - address p3 - ) internal view { + function log(bool p0, address p1, bool p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,address,bool,address)", p0, p1, p2, p3)); } - function log( - bool p0, - address p1, - address p2, - uint256 p3 - ) internal view { + function log(bool p0, address p1, address p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,address,address,uint)", p0, p1, p2, p3)); } - function log( - bool p0, - address p1, - address p2, - string memory p3 - ) internal view { + function log(bool p0, address p1, address p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,address,address,string)", p0, p1, p2, p3)); } - function log( - bool p0, - address p1, - address p2, - bool p3 - ) internal view { + function log(bool p0, address p1, address p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,address,address,bool)", p0, p1, p2, p3)); } - function log( - bool p0, - address p1, - address p2, - address p3 - ) internal view { + function log(bool p0, address p1, address p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(bool,address,address,address)", p0, p1, p2, p3)); } - function log( - address p0, - uint256 p1, - uint256 p2, - uint256 p3 - ) internal view { + function log(address p0, uint256 p1, uint256 p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,uint,uint,uint)", p0, p1, p2, p3)); } - function log( - address p0, - uint256 p1, - uint256 p2, - string memory p3 - ) internal view { + function log(address p0, uint256 p1, uint256 p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,uint,uint,string)", p0, p1, p2, p3)); } - function log( - address p0, - uint256 p1, - uint256 p2, - bool p3 - ) internal view { + function log(address p0, uint256 p1, uint256 p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,uint,uint,bool)", p0, p1, p2, p3)); } - function log( - address p0, - uint256 p1, - uint256 p2, - address p3 - ) internal view { + function log(address p0, uint256 p1, uint256 p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,uint,uint,address)", p0, p1, p2, p3)); } - function log( - address p0, - uint256 p1, - string memory p2, - uint256 p3 - ) internal view { + function log(address p0, uint256 p1, string memory p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,uint,string,uint)", p0, p1, p2, p3)); } - function log( - address p0, - uint256 p1, - string memory p2, - string memory p3 - ) internal view { + function log(address p0, uint256 p1, string memory p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,uint,string,string)", p0, p1, p2, p3)); } - function log( - address p0, - uint256 p1, - string memory p2, - bool p3 - ) internal view { + function log(address p0, uint256 p1, string memory p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,uint,string,bool)", p0, p1, p2, p3)); } - function log( - address p0, - uint256 p1, - string memory p2, - address p3 - ) internal view { + function log(address p0, uint256 p1, string memory p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,uint,string,address)", p0, p1, p2, p3)); } - function log( - address p0, - uint256 p1, - bool p2, - uint256 p3 - ) internal view { + function log(address p0, uint256 p1, bool p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,uint,bool,uint)", p0, p1, p2, p3)); } - function log( - address p0, - uint256 p1, - bool p2, - string memory p3 - ) internal view { + function log(address p0, uint256 p1, bool p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,uint,bool,string)", p0, p1, p2, p3)); } - function log( - address p0, - uint256 p1, - bool p2, - bool p3 - ) internal view { + function log(address p0, uint256 p1, bool p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,uint,bool,bool)", p0, p1, p2, p3)); } - function log( - address p0, - uint256 p1, - bool p2, - address p3 - ) internal view { + function log(address p0, uint256 p1, bool p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,uint,bool,address)", p0, p1, p2, p3)); } - function log( - address p0, - uint256 p1, - address p2, - uint256 p3 - ) internal view { + function log(address p0, uint256 p1, address p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,uint,address,uint)", p0, p1, p2, p3)); } - function log( - address p0, - uint256 p1, - address p2, - string memory p3 - ) internal view { + function log(address p0, uint256 p1, address p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,uint,address,string)", p0, p1, p2, p3)); } - function log( - address p0, - uint256 p1, - address p2, - bool p3 - ) internal view { + function log(address p0, uint256 p1, address p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,uint,address,bool)", p0, p1, p2, p3)); } - function log( - address p0, - uint256 p1, - address p2, - address p3 - ) internal view { + function log(address p0, uint256 p1, address p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,uint,address,address)", p0, p1, p2, p3)); } - function log( - address p0, - string memory p1, - uint256 p2, - uint256 p3 - ) internal view { + function log(address p0, string memory p1, uint256 p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,string,uint,uint)", p0, p1, p2, p3)); } - function log( - address p0, - string memory p1, - uint256 p2, - string memory p3 - ) internal view { + function log(address p0, string memory p1, uint256 p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,string,uint,string)", p0, p1, p2, p3)); } - function log( - address p0, - string memory p1, - uint256 p2, - bool p3 - ) internal view { + function log(address p0, string memory p1, uint256 p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,string,uint,bool)", p0, p1, p2, p3)); } - function log( - address p0, - string memory p1, - uint256 p2, - address p3 - ) internal view { + function log(address p0, string memory p1, uint256 p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,string,uint,address)", p0, p1, p2, p3)); } - function log( - address p0, - string memory p1, - string memory p2, - uint256 p3 - ) internal view { + function log(address p0, string memory p1, string memory p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,string,string,uint)", p0, p1, p2, p3)); } - function log( - address p0, - string memory p1, - string memory p2, - string memory p3 - ) internal view { + function log(address p0, string memory p1, string memory p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,string,string,string)", p0, p1, p2, p3)); } - function log( - address p0, - string memory p1, - string memory p2, - bool p3 - ) internal view { + function log(address p0, string memory p1, string memory p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,string,string,bool)", p0, p1, p2, p3)); } - function log( - address p0, - string memory p1, - string memory p2, - address p3 - ) internal view { + function log(address p0, string memory p1, string memory p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,string,string,address)", p0, p1, p2, p3)); } - function log( - address p0, - string memory p1, - bool p2, - uint256 p3 - ) internal view { + function log(address p0, string memory p1, bool p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,string,bool,uint)", p0, p1, p2, p3)); } - function log( - address p0, - string memory p1, - bool p2, - string memory p3 - ) internal view { + function log(address p0, string memory p1, bool p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,string,bool,string)", p0, p1, p2, p3)); } - function log( - address p0, - string memory p1, - bool p2, - bool p3 - ) internal view { + function log(address p0, string memory p1, bool p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,string,bool,bool)", p0, p1, p2, p3)); } - function log( - address p0, - string memory p1, - bool p2, - address p3 - ) internal view { + function log(address p0, string memory p1, bool p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,string,bool,address)", p0, p1, p2, p3)); } - function log( - address p0, - string memory p1, - address p2, - uint256 p3 - ) internal view { + function log(address p0, string memory p1, address p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,string,address,uint)", p0, p1, p2, p3)); } - function log( - address p0, - string memory p1, - address p2, - string memory p3 - ) internal view { + function log(address p0, string memory p1, address p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,string,address,string)", p0, p1, p2, p3)); } - function log( - address p0, - string memory p1, - address p2, - bool p3 - ) internal view { + function log(address p0, string memory p1, address p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,string,address,bool)", p0, p1, p2, p3)); } - function log( - address p0, - string memory p1, - address p2, - address p3 - ) internal view { + function log(address p0, string memory p1, address p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,string,address,address)", p0, p1, p2, p3)); } - function log( - address p0, - bool p1, - uint256 p2, - uint256 p3 - ) internal view { + function log(address p0, bool p1, uint256 p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,bool,uint,uint)", p0, p1, p2, p3)); } - function log( - address p0, - bool p1, - uint256 p2, - string memory p3 - ) internal view { + function log(address p0, bool p1, uint256 p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,bool,uint,string)", p0, p1, p2, p3)); } - function log( - address p0, - bool p1, - uint256 p2, - bool p3 - ) internal view { + function log(address p0, bool p1, uint256 p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,bool,uint,bool)", p0, p1, p2, p3)); } - function log( - address p0, - bool p1, - uint256 p2, - address p3 - ) internal view { + function log(address p0, bool p1, uint256 p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,bool,uint,address)", p0, p1, p2, p3)); } - function log( - address p0, - bool p1, - string memory p2, - uint256 p3 - ) internal view { + function log(address p0, bool p1, string memory p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,bool,string,uint)", p0, p1, p2, p3)); } - function log( - address p0, - bool p1, - string memory p2, - string memory p3 - ) internal view { + function log(address p0, bool p1, string memory p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,bool,string,string)", p0, p1, p2, p3)); } - function log( - address p0, - bool p1, - string memory p2, - bool p3 - ) internal view { + function log(address p0, bool p1, string memory p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,bool,string,bool)", p0, p1, p2, p3)); } - function log( - address p0, - bool p1, - string memory p2, - address p3 - ) internal view { + function log(address p0, bool p1, string memory p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,bool,string,address)", p0, p1, p2, p3)); } - function log( - address p0, - bool p1, - bool p2, - uint256 p3 - ) internal view { + function log(address p0, bool p1, bool p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,bool,bool,uint)", p0, p1, p2, p3)); } - function log( - address p0, - bool p1, - bool p2, - string memory p3 - ) internal view { + function log(address p0, bool p1, bool p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,bool,bool,string)", p0, p1, p2, p3)); } - function log( - address p0, - bool p1, - bool p2, - bool p3 - ) internal view { + function log(address p0, bool p1, bool p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,bool,bool,bool)", p0, p1, p2, p3)); } - function log( - address p0, - bool p1, - bool p2, - address p3 - ) internal view { + function log(address p0, bool p1, bool p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,bool,bool,address)", p0, p1, p2, p3)); } - function log( - address p0, - bool p1, - address p2, - uint256 p3 - ) internal view { + function log(address p0, bool p1, address p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,bool,address,uint)", p0, p1, p2, p3)); } - function log( - address p0, - bool p1, - address p2, - string memory p3 - ) internal view { + function log(address p0, bool p1, address p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,bool,address,string)", p0, p1, p2, p3)); } - function log( - address p0, - bool p1, - address p2, - bool p3 - ) internal view { + function log(address p0, bool p1, address p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,bool,address,bool)", p0, p1, p2, p3)); } - function log( - address p0, - bool p1, - address p2, - address p3 - ) internal view { + function log(address p0, bool p1, address p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,bool,address,address)", p0, p1, p2, p3)); } - function log( - address p0, - address p1, - uint256 p2, - uint256 p3 - ) internal view { + function log(address p0, address p1, uint256 p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,address,uint,uint)", p0, p1, p2, p3)); } - function log( - address p0, - address p1, - uint256 p2, - string memory p3 - ) internal view { + function log(address p0, address p1, uint256 p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,address,uint,string)", p0, p1, p2, p3)); } - function log( - address p0, - address p1, - uint256 p2, - bool p3 - ) internal view { + function log(address p0, address p1, uint256 p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,address,uint,bool)", p0, p1, p2, p3)); } - function log( - address p0, - address p1, - uint256 p2, - address p3 - ) internal view { + function log(address p0, address p1, uint256 p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,address,uint,address)", p0, p1, p2, p3)); } - function log( - address p0, - address p1, - string memory p2, - uint256 p3 - ) internal view { + function log(address p0, address p1, string memory p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,address,string,uint)", p0, p1, p2, p3)); } - function log( - address p0, - address p1, - string memory p2, - string memory p3 - ) internal view { + function log(address p0, address p1, string memory p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,address,string,string)", p0, p1, p2, p3)); } - function log( - address p0, - address p1, - string memory p2, - bool p3 - ) internal view { + function log(address p0, address p1, string memory p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,address,string,bool)", p0, p1, p2, p3)); } - function log( - address p0, - address p1, - string memory p2, - address p3 - ) internal view { + function log(address p0, address p1, string memory p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,address,string,address)", p0, p1, p2, p3)); } - function log( - address p0, - address p1, - bool p2, - uint256 p3 - ) internal view { + function log(address p0, address p1, bool p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,address,bool,uint)", p0, p1, p2, p3)); } - function log( - address p0, - address p1, - bool p2, - string memory p3 - ) internal view { + function log(address p0, address p1, bool p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,address,bool,string)", p0, p1, p2, p3)); } - function log( - address p0, - address p1, - bool p2, - bool p3 - ) internal view { + function log(address p0, address p1, bool p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,address,bool,bool)", p0, p1, p2, p3)); } - function log( - address p0, - address p1, - bool p2, - address p3 - ) internal view { + function log(address p0, address p1, bool p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,address,bool,address)", p0, p1, p2, p3)); } - function log( - address p0, - address p1, - address p2, - uint256 p3 - ) internal view { + function log(address p0, address p1, address p2, uint256 p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,address,address,uint)", p0, p1, p2, p3)); } - function log( - address p0, - address p1, - address p2, - string memory p3 - ) internal view { + function log(address p0, address p1, address p2, string memory p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,address,address,string)", p0, p1, p2, p3)); } - function log( - address p0, - address p1, - address p2, - bool p3 - ) internal view { + function log(address p0, address p1, address p2, bool p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,address,address,bool)", p0, p1, p2, p3)); } - function log( - address p0, - address p1, - address p2, - address p3 - ) internal view { + function log(address p0, address p1, address p2, address p3) internal view { _sendLogPayload(abi.encodeWithSignature("log(address,address,address,address)", p0, p1, p2, p3)); } } diff --git a/src/test/utils/SignatureMint1155Utils.sol b/src/test/utils/SignatureMint1155Utils.sol new file mode 100644 index 000000000..e6be03f55 --- /dev/null +++ b/src/test/utils/SignatureMint1155Utils.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.0; + +import "contracts/extension/interface/ISignatureMintERC1155.sol"; + +contract SignatureMint1155Utils { + bytes32 internal DOMAIN_SEPARATOR; + + constructor() { + bytes32 typeHash = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + bytes32 hashedName = keccak256(bytes("SignatureMintERC1155")); + bytes32 hashedVersion = keccak256(bytes("1")); + DOMAIN_SEPARATOR = _buildDomainSeparator(typeHash, hashedName, hashedVersion); + } + + bytes32 internal constant TYPEHASH = + keccak256( + "MintRequest(address to,address royaltyRecipient,uint256 royaltyBps,address primarySaleRecipient,uint256 tokenId,string uri,uint256 quantity,uint256 pricePerToken,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + + function _buildDomainSeparator( + bytes32 typeHash, + bytes32 nameHash, + bytes32 versionHash + ) private view returns (bytes32) { + return keccak256(abi.encode(typeHash, nameHash, versionHash, block.chainid, address(this))); + } + + // computes the hash of a permit + function getStructHash(ISignatureMintERC1155.MintRequest memory _req) internal pure returns (bytes32) { + return + keccak256( + bytes.concat( + abi.encode( + TYPEHASH, + _req.to, + _req.royaltyRecipient, + _req.royaltyBps, + _req.primarySaleRecipient, + _req.tokenId, + keccak256(bytes(_req.uri)) + ), + abi.encode( + _req.quantity, + _req.pricePerToken, + _req.currency, + _req.validityStartTimestamp, + _req.validityEndTimestamp, + _req.uid + ) + ) + ); + } + + // computes the hash of the fully encoded EIP-712 message for the domain, which can be used to recover the signer + function getTypedDataHash(ISignatureMintERC1155.MintRequest memory _req) public view returns (bytes32) { + return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, getStructHash(_req))); + } +} diff --git a/src/test/utils/Wallet.sol b/src/test/utils/Wallet.sol index e0a3072e1..e3f8dc4a9 100644 --- a/src/test/utils/Wallet.sol +++ b/src/test/utils/Wallet.sol @@ -9,19 +9,11 @@ import "../mocks/MockERC721.sol"; import "../mocks/MockERC1155.sol"; contract Wallet is ERC721Holder, ERC1155Holder { - function transferERC20( - address token, - address to, - uint256 amount - ) public { + function transferERC20(address token, address to, uint256 amount) public { MockERC20(token).transfer(to, amount); } - function setAllowanceERC20( - address token, - address spender, - uint256 allowanceAmount - ) public { + function setAllowanceERC20(address token, address spender, uint256 allowanceAmount) public { MockERC20(token).approve(spender, allowanceAmount); } @@ -29,19 +21,11 @@ contract Wallet is ERC721Holder, ERC1155Holder { MockERC20(token).burn(amount); } - function transferERC721( - address token, - address to, - uint256 tokenId - ) public { + function transferERC721(address token, address to, uint256 tokenId) public { MockERC721(token).transferFrom(address(this), to, tokenId); } - function setApprovalForAllERC721( - address token, - address operator, - bool toApprove - ) public { + function setApprovalForAllERC721(address token, address operator, bool toApprove) public { MockERC721(token).setApprovalForAll(operator, toApprove); } @@ -49,29 +33,15 @@ contract Wallet is ERC721Holder, ERC1155Holder { MockERC721(token).burn(tokenId); } - function transferERC1155( - address token, - address to, - uint256 tokenId, - uint256 amount, - bytes calldata data - ) external { + function transferERC1155(address token, address to, uint256 tokenId, uint256 amount, bytes calldata data) external { MockERC1155(token).safeTransferFrom(address(this), to, tokenId, amount, data); } - function setApprovalForAllERC1155( - address token, - address operator, - bool toApprove - ) public { + function setApprovalForAllERC1155(address token, address operator, bool toApprove) public { MockERC1155(token).setApprovalForAll(operator, toApprove); } - function burnERC1155( - address token, - uint256 tokenId, - uint256 amount - ) public { + function burnERC1155(address token, uint256 tokenId, uint256 amount) public { MockERC1155(token).burn(address(this), tokenId, amount); } } diff --git a/src/test/vote-BTT/initialize/initialize.t.sol b/src/test/vote-BTT/initialize/initialize.t.sol new file mode 100644 index 000000000..9e9227ac6 --- /dev/null +++ b/src/test/vote-BTT/initialize/initialize.t.sol @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { ERC20Vote } from "contracts/base/ERC20Vote.sol"; + +contract MyVoteERC20 is VoteERC20 { + function eip712NameHash() external view returns (bytes32) { + return _EIP712NameHash(); + } + + function eip712VersionHash() external view returns (bytes32) { + return _EIP712VersionHash(); + } +} + +contract VoteERC20Test_Initialize is BaseTest { + address payable public implementation; + address payable public proxy; + address public token; + uint256 public initialVotingDelay; + uint256 public initialVotingPeriod; + uint256 public initialProposalThreshold; + uint256 public initialVoteQuorumFraction; + + event VotingDelaySet(uint256 oldVotingDelay, uint256 newVotingDelay); + event VotingPeriodSet(uint256 oldVotingPeriod, uint256 newVotingPeriod); + event ProposalThresholdSet(uint256 oldProposalThreshold, uint256 newProposalThreshold); + event QuorumNumeratorUpdated(uint256 oldQuorumNumerator, uint256 newQuorumNumerator); + + function setUp() public override { + super.setUp(); + + // Deploy voting token + token = address(new ERC20Vote(deployer, "Voting VoteERC20", "VT")); + + // Voting param initial values + initialVotingDelay = 5; + initialVotingPeriod = 10; + initialProposalThreshold = 100; + initialVoteQuorumFraction = 50; + + // Deploy implementation. + implementation = payable(address(new MyVoteERC20())); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = payable( + address( + new TWProxy( + implementation, + abi.encodeCall( + VoteERC20.initialize, + ( + NAME, + CONTRACT_URI, + forwarders(), + token, + initialVotingDelay, + initialVotingPeriod, + initialProposalThreshold, + initialVoteQuorumFraction + ) + ) + ) + ) + ); + } + + function test_initialize_initializingImplementation() public { + vm.expectRevert("Initializable: contract is already initialized"); + VoteERC20(implementation).initialize( + NAME, + CONTRACT_URI, + forwarders(), + token, + initialVotingDelay, + initialVotingPeriod, + initialProposalThreshold, + initialVoteQuorumFraction + ); + } + + modifier whenNotImplementation() { + _; + } + + function test_initialize_proxyAlreadyInitialized() public whenNotImplementation { + vm.expectRevert("Initializable: contract is already initialized"); + MyVoteERC20(proxy).initialize( + NAME, + CONTRACT_URI, + forwarders(), + token, + initialVotingDelay, + initialVotingPeriod, + initialProposalThreshold, + initialVoteQuorumFraction + ); + } + + modifier whenProxyNotInitialized() { + proxy = payable(address(new TWProxy(implementation, ""))); + _; + } + + function test_initialize() public whenNotImplementation whenProxyNotInitialized { + MyVoteERC20(proxy).initialize( + NAME, + CONTRACT_URI, + forwarders(), + token, + initialVotingDelay, + initialVotingPeriod, + initialProposalThreshold, + initialVoteQuorumFraction + ); + + // check state + MyVoteERC20 voteContract = MyVoteERC20(proxy); + + assertEq(voteContract.eip712NameHash(), keccak256(bytes(NAME))); + assertEq(voteContract.eip712VersionHash(), keccak256(bytes("1"))); + + address[] memory _trustedForwarders = forwarders(); + for (uint256 i = 0; i < _trustedForwarders.length; i++) { + assertTrue(voteContract.isTrustedForwarder(_trustedForwarders[i])); + } + + assertEq(voteContract.name(), NAME); + assertEq(voteContract.contractURI(), CONTRACT_URI); + assertEq(voteContract.votingDelay(), initialVotingDelay); + assertEq(voteContract.votingPeriod(), initialVotingPeriod); + assertEq(voteContract.proposalThreshold(), initialProposalThreshold); + assertEq(voteContract.quorumNumerator(), initialVoteQuorumFraction); + assertEq(address(voteContract.token()), token); + } + + function test_initialize_event_VotingDelaySet() public whenNotImplementation whenProxyNotInitialized { + vm.expectEmit(false, false, false, true); + emit VotingDelaySet(0, initialVotingDelay); + MyVoteERC20(proxy).initialize( + NAME, + CONTRACT_URI, + forwarders(), + token, + initialVotingDelay, + initialVotingPeriod, + initialProposalThreshold, + initialVoteQuorumFraction + ); + } + + function test_initialize_event_VotingPeriodSet() public whenNotImplementation whenProxyNotInitialized { + vm.expectEmit(false, false, false, true); + emit VotingPeriodSet(0, initialVotingPeriod); + MyVoteERC20(proxy).initialize( + NAME, + CONTRACT_URI, + forwarders(), + token, + initialVotingDelay, + initialVotingPeriod, + initialProposalThreshold, + initialVoteQuorumFraction + ); + } + + function test_initialize_event_ProposalThresholdSet() public whenNotImplementation whenProxyNotInitialized { + vm.expectEmit(false, false, false, true); + emit ProposalThresholdSet(0, initialProposalThreshold); + MyVoteERC20(proxy).initialize( + NAME, + CONTRACT_URI, + forwarders(), + token, + initialVotingDelay, + initialVotingPeriod, + initialProposalThreshold, + initialVoteQuorumFraction + ); + } + + function test_initialize_event_QuorumNumeratorUpdated() public whenNotImplementation whenProxyNotInitialized { + vm.expectEmit(false, false, false, true); + emit QuorumNumeratorUpdated(0, initialVoteQuorumFraction); + MyVoteERC20(proxy).initialize( + NAME, + CONTRACT_URI, + forwarders(), + token, + initialVotingDelay, + initialVotingPeriod, + initialProposalThreshold, + initialVoteQuorumFraction + ); + } +} diff --git a/src/test/vote-BTT/initialize/initialize.tree b/src/test/vote-BTT/initialize/initialize.tree new file mode 100644 index 000000000..6b935174b --- /dev/null +++ b/src/test/vote-BTT/initialize/initialize.tree @@ -0,0 +1,30 @@ +initialize( + string memory _name, + string memory _contractURI, + address[] memory _trustedForwarders, + address _token, + uint256 _initialVotingDelay, + uint256 _initialVotingPeriod, + uint256 _initialProposalThreshold, + uint256 _initialVoteQuorumFraction +) +├── when initializing the implementation contract (not proxy) +│ └── it should revert ✅ +└── when it is a proxy to the implementation + └── when it is already initialized + │ └── it should revert ✅ + └── when it is not initialized + └── it should set trustedForwarder mapping to true for all addresses in `_trustedForwarders` ✅ + └── it should correctly set EIP712 name hash and version hash ✅ + └── it should set name to `_name` input param ✅ + └── it should set contractURI to `_contractURI` param value ✅ + └── it should set votingDelay to `_initialVotingDelay` param value ✅ + └── it should emit VotingDelaySet event ✅ + └── it should set votingPeriod to `_initialVotingPeriod` param value ✅ + └── it should emit VotingPeriodSet event ✅ + └── it should set proposalThreshold to `_initialProposalThreshold` param value ✅ + └── it should emit ProposalThresholdSet event ✅ + └── it should set voting token address as the `_token` param value ✅ + └── it should set initial quorum numerator as `_initialVoteQuorumFraction` param value ✅ + └── it should emit QuorumNumeratorUpdated event ✅ + diff --git a/src/test/vote-BTT/other-functions/other.t.sol b/src/test/vote-BTT/other-functions/other.t.sol new file mode 100644 index 000000000..960a94e7e --- /dev/null +++ b/src/test/vote-BTT/other-functions/other.t.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; +import { IStaking721 } from "contracts/extension/interface/IStaking721.sol"; +import { IERC2981 } from "contracts/eip/interface/IERC2981.sol"; + +import "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721ReceiverUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155ReceiverUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/introspection/IERC165Upgradeable.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { ERC20Vote } from "contracts/base/ERC20Vote.sol"; + +contract MyVoteERC20 is VoteERC20 {} + +contract VoteERC20Test_OtherFunctions is BaseTest { + address payable public implementation; + address payable public proxy; + + address public token; + uint256 public initialVotingDelay; + uint256 public initialVotingPeriod; + uint256 public initialProposalThreshold; + uint256 public initialVoteQuorumFraction; + + MyVoteERC20 public voteContract; + + function setUp() public override { + super.setUp(); + + // Deploy voting token + vm.prank(deployer); + token = address(new ERC20Vote(deployer, "Voting VoteERC20", "VT")); + + // Voting param initial values + initialVotingDelay = 1; + initialVotingPeriod = 100; + initialProposalThreshold = 10; + initialVoteQuorumFraction = 1; + + // Deploy implementation. + implementation = payable(address(new MyVoteERC20())); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = payable( + address( + new TWProxy( + implementation, + abi.encodeCall( + VoteERC20.initialize, + ( + NAME, + CONTRACT_URI, + forwarders(), + token, + initialVotingDelay, + initialVotingPeriod, + initialProposalThreshold, + initialVoteQuorumFraction + ) + ) + ) + ) + ); + + voteContract = MyVoteERC20(proxy); + } + + function test_contractType() public { + assertEq(voteContract.contractType(), bytes32("VoteERC20")); + } + + function test_contractVersion() public { + assertEq(voteContract.contractVersion(), uint8(1)); + } + + function test_supportsInterface() public { + assertTrue(voteContract.supportsInterface(type(IERC165).interfaceId)); + assertTrue(voteContract.supportsInterface(type(IERC165Upgradeable).interfaceId)); + assertTrue(voteContract.supportsInterface(type(IERC721ReceiverUpgradeable).interfaceId)); + assertTrue(voteContract.supportsInterface(type(IERC1155ReceiverUpgradeable).interfaceId)); + // assertTrue(voteContract.supportsInterface(type(IGovernorUpgradeable).interfaceId)); + + // false for other not supported interfaces + assertFalse(voteContract.supportsInterface(type(IStaking721).interfaceId)); + } +} diff --git a/src/test/vote-BTT/other-functions/other.tree b/src/test/vote-BTT/other-functions/other.tree new file mode 100644 index 000000000..2649d89ae --- /dev/null +++ b/src/test/vote-BTT/other-functions/other.tree @@ -0,0 +1,9 @@ +contractType() +├── it should return bytes32("VoteERC20") ✅ + +contractVersion() +├── it should return uint8(1) ✅ + +supportsInterface(bytes4 interfaceId) +├── it should return true for supported interface ✅ +├── it should return false for not supported interface ✅ diff --git a/src/test/vote-BTT/propose/propose.t.sol b/src/test/vote-BTT/propose/propose.t.sol new file mode 100644 index 000000000..4bdd7f2ad --- /dev/null +++ b/src/test/vote-BTT/propose/propose.t.sol @@ -0,0 +1,281 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { ERC20Vote } from "contracts/base/ERC20Vote.sol"; + +contract MyVoteERC20 is VoteERC20 {} + +contract VoteERC20Test_Propose is BaseTest { + address payable public implementation; + address payable public proxy; + address internal caller; + string internal _contractURI; + + address public token; + uint256 public initialVotingDelay; + uint256 public initialVotingPeriod; + uint256 public initialProposalThreshold; + uint256 public initialVoteQuorumFraction; + + uint256 public proposalIdOne; + address[] public targetsOne; + uint256[] public valuesOne; + bytes[] public calldatasOne; + string public descriptionOne; + + uint256 public proposalIdTwo; + address[] public targetsTwo; + uint256[] public valuesTwo; + bytes[] public calldatasTwo; + string public descriptionTwo; + + MyVoteERC20 internal voteContract; + + event ProposalCreated( + uint256 proposalId, + address proposer, + address[] targets, + uint256[] values, + string[] signatures, + bytes[] calldatas, + uint256 startBlock, + uint256 endBlock, + string description + ); + + function setUp() public override { + super.setUp(); + + // Deploy voting token + vm.prank(deployer); + token = address(new ERC20Vote(deployer, "Voting VoteERC20", "VT")); + + // Voting param initial values + initialVotingDelay = 1; + initialVotingPeriod = 100; + initialProposalThreshold = 10; + initialVoteQuorumFraction = 1; + + // Deploy implementation. + implementation = payable(address(new MyVoteERC20())); + + caller = getActor(1); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = payable( + address( + new TWProxy( + implementation, + abi.encodeCall( + VoteERC20.initialize, + ( + NAME, + CONTRACT_URI, + forwarders(), + token, + initialVotingDelay, + initialVotingPeriod, + initialProposalThreshold, + initialVoteQuorumFraction + ) + ) + ) + ) + ); + + voteContract = MyVoteERC20(proxy); + _contractURI = "ipfs://contracturi"; + + // mint governance tokens + vm.startPrank(deployer); + ERC20Vote(token).mintTo(caller, 100); + ERC20Vote(token).mintTo(deployer, 100); + vm.stopPrank(); + + // delegate votes to self + vm.prank(caller); + ERC20Vote(token).delegate(caller); + vm.prank(deployer); + ERC20Vote(token).delegate(deployer); + + vm.roll(2); + + // create first proposal + _createProposalOne(); + } + + function _createProposalOne() internal { + descriptionOne = "set proposal one"; + + bytes memory data = abi.encodeWithSelector(VoteERC20.setContractURI.selector, _contractURI); + + targetsOne.push(address(voteContract)); + valuesOne.push(0); + calldatasOne.push(data); + + vm.prank(deployer); + proposalIdOne = voteContract.propose(targetsOne, valuesOne, calldatasOne, descriptionOne); + } + + function _setupProposalTwo() internal { + descriptionTwo = "set proposal two"; + + bytes memory data = abi.encodeWithSelector(VoteERC20.setContractURI.selector, _contractURI); + + targetsTwo.push(address(voteContract)); + valuesTwo.push(0); + calldatasTwo.push(data); + } + + function test_propose_votesBelowThreshold() public { + _setupProposalTwo(); + + vm.prank(address(0x123)); // random address that doesn't have threshold votes + vm.expectRevert("Governor: proposer votes below proposal threshold"); + voteContract.propose(targetsTwo, valuesTwo, calldatasTwo, descriptionTwo); + } + + modifier hasThresholdVotes() { + _; + } + + function test_propose_emptyTargets() public hasThresholdVotes { + address[] memory _targets; + uint256[] memory _values; + bytes[] memory _calldatas; + string memory _description; + + vm.prank(caller); + vm.expectRevert("Governor: empty proposal"); + voteContract.propose(_targets, _values, _calldatas, _description); + } + + modifier whenNotEmptyTargets() { + _; + } + + function test_propose_lengthMismatchTargetsValues() public hasThresholdVotes whenNotEmptyTargets { + _setupProposalTwo(); + + uint256[] memory _values; + + vm.prank(caller); + vm.expectRevert("Governor: invalid proposal length"); + voteContract.propose(targetsTwo, _values, calldatasTwo, descriptionTwo); + } + + modifier whenTargetValuesEqualLength() { + _; + } + + function test_propose_lengthMismatchTargetsCalldatas() + public + hasThresholdVotes + whenNotEmptyTargets + whenTargetValuesEqualLength + { + _setupProposalTwo(); + + bytes[] memory _calldatas; + + vm.prank(caller); + vm.expectRevert("Governor: invalid proposal length"); + voteContract.propose(targetsTwo, valuesTwo, _calldatas, descriptionTwo); + } + + modifier whenTargetCalldatasEqualLength() { + _; + } + + function test_propose_proposalAlreadyExists() + public + hasThresholdVotes + whenNotEmptyTargets + whenTargetValuesEqualLength + whenTargetCalldatasEqualLength + { + // creating proposalOne again + + vm.prank(caller); + vm.expectRevert("Governor: proposal already exists"); + voteContract.propose(targetsOne, valuesOne, calldatasOne, descriptionOne); + } + + modifier whenProposalNotAlreadyExists() { + _; + } + + function test_propose() + public + hasThresholdVotes + whenNotEmptyTargets + whenTargetValuesEqualLength + whenTargetCalldatasEqualLength + whenProposalNotAlreadyExists + { + _setupProposalTwo(); + + vm.prank(caller); + proposalIdTwo = voteContract.propose(targetsTwo, valuesTwo, calldatasTwo, descriptionTwo); + + assertEq(voteContract.proposalSnapshot(proposalIdTwo), voteContract.votingDelay() + block.number); + assertEq( + voteContract.proposalDeadline(proposalIdTwo), + voteContract.proposalSnapshot(proposalIdTwo) + voteContract.votingPeriod() + ); + assertEq(voteContract.proposalIndex(), 2); // because two proposals have been created + assertEq(voteContract.getAllProposals().length, 2); + + ( + uint256 _proposalId, + address _proposer, + uint256 _startBlock, + uint256 _endBlock, + string memory _description + ) = voteContract.proposals(1); + + assertEq(_proposalId, proposalIdTwo); + assertEq(_proposer, caller); + assertEq(_startBlock, voteContract.proposalSnapshot(proposalIdTwo)); + assertEq(_endBlock, voteContract.proposalDeadline(proposalIdTwo)); + assertEq(_description, descriptionTwo); + } + + function test_propose_event_ProposalCreated() + public + hasThresholdVotes + whenNotEmptyTargets + whenTargetValuesEqualLength + whenTargetCalldatasEqualLength + whenProposalNotAlreadyExists + { + _setupProposalTwo(); + uint256 _expectedProposalId = voteContract.hashProposal( + targetsTwo, + valuesTwo, + calldatasTwo, + keccak256(bytes(descriptionTwo)) + ); + string[] memory signatures = new string[](targetsTwo.length); + + vm.startPrank(caller); + vm.expectEmit(false, false, false, true); + emit ProposalCreated( + _expectedProposalId, + caller, + targetsTwo, + valuesTwo, + signatures, + calldatasTwo, + voteContract.votingDelay() + block.number, + voteContract.votingDelay() + block.number + voteContract.votingPeriod(), + descriptionTwo + ); + voteContract.propose(targetsTwo, valuesTwo, calldatasTwo, descriptionTwo); + vm.stopPrank(); + } +} diff --git a/src/test/vote-BTT/propose/propose.tree b/src/test/vote-BTT/propose/propose.tree new file mode 100644 index 000000000..5df017a7e --- /dev/null +++ b/src/test/vote-BTT/propose/propose.tree @@ -0,0 +1,26 @@ +propose( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + string memory description +) +├── when caller has votes below proposal threshold + │ └── it should revert ✅ + └── when caller has votes above or equal to proposal threshold + └── when length of `targets` is zero + │ └── it should revert ✅ + └── when length of `targets` is not zero + └── when lengths of `targets` and `values` not equal + │ └── it should revert ✅ + └── when lengths of `targets` and `values` are equal + └── when lengths of `targets` and `calldatas` not equal + │ └── it should revert ✅ + └── when lengths of `targets` and `calldatas` are equal + └── when proposal already exists + │ └── it should revert ✅ + └── when proposal doesn't already exist + └── it should set vote start deadline equal to block number plus voting delay ✅ + └── it should set vote end deadline equal to voting period plus vote start deadline ✅ + └── it should increment proposalIndex by 1 ✅ + └── it should add the new proposal in proposals mapping ✅ + └── it should emit ProposalCreated event ✅ \ No newline at end of file diff --git a/src/test/vote-BTT/set-contract-uri/setContractURI.t.sol b/src/test/vote-BTT/set-contract-uri/setContractURI.t.sol new file mode 100644 index 000000000..cc35af82e --- /dev/null +++ b/src/test/vote-BTT/set-contract-uri/setContractURI.t.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { ERC20Vote } from "contracts/base/ERC20Vote.sol"; + +contract MyVoteERC20 is VoteERC20 {} + +contract VoteERC20Test_SetContractURI is BaseTest { + address payable public implementation; + address payable public proxy; + address internal caller; + string internal _contractURI; + + address public token; + uint256 public initialVotingDelay; + uint256 public initialVotingPeriod; + uint256 public initialProposalThreshold; + uint256 public initialVoteQuorumFraction; + + uint256 public proposalId; + address[] public targets; + uint256[] public values; + bytes[] public calldatas; + string public description; + + MyVoteERC20 internal voteContract; + + function setUp() public override { + super.setUp(); + + // Deploy voting token + vm.prank(deployer); + token = address(new ERC20Vote(deployer, "Voting VoteERC20", "VT")); + + // Voting param initial values + initialVotingDelay = 1; + initialVotingPeriod = 100; + initialProposalThreshold = 10; + initialVoteQuorumFraction = 1; + + // Deploy implementation. + implementation = payable(address(new MyVoteERC20())); + + caller = getActor(1); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = payable( + address( + new TWProxy( + implementation, + abi.encodeCall( + VoteERC20.initialize, + ( + NAME, + CONTRACT_URI, + forwarders(), + token, + initialVotingDelay, + initialVotingPeriod, + initialProposalThreshold, + initialVoteQuorumFraction + ) + ) + ) + ) + ); + + voteContract = MyVoteERC20(proxy); + _contractURI = "ipfs://contracturi"; + + // mint governance tokens + vm.startPrank(deployer); + ERC20Vote(token).mintTo(caller, 100); + ERC20Vote(token).mintTo(deployer, 100); + vm.stopPrank(); + + // delegate votes to self + vm.prank(caller); + ERC20Vote(token).delegate(caller); + vm.prank(deployer); + ERC20Vote(token).delegate(deployer); + } + + function _createProposalForSetContractURI() internal { + description = "set contract URI"; + + bytes memory data = abi.encodeWithSelector(VoteERC20.setContractURI.selector, _contractURI); + + targets.push(address(voteContract)); + values.push(0); + calldatas.push(data); + + vm.prank(deployer); + proposalId = voteContract.propose(targets, values, calldatas, description); + } + + function test_setContractURI_callerNotAuthorized() public { + vm.prank(address(0x123)); + vm.expectRevert("Governor: onlyGovernance"); + voteContract.setContractURI(_contractURI); + } + + modifier whenCallerAuthorized() { + vm.roll(2); + _createProposalForSetContractURI(); + _; + } + + function test_setContractURI_empty() public whenCallerAuthorized { + vm.roll(10); + // first try execute without votes + vm.expectRevert("Governor: proposal not successful"); + voteContract.execute(targets, values, calldatas, keccak256(bytes(description))); + + // vote on proposal + vm.prank(caller); + voteContract.castVote(proposalId, 1); + + // execute + vm.roll(200); // deadline must be over, before execute can be called + voteContract.execute(targets, values, calldatas, keccak256(bytes(description))); + + // check state: get contract uri + assertEq(voteContract.contractURI(), _contractURI); + } +} diff --git a/src/test/vote-BTT/set-contract-uri/setContractURI.tree b/src/test/vote-BTT/set-contract-uri/setContractURI.tree new file mode 100644 index 000000000..f7819fc38 --- /dev/null +++ b/src/test/vote-BTT/set-contract-uri/setContractURI.tree @@ -0,0 +1,8 @@ +setContractURI(string calldata uri) +├── when caller is not authorized (i.e. execution not going through governance proposals) + │ └── it should revert ✅ + └── when caller is authorized (execution through governance proposals) + └── when `uri` is empty + │ └── it should update contract URI to empty string ✅ + └── when `uri` is not empty + └── it should update contract URI to `uri` ✅ \ No newline at end of file diff --git a/tasks/clean.ts b/tasks/clean.ts deleted file mode 100644 index 65ac7ec21..000000000 --- a/tasks/clean.ts +++ /dev/null @@ -1,12 +0,0 @@ -import fsExtra from "fs-extra"; -import { TASK_CLEAN } from "hardhat/builtin-tasks/task-names"; -import { task } from "hardhat/config"; - -task(TASK_CLEAN, "Overrides the standard clean task", async function (_taskArgs, { config }, runSuper) { - await fsExtra.remove("./coverage"); - await fsExtra.remove("./coverage.json"); - if (config.typechain?.outDir) { - await fsExtra.remove(config.typechain.outDir); - } - await runSuper(); -}); diff --git a/tasks/verify/console.ts b/tasks/verify/console.ts deleted file mode 100644 index d3c1b33e1..000000000 --- a/tasks/verify/console.ts +++ /dev/null @@ -1,33 +0,0 @@ -import hre, { ethers } from "hardhat"; -import { chainlinkVars } from "../../utils/chainlink"; -import addresses from "../../utils/addresses/console.json"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; - -const networkName: string = hre.network.name.toLowerCase(); - -// Get network dependent vars. -const { controlDeployer, forwarder, registry, treasury } = addresses[networkName as keyof typeof addresses] as any; - -async function verify() { - await hre.run("verify:verify", { - address: controlDeployer, - constructorArguments: [], - }); - - await hre.run("verify:verify", { - address: forwarder, - constructorArguments: [], - }); - - await hre.run("verify:verify", { - address: registry, - constructorArguments: [treasury, forwarder, controlDeployer], - }); -} - -verify() - .then(() => process.exit(0)) - .catch(err => { - console.error(err); - process.exit(1); - }); diff --git a/tasks/verify/console_accessPacks.ts b/tasks/verify/console_accessPacks.ts deleted file mode 100644 index 48d5d3b7c..000000000 --- a/tasks/verify/console_accessPacks.ts +++ /dev/null @@ -1,75 +0,0 @@ -import hre, { ethers } from "hardhat"; -import { chainlinkVars } from "../../utils/chainlink"; -import addresses from "../../utils/addresses/console_ap.json"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; - -const networkName: string = hre.network.name.toLowerCase(); - -// Get network dependent vars. -const { protocolControl, pack, market, accessNft, forwarder, registry } = addresses[ - networkName as keyof typeof addresses -] as any; -const { vrfCoordinator, linkTokenAddress, keyHash, fees } = chainlinkVars[networkName as keyof typeof chainlinkVars]; -const contractURI: string = ""; - -async function Forwarder() { - await hre.run("verify:verify", { - address: forwarder, - constructorArguments: [], - }); -} - -async function Registry() { - const [deployer]: SignerWithAddress[] = await ethers.getSigners(); - - await hre.run("verify:verify", { - address: registry, - constructorArguments: [deployer.address, forwarder, ethers.constants.AddressZero], - }); -} - -async function ProtocolControl() { - const [deployer]: SignerWithAddress[] = await ethers.getSigners(); - - await hre.run("verify:verify", { - address: protocolControl, - constructorArguments: [registry, deployer.address, contractURI], - }); -} - -async function Pack() { - await hre.run("verify:verify", { - address: pack, - constructorArguments: [protocolControl, contractURI, vrfCoordinator, linkTokenAddress, keyHash, fees, forwarder], - }); -} - -async function Market() { - await hre.run("verify:verify", { - address: market, - constructorArguments: [protocolControl, forwarder, contractURI], - }); -} - -async function AccessNFT() { - await hre.run("verify:verify", { - address: accessNft, - constructorArguments: [protocolControl, forwarder, contractURI], - }); -} - -async function verify() { - //await Forwarder(); - //await Registry(); - await ProtocolControl(); - await Pack(); - await Market(); - await AccessNFT(); -} - -verify() - .then(() => process.exit(0)) - .catch(err => { - console.error(err); - process.exit(1); - }); diff --git a/utils/addresses/console.json b/utils/addresses/console.json deleted file mode 100644 index 346639a42..000000000 --- a/utils/addresses/console.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "mainnet": { - "treasury": "0x09888aF38E0c153AB3D66CCC5f3FBb7aAB7Db115", - "controlDeployer": "0xC03b2E3BE709b96a9E71797213be58899F431943", - "registry": "0x902a29f2cfe9f8580ad672AaAD7E917d85ca9a2E", - "forwarder": "0xc82BbE41f2cF04e3a8efA18F7032BDD7f6d98a81" - }, - "rinkeby": { - "treasury": "0x7F610Ac4A50f4464cE51e6Ac33DfeB060B35A460", - "controlDeployer": "0xC03b2E3BE709b96a9E71797213be58899F431943", - "registry": "0x902a29f2cfe9f8580ad672AaAD7E917d85ca9a2E", - "forwarder": "0xc82BbE41f2cF04e3a8efA18F7032BDD7f6d98a81" - }, - "polygon": { - "treasury": "0x4769df58427Cd840DF120E764fBB97a3fD968Caf", - "controlDeployer": "0xC03b2E3BE709b96a9E71797213be58899F431943", - "registry": "0x902a29f2cfe9f8580ad672AaAD7E917d85ca9a2E", - "forwarder": "0xc82BbE41f2cF04e3a8efA18F7032BDD7f6d98a81" - }, - "mumbai": { - "treasury": "0xE00994EBDB59f70350E2cdeb897796F732331562", - "registry": "0x902a29f2cfe9f8580ad672AaAD7E917d85ca9a2E", - "forwarder": "0xc82BbE41f2cF04e3a8efA18F7032BDD7f6d98a81", - "controlDeployer": "0xC03b2E3BE709b96a9E71797213be58899F431943" - }, - "avax": { - "treasury": "0xE00994EBDB59f70350E2cdeb897796F732331562", - "controlDeployer": "0xC03b2E3BE709b96a9E71797213be58899F431943", - "registry": "0x902a29f2cfe9f8580ad672AaAD7E917d85ca9a2E", - "forwarder": "0xc82BbE41f2cF04e3a8efA18F7032BDD7f6d98a81" - }, - "avax_testnet": { - "treasury": "0xE00994EBDB59f70350E2cdeb897796F732331562", - "controlDeployer": "0xC03b2E3BE709b96a9E71797213be58899F431943", - "registry": "0x902a29f2cfe9f8580ad672AaAD7E917d85ca9a2E", - "forwarder": "0xc82BbE41f2cF04e3a8efA18F7032BDD7f6d98a81" - }, - "fantom": { - "treasury": "0x3CFd70C17013f2fC0069EdfF6A364ae835acE5dE", - "controlDeployer": "0xC03b2E3BE709b96a9E71797213be58899F431943", - "registry": "0x902a29f2cfe9f8580ad672AaAD7E917d85ca9a2E", - "forwarder": "0xc82BbE41f2cF04e3a8efA18F7032BDD7f6d98a81" - }, - "fantom_testnet": { - "treasury": "0x28d1268Cb1cbb5211Af79BFd65E429B734E19d5B", - "controlDeployer": "0xC03b2E3BE709b96a9E71797213be58899F431943", - "registry": "0x902a29f2cfe9f8580ad672AaAD7E917d85ca9a2E", - "forwarder": "0xc82BbE41f2cF04e3a8efA18F7032BDD7f6d98a81" - } -} diff --git a/utils/addresses/console_ap.json b/utils/addresses/console_ap.json deleted file mode 100644 index 441100c64..000000000 --- a/utils/addresses/console_ap.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "mainnet": { - "treasury": "0xE00994EBDB59f70350E2cdeb897796F732331562" - }, - "rinkeby": { - "treasury": "0xE00994EBDB59f70350E2cdeb897796F732331562" - }, - "polygon": { - "treasury": "0xE00994EBDB59f70350E2cdeb897796F732331562", - "registry": "0x3C8F6678b36291DDca275352D7413487C3Db2e20", - "forwarder": "0x3F3a82555bf686d8B8836a5Cd92409EF77511fbE", - "protocolControl": "0x04dACa227E30E50941e4c126bA7e4F4035Bb93f9", - "pack": "0xB350f6b4FD9e2009ad64db4E7a064E00dB82cE67", - "market": "0x55e4475DdE15F39E3bE837d205E2a09a8861153e", - "accessNft": "0xf1482e0aC5742B0683f51ed8e87bdbFA66f3da3f" - }, - "mumbai": { - "treasury": "0xE00994EBDB59f70350E2cdeb897796F732331562", - "registry": "0x3C8F6678b36291DDca275352D7413487C3Db2e20", - "forwarder": "0x3F3a82555bf686d8B8836a5Cd92409EF77511fbE", - "protocolControl": "0x04dACa227E30E50941e4c126bA7e4F4035Bb93f9", - "pack": "0xB350f6b4FD9e2009ad64db4E7a064E00dB82cE67", - "market": "0x55e4475DdE15F39E3bE837d205E2a09a8861153e", - "accessNft": "0xf1482e0aC5742B0683f51ed8e87bdbFA66f3da3f" - }, - "avax": { - "treasury": "0xE00994EBDB59f70350E2cdeb897796F732331562" - }, - "avax_testnet": { - "treasury": "0xE00994EBDB59f70350E2cdeb897796F732331562" - }, - "fantom": { - "treasury": "0xE00994EBDB59f70350E2cdeb897796F732331562" - }, - "fantom_testnet": { - "treasury": "0xE00994EBDB59f70350E2cdeb897796F732331562" - } -} diff --git a/utils/chainIds.ts b/utils/chainIds.ts deleted file mode 100644 index dca03b084..000000000 --- a/utils/chainIds.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const chainIds = { - ganache: 1337, - goerli: 5, - hardhat: 31337, - kovan: 42, - mainnet: 1, - rinkeby: 4, - ropsten: 3, - polygon: 137, - mumbai: 80001, -}; diff --git a/utils/chainlink.ts b/utils/chainlink.ts deleted file mode 100644 index 1c9f5c19a..000000000 --- a/utils/chainlink.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ethers } from "ethers"; - -export const chainlinkVars = { - matic: { - vrfCoordinator: "0x3d2341ADb2D31f1c5530cDC622016af293177AE0", - linkTokenAddress: "0xb0897686c545045aFc77CF20eC7A532E3120E0F1", - keyHash: "0xf86195cf7690c55907b2b611ebb7343a6f649bff128701cc542f0569e2c549da", - fees: ethers.utils.parseEther("0.0001"), - }, - polygon: { - vrfCoordinator: "0x3d2341ADb2D31f1c5530cDC622016af293177AE0", - linkTokenAddress: "0xb0897686c545045aFc77CF20eC7A532E3120E0F1", - keyHash: "0xf86195cf7690c55907b2b611ebb7343a6f649bff128701cc542f0569e2c549da", - fees: ethers.utils.parseEther("0.0001"), - }, - mumbai: { - vrfCoordinator: "0x8C7382F9D8f56b33781fE506E897a4F1e2d17255", - linkTokenAddress: "0x326C977E6efc84E512bB9C30f76E30c160eD06FB", - keyHash: "0x6e75b569a01ef56d18cab6a8e71e6600d6ce853834d4a5748b720d06f878b3a4", - fees: ethers.utils.parseEther("0.0001"), - }, - rinkeby: { - vrfCoordinator: "0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B", - linkTokenAddress: "0x01be23585060835e02b77ef475b0cc51aa1e0709", - keyHash: "0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311", - fees: ethers.utils.parseEther("0.1"), - }, - mainnet: { - vrfCoordinator: "0xf0d54349aDdcf704F77AE15b96510dEA15cb7952", - linkTokenAddress: "0x514910771AF9Ca656af840dff83E8264EcF986CA", - keyHash: "0xAA77729D3466CA35AE8D28B3BBAC7CC36A5031EFDC430821C02BC31A238AF445", - fees: ethers.utils.parseEther("2"), - }, -}; diff --git a/utils/contractTypes.ts b/utils/contractTypes.ts deleted file mode 100644 index c8325d7ca..000000000 --- a/utils/contractTypes.ts +++ /dev/null @@ -1,15 +0,0 @@ -const contractTypes: string[] = [ - "DropERC20", - "DropERC721", - "DropERC1155", - "TokenERC20", - "TokenERC721", - "TokenERC1155", - "Marketplace", - "VoteERC20", - "Pack", - "Split", - "Multiwrap", -] - -export default contractTypes; \ No newline at end of file diff --git a/utils/meta-tx/autotask.js b/utils/meta-tx/autotask.js deleted file mode 100644 index b40651006..000000000 --- a/utils/meta-tx/autotask.js +++ /dev/null @@ -1,88 +0,0 @@ -const ethers = require("ethers"); -const { DefenderRelaySigner, DefenderRelayProvider } = require("defender-relay-client/lib/ethers"); - -exports.handler = async function (event) { - // Forwarder details - const ForwarderAddress = "0xF2c1F9c70e0E09835ABfD89d7D2E0a9358Cc3A91"; - const ForwarderAbi = [ - { inputs: [], stateMutability: "nonpayable", type: "constructor" }, - { - inputs: [ - { - components: [ - { internalType: "address", name: "from", type: "address" }, - { internalType: "address", name: "to", type: "address" }, - { internalType: "uint256", name: "value", type: "uint256" }, - { internalType: "uint256", name: "gas", type: "uint256" }, - { internalType: "uint256", name: "nonce", type: "uint256" }, - { internalType: "bytes", name: "data", type: "bytes" }, - ], - internalType: "struct MinimalForwarder.ForwardRequest", - name: "req", - type: "tuple", - }, - { internalType: "bytes", name: "signature", type: "bytes" }, - ], - name: "execute", - outputs: [ - { internalType: "bool", name: "", type: "bool" }, - { internalType: "bytes", name: "", type: "bytes" }, - ], - stateMutability: "payable", - type: "function", - }, - { - inputs: [{ internalType: "address", name: "from", type: "address" }], - name: "getNonce", - outputs: [{ internalType: "uint256", name: "", type: "uint256" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - components: [ - { internalType: "address", name: "from", type: "address" }, - { internalType: "address", name: "to", type: "address" }, - { internalType: "uint256", name: "value", type: "uint256" }, - { internalType: "uint256", name: "gas", type: "uint256" }, - { internalType: "uint256", name: "nonce", type: "uint256" }, - { internalType: "bytes", name: "data", type: "bytes" }, - ], - internalType: "struct MinimalForwarder.ForwardRequest", - name: "req", - type: "tuple", - }, - { internalType: "bytes", name: "signature", type: "bytes" }, - ], - name: "verify", - outputs: [{ internalType: "bool", name: "", type: "bool" }], - stateMutability: "view", - type: "function", - }, - ]; - - // Parse webhook payload - if (!event.request || !event.request.body) throw new Error(`Missing payload`); - const { request, signature } = event.request.body; - console.log(`Relaying`, request); - - // Initialize Relayer provider and signer, and forwarder contract - const credentials = { ...event }; - const provider = new DefenderRelayProvider(credentials); - const signer = new DefenderRelaySigner(credentials, provider, { speed: "fast" }); - const forwarder = new ethers.Contract(ForwarderAddress, ForwarderAbi, signer); - - // ===== Relay transaction! ===== - - // Validate request on the forwarder contract - const valid = await forwarder.verify(request, signature); - if (!valid) throw new Error(`Invalid request`); - - // Send meta-tx through relayer to the forwarder contract - const gasLimit = (parseInt(request.gas) + 50000).toString(); - const tx = await forwarder.execute(request, signature, { gasLimit }); - - console.log(`Sent meta-tx: ${tx.hash}`); - return { txHash: tx.hash, receipt: tx }; -}; diff --git a/utils/meta-tx/signer.js b/utils/meta-tx/signer.js deleted file mode 100644 index 7bc130bf1..000000000 --- a/utils/meta-tx/signer.js +++ /dev/null @@ -1,68 +0,0 @@ -const ethSigUtil = require("eth-sig-util"); - -const EIP712Domain = [ - { name: "name", type: "string" }, - { name: "version", type: "string" }, - { name: "chainId", type: "uint256" }, - { name: "verifyingContract", type: "address" }, -]; - -const ForwardRequest = [ - { name: "from", type: "address" }, - { name: "to", type: "address" }, - { name: "value", type: "uint256" }, - { name: "gas", type: "uint256" }, - { name: "nonce", type: "uint256" }, - { name: "data", type: "bytes" }, -]; - -function getMetaTxTypeData(chainId, verifyingContract) { - return { - types: { - EIP712Domain, - ForwardRequest, - }, - domain: { - name: "GSNv2 Forwarder", - version: "0.0.1", - chainId, - verifyingContract, - }, - primaryType: "ForwardRequest", - }; -} - -async function signTypedData(signer, from, data) { - // If signer is a private key, use it to sign - if (typeof signer === "string") { - const privateKey = Buffer.from(signer.replace(/^0x/, ""), "hex"); - return ethSigUtil.signTypedMessage(privateKey, { data }); - } - - const [method, argData] = ["eth_signTypedData_v4", JSON.stringify(data)]; - return await signer.send(method, [from, argData]); -} - -async function buildRequest(forwarder, input) { - const nonce = await forwarder.getNonce(input.from).then(nonce => nonce.toString()); - return { value: 0, gas: 5e6, nonce, ...input }; -} - -async function buildTypedData(forwarder, request) { - const chainId = await forwarder.provider.getNetwork().then(n => n.chainId); - const typeData = getMetaTxTypeData(chainId, forwarder.address); - return { ...typeData, message: request }; -} - -async function signMetaTxRequest(signer, forwarder, input) { - const request = await buildRequest(forwarder, input); - const toSign = await buildTypedData(forwarder, request); - const signature = await signTypedData(signer, input.from, toSign); - return { signature, request }; -} - -module.exports = { - signMetaTxRequest, - buildRequest, - buildTypedData, -}; diff --git a/utils/nativeTokenWrapper.ts b/utils/nativeTokenWrapper.ts deleted file mode 100644 index 355518f75..000000000 --- a/utils/nativeTokenWrapper.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const nativeTokenWrapper: Record = { - 1: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", - 4: "0xc778417E063141139Fce010982780140Aa0cD5Ab", // rinkeby - 5: "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6", // goerli - 137: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", - 80001: "0x9c3C9283D3e44854697Cd22D3Faa240Cfb032889", - 43114: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", - 43113: "0xd00ae08403B9bbb9124bB305C09058E32C39A48c", - 250: "0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83", - 4002: "0xf1277d1Ed8AD466beddF92ef448A132661956621", - 10: "0x4200000000000000000000000000000000000006", // optimism - 69: "0xbC6F6b680bc61e30dB47721c6D1c5cde19C1300d", // optimism testnet - 42161: "0x82af49447d8a07e3bd95bd0d56f35241523fbab1", // arbitrum - 421611: "0xEBbc3452Cc911591e4F18f3b36727Df45d6bd1f9", // arbitrum testnet -}; diff --git a/utils/protocolModules.ts b/utils/protocolModules.ts deleted file mode 100644 index 0b8ae6fce..000000000 --- a/utils/protocolModules.ts +++ /dev/null @@ -1,13 +0,0 @@ -/// @dev Pack protocol module names. -enum ModuleType { - Coin = 0, - NFTCollection = 1, - NFT = 2, - DynamicNFT = 3, - AccessNFT = 4, - Pack = 5, - Market = 6, - Other = 7, -} - -export default ModuleType; diff --git a/utils/tests/gasless.ts b/utils/tests/gasless.ts deleted file mode 100644 index 1e06f0baf..000000000 --- a/utils/tests/gasless.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Types -import { BytesLike } from "@ethersproject/bytes"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { Forwarder } from "../../typechain/Forwarder"; - -// Signature -const { signMetaTxRequest } = require("../meta-tx/signer"); - -type Payload = { - from: string; - to: string; - data: BytesLike; -}; - -export async function sendGaslessTx( - signer: SignerWithAddress, - forwarder: Forwarder, - relayer: SignerWithAddress, - payload: Payload, -) { - // Get params - const { from, to, data } = payload; - - // Sign tx request - const { request, signature } = await signMetaTxRequest(signer.provider, forwarder, { from, to, data }); - - // Call forwarder with signed tx request - await forwarder.connect(relayer).execute(request, signature); -} diff --git a/utils/tests/getContracts.ts b/utils/tests/getContracts.ts deleted file mode 100644 index 3805b15f5..000000000 --- a/utils/tests/getContracts.ts +++ /dev/null @@ -1,306 +0,0 @@ -import { ethers } from "hardhat"; - -// Utils -import { chainlinkVars } from "../../utils/chainlink"; - -// Types -import { BigNumber } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { Log } from "@ethersproject/abstract-provider"; - -// Contract types -import { Forwarder } from "../../typechain/Forwarder"; -import { WETH9 } from "../../typechain/WETH9"; -import { ControlDeployer } from "../../typechain/ControlDeployer"; -import { Registry } from "../../typechain/Registry"; -import { ProtocolControl } from "../../typechain/ProtocolControl"; -import { AccessNFT } from "../../typechain/AccessNFT"; -import { NFT } from "../../typechain/NFT"; -import { Coin } from "../../typechain/Coin"; -import { Pack } from "../../typechain/Pack"; -import { Market } from "../../typechain/Market"; -import { Marketplace } from "../../typechain/Marketplace"; -import { LazyNFT } from "../../typechain/LazyNFT"; -import { LazyMintERC1155 } from "typechain/LazyMintERC1155"; -import { LazyMintERC721 } from "typechain/LazyMintERC721"; -import { LazyMintERC20 } from "typechain/LazyMintERC20"; -import { SignatureMint721 } from "typechain/SignatureMint721"; -import { SignatureMint1155 } from "typechain/SignatureMint1155"; - -export type Contracts = { - registry: Registry; - forwarder: Forwarder; - protocolControl: ProtocolControl; - accessNft: AccessNFT; - coin: Coin; - pack: Pack; - market: Market; - marketv2: Marketplace; - nft: NFT; - lazynft: LazyNFT; - weth: WETH9; - lazyMintERC1155: LazyMintERC1155; - lazyMintERC721: LazyMintERC721; - lazyMintERC20: LazyMintERC20; - sigMint721: SignatureMint721; - sigMint1155: SignatureMint1155; -}; - -export async function getContracts( - protocolProvider: SignerWithAddress, - protocolAdmin: SignerWithAddress, - networkName: string = "rinkeby", -): Promise { - // Deploy Forwarder - const forwarder: Forwarder = (await ethers - .getContractFactory("Forwarder") - .then(f => f.connect(protocolProvider).deploy())) as Forwarder; - - // Deploy ControlDeployer - const controlDeployer: ControlDeployer = (await ethers - .getContractFactory("ControlDeployer") - .then(f => f.connect(protocolProvider).deploy())) as ControlDeployer; - - // Deploy Registry - const registry: Registry = (await ethers.getContractFactory("Registry").then(f => - f.connect(protocolProvider).deploy( - protocolProvider.address, // Protocol provider treasury. - forwarder.address, // Forwarder address. - controlDeployer.address, // ControlDeployer address. - ), - )) as Registry; - - // Grant `REGISTRY_ROLE` in ControlDeployer, to Registry. - const REGISTRY_ROLE = await controlDeployer.REGISTRY_ROLE(); - await controlDeployer.connect(protocolProvider).grantRole(REGISTRY_ROLE, registry.address); - - // Deploy ProtocolControl via Registry. - const protocolControlURI: string = ""; - const deployReceipt = await registry - .connect(protocolAdmin) - .deployProtocol(protocolControlURI) - .then(tx => tx.wait()); - - // Get ProtocolControl address - const log = deployReceipt.logs.find( - x => x.topics.indexOf(registry.interface.getEventTopic("NewProtocolControl")) >= 0, - ); - const protocolControlAddr: string = registry.interface.parseLog(log as Log).args.controlAddress; - - // Get ProtocolControl contract. - const protocolControl: ProtocolControl = (await ethers.getContractAt( - "ProtocolControl", - protocolControlAddr, - )) as ProtocolControl; - - // Deploy Pack - const { vrfCoordinator, linkTokenAddress, keyHash, fees } = chainlinkVars[networkName as keyof typeof chainlinkVars]; - const packContractURI: string = ""; - const pack: Pack = (await ethers - .getContractFactory("Pack") - .then(f => - f - .connect(protocolAdmin) - .deploy( - protocolControl.address, - packContractURI, - vrfCoordinator, - linkTokenAddress, - keyHash, - fees, - forwarder.address, - 0, - ), - )) as Pack; - - // Deploy Market - const marketContractURI: string = ""; - const market: Market = (await ethers - .getContractFactory("Market") - .then(f => - f.connect(protocolAdmin).deploy(protocolControl.address, forwarder.address, marketContractURI, 0), - )) as Market; - - // Deploy WETH - const weth: WETH9 = await ethers.getContractFactory("WETH9").then(f => f.deploy()); - - // Deploy Marketplace - const marketv2: Marketplace = (await ethers - .getContractFactory("Marketplace") - .then(f => - f.connect(protocolAdmin).deploy(protocolControl.address, forwarder.address, weth.address, marketContractURI, 0), - )) as Marketplace; - - // Deploy LazyMintERC1155 - const contractURI: string = "ipfs://contractURI/"; - const trustedForwarderAddr: string = forwarder.address; - const nativeTokenWrapperAddr: string = weth.address; - const defaultSaleRecipient: string = protocolAdmin.address; - const royaltyBps: BigNumber = BigNumber.from(0); - const feeBps: BigNumber = BigNumber.from(0); - - const lazyMintERC1155: LazyMintERC1155 = (await ethers - .getContractFactory("LazyMintERC1155") - .then(f => - f - .connect(protocolAdmin) - .deploy( - contractURI, - protocolControl.address, - trustedForwarderAddr, - nativeTokenWrapperAddr, - defaultSaleRecipient, - royaltyBps, - feeBps, - ), - )) as LazyMintERC1155; - - // Deploy LazyMintERC721 - const name_lazyMintERC721: string = "LazyMintERC721"; - const symbol_lazyMintERC721: string = "LAZY"; - - const lazyMintERC721: LazyMintERC721 = (await ethers - .getContractFactory("LazyMintERC721") - .then(f => - f - .connect(protocolAdmin) - .deploy( - name_lazyMintERC721, - symbol_lazyMintERC721, - contractURI, - protocolControl.address, - trustedForwarderAddr, - nativeTokenWrapperAddr, - defaultSaleRecipient, - royaltyBps, - feeBps, - ), - )) as LazyMintERC721; - - // Deploy LazyMintERC20 - - const name_lazyMintERC20: string = "Lazy token"; - const symbol_lazyMintERC20: string = "LAZY"; - - const lazyMintERC20: LazyMintERC20 = (await ethers - .getContractFactory("LazyMintERC20") - .then(f => - f - .connect(protocolAdmin) - .deploy( - name_lazyMintERC20, - symbol_lazyMintERC20, - contractURI, - protocolControl.address, - trustedForwarderAddr, - nativeTokenWrapperAddr, - defaultSaleRecipient, - royaltyBps, - feeBps, - ), - )) as LazyMintERC20; - - // Deploy SignatureMint721 - const name_sigMint721: string = "SignatureMint721"; - const symbol_sigMint721: string = "SIGMINT"; - - const sigMint721: SignatureMint721 = (await ethers - .getContractFactory("SignatureMint721") - .then(f => - f - .connect(protocolAdmin) - .deploy( - name_sigMint721, - symbol_sigMint721, - contractURI, - protocolControl.address, - trustedForwarderAddr, - nativeTokenWrapperAddr, - defaultSaleRecipient, - royaltyBps, - feeBps, - ), - )) as SignatureMint721; - - // Deploy SignatureMint1155 - const sigMint1155: SignatureMint1155 = (await ethers - .getContractFactory("SignatureMint1155") - .then(f => - f - .connect(protocolAdmin) - .deploy( - contractURI, - protocolControl.address, - trustedForwarderAddr, - nativeTokenWrapperAddr, - defaultSaleRecipient, - royaltyBps, - feeBps, - ), - )) as SignatureMint1155; - - // Deploy AccessNFT - const accessNFTContractURI: string = ""; - const accessNft: AccessNFT = (await ethers - .getContractFactory("AccessNFT") - .then(f => - f.connect(protocolAdmin).deploy(protocolControl.address, forwarder.address, accessNFTContractURI, 0), - )) as AccessNFT; - - // Get NFT contract - const name: string = "name"; - const symbol: string = "SYMBOL"; - const nftContractURI: string = ""; - const nft: NFT = (await ethers - .getContractFactory("NFT") - .then(f => - f.connect(protocolAdmin).deploy(protocolControl.address, name, symbol, forwarder.address, nftContractURI, 0), - )) as NFT; - - // Deploy Coin - const coinName = "name"; - const coinSymbol = "SYMBOL"; - const coinURI = ""; - - const coin: Coin = (await ethers - .getContractFactory("Coin") - .then(f => f.connect(protocolAdmin).deploy(coinName, coinSymbol, forwarder.address, coinURI))) as Coin; - - // Get NFT contract - const lazyContractURI: string = ""; - const lazyBaseURI: string = "ipfs://baseuri/"; - const lazyMaxSupply = 420; - const lazynft: LazyNFT = (await ethers.getContractFactory("LazyNFT").then(f => - f.connect(protocolAdmin).deploy( - protocolControl.address, - name, - symbol, - forwarder.address, - lazyContractURI, - lazyBaseURI, - lazyMaxSupply, - 0, // royalty - 0, // fee - protocolControl.address, // sale recipient - ), - )) as LazyNFT; - - return { - registry, - weth, - forwarder, - protocolControl, - pack, - market, - marketv2, - accessNft, - coin, - nft, - lazynft, - lazyMintERC1155, - lazyMintERC721, - lazyMintERC20, - sigMint721, - sigMint1155, - }; -} diff --git a/utils/tests/hardhatFork.ts b/utils/tests/hardhatFork.ts deleted file mode 100644 index a57db3c82..000000000 --- a/utils/tests/hardhatFork.ts +++ /dev/null @@ -1,39 +0,0 @@ -// from: https://github.com/ethereumvex/SushiMaker-bridge-exploit/blob/master/utils/utils.js -import hre from "hardhat"; -import { chainIds } from "../chainIds"; - -const ethers = hre.ethers; -require("dotenv").config(); - -const defaultForkBlock = 9414004; // randomly set - -export const forkFrom = async (network: keyof typeof chainIds) => { - let alchemyKey: string = process.env.ALCHEMY_KEY || ""; - - let nodeUrl: string = - chainIds[network] == 137 || chainIds[network] == 80001 - ? network == "polygon" - ? `https://polygon-mainnet.g.alchemy.com/v2/${alchemyKey}` - : `https://polygon-mumbai.g.alchemy.com/v2/${alchemyKey}` - : `https://eth-${network}.alchemyapi.io/v2/${alchemyKey}`; - - await hre.network.provider.request({ - method: "hardhat_reset", - params: [ - { - forking: { - jsonRpcUrl: nodeUrl, - blockNumber: defaultForkBlock, - }, - }, - ], - }); -}; - -export const impersonate = async function getImpersonatedSigner(address: any) { - await hre.network.provider.request({ - method: "hardhat_impersonateAccount", - params: [address], - }); - return ethers.provider.getSigner(address); -}; diff --git a/utils/tests/params.ts b/utils/tests/params.ts deleted file mode 100644 index 09c30c301..000000000 --- a/utils/tests/params.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { BigNumber } from "ethers"; -import { ethers } from "hardhat"; - -export function getURIs(num: number = 0): string[] { - const numToReturn = num == 0 ? 5 + Math.floor(Math.random() * 5) : num; - - const step = 1 + Math.floor(Math.random() * 100); - const masterURI: string = "ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/"; - return [...Array(numToReturn).keys()].map((val: number) => masterURI + (val + step).toString()); -} - -export function getAmounts(num: number): number[] { - return [...Array(num).keys()].map(val => 1 + Math.floor(Math.random() * 100)); -} - -export function getAmountBounded(max: number): BigNumber { - const amount = Math.floor(Math.random() * max); - return amount == 0 ? BigNumber.from(max) : BigNumber.from(amount); -} - -export function getBoundedEtherAmount(): BigNumber { - return ethers.utils.parseEther((1 + Math.random()).toString()); -} diff --git a/utils/txOptions.ts b/utils/txOptions.ts deleted file mode 100644 index 5005df937..000000000 --- a/utils/txOptions.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ethers } from "hardhat"; - -export const txOptions = { - polygon: { - gasPrice: ethers.utils.parseUnits("35", "gwei"), - }, - - matic: { - gasPrice: ethers.utils.parseUnits("35", "gwei"), - }, - - mumbai: { - gasPrice: ethers.utils.parseUnits("35", "gwei"), - }, - - rinkeby: { - gasPrice: ethers.utils.parseUnits("10", "gwei"), - }, - - localhost: { - gasPrice: ethers.utils.parseUnits("10", "gwei"), - }, -}; diff --git a/utils/xor.ts b/utils/xor.ts deleted file mode 100644 index 4e75473c1..000000000 --- a/utils/xor.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { ethers } from "ethers"; - -/** - function encryptDecrypt(bytes memory data, bytes memory key) public pure returns (bytes memory result) { - // Store data length on stack for later use - uint256 length = data.length; - - assembly { - // Set result to free memory pointer - result := mload(0x40) - // Increase free memory pointer by lenght + 32 - mstore(0x40, add(add(result, length), 32)) - // Set result length - mstore(result, length) - } - - // Iterate over the data stepping by 32 bytes - for (uint256 i = 0; i < length; i += 32) { - // Generate hash of the key and offset - bytes32 hash = keccak256(abi.encodePacked(key, i)); - - bytes32 chunk; - assembly { - // Read 32-bytes data chunk - chunk := mload(add(data, add(i, 32))) - } - // XOR the chunk with hash - chunk ^= hash; - assembly { - // Write 32-byte encrypted chunk - mstore(add(result, add(i, 32)), chunk) - } - } - } -*/ - -function xor(data: Uint8Array, key: Uint8Array) { - const len = data.length; - const result = []; - for (let i = 0; i < len; i += 32) { - const hash = ethers.utils.solidityKeccak256(["bytes", "uint256"], [key, i]); - const slice = data.slice(i, i + 32); - const hashsliced = ethers.utils.arrayify(hash).slice(0, slice.length); // weird that we need to slice the hash - const chunk = ethers.BigNumber.from(slice).xor(hashsliced); - result.push(chunk); - } - return `0x${result.map(chunk => chunk.toHexString().substring(2)).join("")}`; -} - -async function run() { - //console.log(ethers.utils.id("hello")); - const text = "ipfs://secret_this_is_a_super_long_ipfs_url_maybe_you_will_find_it_useful/"; - //const expected = - //"0xe23d08b128c4405cfd8e350d383a52b3ad42cb244328b3bdfbc58aee4d7221cbab5bc7321dd0006044e5e7392dc8807dc5d2bd231d251067fc445467064ee964f41e4d35a062574422b8"; - //const x = xor(ethers.utils.toUtf8Bytes(text), ethers.utils.toUtf8Bytes("secret")); -} - -run(); diff --git a/yarn.lock b/yarn.lock index 4aed127f2..b446e0d3c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,590 +2,493 @@ # yarn lockfile v1 +"@aashutoshrathi/word-wrap@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" + integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== + "@babel/code-frame@^7.0.0": - version "7.16.7" - resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz" - integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" + integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== dependencies: - "@babel/highlight" "^7.16.7" + "@babel/highlight" "^7.23.4" + chalk "^2.4.2" -"@babel/helper-validator-identifier@^7.16.7": - version "7.16.7" - resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz" - integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== -"@babel/highlight@^7.16.7": - version "7.17.9" - resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz" - integrity sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg== +"@babel/highlight@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b" + integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A== dependencies: - "@babel/helper-validator-identifier" "^7.16.7" - chalk "^2.0.0" + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" js-tokens "^4.0.0" -"@chainlink/contracts@^0.4.0": - version "0.4.0" - resolved "https://registry.npmjs.org/@chainlink/contracts/-/contracts-0.4.0.tgz" - integrity sha512-yZGeCBd7d+qxfw9r/JxtPzsW2kCc6MorPRZ/tDKnaJI98H99j5P2Fosfehmcwk6wVZlz+0Bp4kS1y480nw3Zow== - -"@cspotcode/source-map-consumer@0.8.0": - version "0.8.0" - resolved "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz" - integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== - -"@cspotcode/source-map-support@0.7.0": - version "0.7.0" - resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz" - integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== +"@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: - "@cspotcode/source-map-consumer" "0.8.0" + "@jridgewell/trace-mapping" "0.3.9" -"@ensdomains/ens@^0.4.4": - version "0.4.5" - resolved "https://registry.npmjs.org/@ensdomains/ens/-/ens-0.4.5.tgz" - integrity sha512-JSvpj1iNMFjK6K+uVl4unqMoa9rf5jopb8cya5UGBWz23Nw8hSNT7efgUx4BTlAPAgpNlEioUfeTyQ6J9ZvTVw== +"@esbuild/linux-loong64@0.14.54": + version "0.14.54" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028" + integrity sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw== + +"@eslint-community/eslint-utils@^4.2.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== dependencies: - bluebird "^3.5.2" - eth-ens-namehash "^2.0.8" - solc "^0.4.20" - testrpc "0.0.1" - web3-utils "^1.0.0-beta.31" + eslint-visitor-keys "^3.3.0" -"@ensdomains/resolver@^0.2.4": - version "0.2.4" - resolved "https://registry.npmjs.org/@ensdomains/resolver/-/resolver-0.2.4.tgz" - integrity sha512-bvaTH34PMCbv6anRa9I/0zjLJgY4EuznbEMgbV77JBCQ9KNC46rzi0avuxpOfu+xDjPEtSFGqVEOr5GlUSGudA== +"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" + integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== -"@eslint/eslintrc@^1.2.1": - version "1.2.1" - resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz" - integrity sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ== +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.3.1" - globals "^13.9.0" + espree "^9.6.0" + globals "^13.19.0" ignore "^5.2.0" import-fresh "^3.2.1" js-yaml "^4.1.0" - minimatch "^3.0.4" + minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@ethereum-waffle/chai@^3.4.4": - version "3.4.4" - resolved "https://registry.npmjs.org/@ethereum-waffle/chai/-/chai-3.4.4.tgz" - integrity sha512-/K8czydBtXXkcM9X6q29EqEkc5dN3oYenyH2a9hF7rGAApAJUpH8QBtojxOY/xQ2up5W332jqgxwp0yPiYug1g== - dependencies: - "@ethereum-waffle/provider" "^3.4.4" - ethers "^5.5.2" - -"@ethereum-waffle/compiler@^3.4.4": - version "3.4.4" - resolved "https://registry.npmjs.org/@ethereum-waffle/compiler/-/compiler-3.4.4.tgz" - integrity sha512-RUK3axJ8IkD5xpWjWoJgyHclOeEzDLQFga6gKpeGxiS/zBu+HB0W2FvsrrLalTFIaPw/CGYACRBSIxqiCqwqTQ== - dependencies: - "@resolver-engine/imports" "^0.3.3" - "@resolver-engine/imports-fs" "^0.3.3" - "@typechain/ethers-v5" "^2.0.0" - "@types/mkdirp" "^0.5.2" - "@types/node-fetch" "^2.5.5" - ethers "^5.0.1" - mkdirp "^0.5.1" - node-fetch "^2.6.1" - solc "^0.6.3" - ts-generator "^0.1.1" - typechain "^3.0.0" - -"@ethereum-waffle/ens@^3.4.4": - version "3.4.4" - resolved "https://registry.npmjs.org/@ethereum-waffle/ens/-/ens-3.4.4.tgz" - integrity sha512-0m4NdwWxliy3heBYva1Wr4WbJKLnwXizmy5FfSSr5PMbjI7SIGCdCB59U7/ZzY773/hY3bLnzLwvG5mggVjJWg== - dependencies: - "@ensdomains/ens" "^0.4.4" - "@ensdomains/resolver" "^0.2.4" - ethers "^5.5.2" - -"@ethereum-waffle/mock-contract@^3.4.4": - version "3.4.4" - resolved "https://registry.npmjs.org/@ethereum-waffle/mock-contract/-/mock-contract-3.4.4.tgz" - integrity sha512-Mp0iB2YNWYGUV+VMl5tjPsaXKbKo8MDH9wSJ702l9EBjdxFf/vBvnMBAC1Fub1lLtmD0JHtp1pq+mWzg/xlLnA== - dependencies: - "@ethersproject/abi" "^5.5.0" - ethers "^5.5.2" - -"@ethereum-waffle/provider@^3.4.4": - version "3.4.4" - resolved "https://registry.npmjs.org/@ethereum-waffle/provider/-/provider-3.4.4.tgz" - integrity sha512-GK8oKJAM8+PKy2nK08yDgl4A80mFuI8zBkE0C9GqTRYQqvuxIyXoLmJ5NZU9lIwyWVv5/KsoA11BgAv2jXE82g== - dependencies: - "@ethereum-waffle/ens" "^3.4.4" - ethers "^5.5.2" - ganache-core "^2.13.2" - patch-package "^6.2.2" - postinstall-postinstall "^2.1.0" - -"@ethereumjs/block@^3.5.0", "@ethereumjs/block@^3.6.0", "@ethereumjs/block@^3.6.2": - version "3.6.2" - resolved "https://registry.npmjs.org/@ethereumjs/block/-/block-3.6.2.tgz" - integrity sha512-mOqYWwMlAZpYUEOEqt7EfMFuVL2eyLqWWIzcf4odn6QgXY8jBI2NhVuJncrMCKeMZrsJAe7/auaRRB6YcdH+Qw== - dependencies: - "@ethereumjs/common" "^2.6.3" - "@ethereumjs/tx" "^3.5.1" - ethereumjs-util "^7.1.4" - merkle-patricia-tree "^4.2.4" - -"@ethereumjs/blockchain@^5.5.0", "@ethereumjs/blockchain@^5.5.2": - version "5.5.2" - resolved "https://registry.npmjs.org/@ethereumjs/blockchain/-/blockchain-5.5.2.tgz" - integrity sha512-Jz26iJmmsQtngerW6r5BDFaew/f2mObLrRZo3rskLOx1lmtMZ8+TX/vJexmivrnWgmAsTdNWhlKUYY4thPhPig== - dependencies: - "@ethereumjs/block" "^3.6.2" - "@ethereumjs/common" "^2.6.3" - "@ethereumjs/ethash" "^1.1.0" - debug "^4.3.3" - ethereumjs-util "^7.1.4" - level-mem "^5.0.1" - lru-cache "^5.1.1" - semaphore-async-await "^1.5.1" - -"@ethereumjs/common@^2.6.0", "@ethereumjs/common@^2.6.3": - version "2.6.3" - resolved "https://registry.npmjs.org/@ethereumjs/common/-/common-2.6.3.tgz" - integrity sha512-mQwPucDL7FDYIg9XQ8DL31CnIYZwGhU5hyOO5E+BMmT71G0+RHvIT5rIkLBirJEKxV6+Rcf9aEIY0kXInxUWpQ== - dependencies: - crc-32 "^1.2.0" - ethereumjs-util "^7.1.4" - -"@ethereumjs/ethash@^1.1.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@ethereumjs/ethash/-/ethash-1.1.0.tgz" - integrity sha512-/U7UOKW6BzpA+Vt+kISAoeDie1vAvY4Zy2KF5JJb+So7+1yKmJeJEHOGSnQIj330e9Zyl3L5Nae6VZyh2TJnAA== - dependencies: - "@ethereumjs/block" "^3.5.0" - "@types/levelup" "^4.3.0" - buffer-xor "^2.0.1" - ethereumjs-util "^7.1.1" - miller-rabin "^4.0.0" - -"@ethereumjs/tx@^3.4.0", "@ethereumjs/tx@^3.5.1": - version "3.5.1" - resolved "https://registry.npmjs.org/@ethereumjs/tx/-/tx-3.5.1.tgz" - integrity sha512-xzDrTiu4sqZXUcaBxJ4n4W5FrppwxLxZB4ZDGVLtxSQR4lVuOnFR6RcUHdg1mpUhAPVrmnzLJpxaeXnPxIyhWA== - dependencies: - "@ethereumjs/common" "^2.6.3" - ethereumjs-util "^7.1.4" - -"@ethereumjs/vm@^5.6.0": - version "5.8.0" - resolved "https://registry.npmjs.org/@ethereumjs/vm/-/vm-5.8.0.tgz" - integrity sha512-mn2G2SX79QY4ckVvZUfxlNUpzwT2AEIkvgJI8aHoQaNYEHhH8rmdVDIaVVgz6//PjK52BZsK23afz+WvSR0Qqw== - dependencies: - "@ethereumjs/block" "^3.6.2" - "@ethereumjs/blockchain" "^5.5.2" - "@ethereumjs/common" "^2.6.3" - "@ethereumjs/tx" "^3.5.1" - async-eventemitter "^0.2.4" - core-js-pure "^3.0.1" - debug "^4.3.3" - ethereumjs-util "^7.1.4" - functional-red-black-tree "^1.0.1" - mcl-wasm "^0.7.1" - merkle-patricia-tree "^4.2.4" - rustbn.js "~0.2.0" - -"@ethersproject/abi@5.0.0-beta.153": - version "5.0.0-beta.153" - resolved "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.0.0-beta.153.tgz" - integrity sha512-aXweZ1Z7vMNzJdLpR1CZUAIgnwjrZeUSvN9syCwlBaEBUFJmFY+HHnfuTI5vIhVs/mRkfJVrbEyl51JZQqyjAg== - dependencies: - "@ethersproject/address" ">=5.0.0-beta.128" - "@ethersproject/bignumber" ">=5.0.0-beta.130" - "@ethersproject/bytes" ">=5.0.0-beta.129" - "@ethersproject/constants" ">=5.0.0-beta.128" - "@ethersproject/hash" ">=5.0.0-beta.128" - "@ethersproject/keccak256" ">=5.0.0-beta.127" - "@ethersproject/logger" ">=5.0.0-beta.129" - "@ethersproject/properties" ">=5.0.0-beta.131" - "@ethersproject/strings" ">=5.0.0-beta.130" - -"@ethersproject/abi@5.6.0", "@ethersproject/abi@^5.0.0-beta.146", "@ethersproject/abi@^5.1.2", "@ethersproject/abi@^5.5.0", "@ethersproject/abi@^5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.6.0.tgz" - integrity sha512-AhVByTwdXCc2YQ20v300w6KVHle9g2OFc28ZAFCPnJyEpkv1xKXjZcSTgWOlv1i+0dqlgF8RCF2Rn2KC1t+1Vg== - dependencies: - "@ethersproject/address" "^5.6.0" - "@ethersproject/bignumber" "^5.6.0" - "@ethersproject/bytes" "^5.6.0" - "@ethersproject/constants" "^5.6.0" - "@ethersproject/hash" "^5.6.0" - "@ethersproject/keccak256" "^5.6.0" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/properties" "^5.6.0" - "@ethersproject/strings" "^5.6.0" - -"@ethersproject/abstract-provider@5.6.0", "@ethersproject/abstract-provider@^5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.6.0.tgz" - integrity sha512-oPMFlKLN+g+y7a79cLK3WiLcjWFnZQtXWgnLAbHZcN3s7L4v90UHpTOrLk+m3yr0gt+/h9STTM6zrr7PM8uoRw== - dependencies: - "@ethersproject/bignumber" "^5.6.0" - "@ethersproject/bytes" "^5.6.0" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/networks" "^5.6.0" - "@ethersproject/properties" "^5.6.0" - "@ethersproject/transactions" "^5.6.0" - "@ethersproject/web" "^5.6.0" - -"@ethersproject/abstract-signer@5.6.0", "@ethersproject/abstract-signer@^5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.6.0.tgz" - integrity sha512-WOqnG0NJKtI8n0wWZPReHtaLkDByPL67tn4nBaDAhmVq8sjHTPbCdz4DRhVu/cfTOvfy9w3iq5QZ7BX7zw56BQ== - dependencies: - "@ethersproject/abstract-provider" "^5.6.0" - "@ethersproject/bignumber" "^5.6.0" - "@ethersproject/bytes" "^5.6.0" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/properties" "^5.6.0" - -"@ethersproject/address@5.6.0", "@ethersproject/address@>=5.0.0-beta.128", "@ethersproject/address@^5.0.2", "@ethersproject/address@^5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/address/-/address-5.6.0.tgz" - integrity sha512-6nvhYXjbXsHPS+30sHZ+U4VMagFC/9zAk6Gd/h3S21YW4+yfb0WfRtaAIZ4kfM4rrVwqiy284LP0GtL5HXGLxQ== - dependencies: - "@ethersproject/bignumber" "^5.6.0" - "@ethersproject/bytes" "^5.6.0" - "@ethersproject/keccak256" "^5.6.0" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/rlp" "^5.6.0" - -"@ethersproject/base64@5.6.0", "@ethersproject/base64@^5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.6.0.tgz" - integrity sha512-2Neq8wxJ9xHxCF9TUgmKeSh9BXJ6OAxWfeGWvbauPh8FuHEjamgHilllx8KkSd5ErxyHIX7Xv3Fkcud2kY9ezw== - dependencies: - "@ethersproject/bytes" "^5.6.0" - -"@ethersproject/basex@5.6.0", "@ethersproject/basex@^5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.6.0.tgz" - integrity sha512-qN4T+hQd/Md32MoJpc69rOwLYRUXwjTlhHDIeUkUmiN/JyWkkLLMoG0TqvSQKNqZOMgN5stbUYN6ILC+eD7MEQ== - dependencies: - "@ethersproject/bytes" "^5.6.0" - "@ethersproject/properties" "^5.6.0" - -"@ethersproject/bignumber@5.6.0", "@ethersproject/bignumber@>=5.0.0-beta.130", "@ethersproject/bignumber@^5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.6.0.tgz" - integrity sha512-VziMaXIUHQlHJmkv1dlcd6GY2PmT0khtAqaMctCIDogxkrarMzA9L94KN1NeXqqOfFD6r0sJT3vCTOFSmZ07DA== - dependencies: - "@ethersproject/bytes" "^5.6.0" - "@ethersproject/logger" "^5.6.0" - bn.js "^4.11.9" - -"@ethersproject/bytes@5.6.1", "@ethersproject/bytes@>=5.0.0-beta.129", "@ethersproject/bytes@^5.6.0": - version "5.6.1" - resolved "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.6.1.tgz" - integrity sha512-NwQt7cKn5+ZE4uDn+X5RAXLp46E1chXoaMmrxAyA0rblpxz8t58lVkrHXoRIn0lz1joQElQ8410GqhTqMOwc6g== - dependencies: - "@ethersproject/logger" "^5.6.0" - -"@ethersproject/constants@5.6.0", "@ethersproject/constants@>=5.0.0-beta.128", "@ethersproject/constants@^5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.6.0.tgz" - integrity sha512-SrdaJx2bK0WQl23nSpV/b1aq293Lh0sUaZT/yYKPDKn4tlAbkH96SPJwIhwSwTsoQQZxuh1jnqsKwyymoiBdWA== - dependencies: - "@ethersproject/bignumber" "^5.6.0" - -"@ethersproject/contracts@5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.6.0.tgz" - integrity sha512-74Ge7iqTDom0NX+mux8KbRUeJgu1eHZ3iv6utv++sLJG80FVuU9HnHeKVPfjd9s3woFhaFoQGf3B3iH/FrQmgw== - dependencies: - "@ethersproject/abi" "^5.6.0" - "@ethersproject/abstract-provider" "^5.6.0" - "@ethersproject/abstract-signer" "^5.6.0" - "@ethersproject/address" "^5.6.0" - "@ethersproject/bignumber" "^5.6.0" - "@ethersproject/bytes" "^5.6.0" - "@ethersproject/constants" "^5.6.0" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/properties" "^5.6.0" - "@ethersproject/transactions" "^5.6.0" - -"@ethersproject/hash@5.6.0", "@ethersproject/hash@>=5.0.0-beta.128", "@ethersproject/hash@^5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.6.0.tgz" - integrity sha512-fFd+k9gtczqlr0/BruWLAu7UAOas1uRRJvOR84uDf4lNZ+bTkGl366qvniUZHKtlqxBRU65MkOobkmvmpHU+jA== - dependencies: - "@ethersproject/abstract-signer" "^5.6.0" - "@ethersproject/address" "^5.6.0" - "@ethersproject/bignumber" "^5.6.0" - "@ethersproject/bytes" "^5.6.0" - "@ethersproject/keccak256" "^5.6.0" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/properties" "^5.6.0" - "@ethersproject/strings" "^5.6.0" - -"@ethersproject/hdnode@5.6.0", "@ethersproject/hdnode@^5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.6.0.tgz" - integrity sha512-61g3Jp3nwDqJcL/p4nugSyLrpl/+ChXIOtCEM8UDmWeB3JCAt5FoLdOMXQc3WWkc0oM2C0aAn6GFqqMcS/mHTw== - dependencies: - "@ethersproject/abstract-signer" "^5.6.0" - "@ethersproject/basex" "^5.6.0" - "@ethersproject/bignumber" "^5.6.0" - "@ethersproject/bytes" "^5.6.0" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/pbkdf2" "^5.6.0" - "@ethersproject/properties" "^5.6.0" - "@ethersproject/sha2" "^5.6.0" - "@ethersproject/signing-key" "^5.6.0" - "@ethersproject/strings" "^5.6.0" - "@ethersproject/transactions" "^5.6.0" - "@ethersproject/wordlists" "^5.6.0" - -"@ethersproject/json-wallets@5.6.0", "@ethersproject/json-wallets@^5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.6.0.tgz" - integrity sha512-fmh86jViB9r0ibWXTQipxpAGMiuxoqUf78oqJDlCAJXgnJF024hOOX7qVgqsjtbeoxmcLwpPsXNU0WEe/16qPQ== - dependencies: - "@ethersproject/abstract-signer" "^5.6.0" - "@ethersproject/address" "^5.6.0" - "@ethersproject/bytes" "^5.6.0" - "@ethersproject/hdnode" "^5.6.0" - "@ethersproject/keccak256" "^5.6.0" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/pbkdf2" "^5.6.0" - "@ethersproject/properties" "^5.6.0" - "@ethersproject/random" "^5.6.0" - "@ethersproject/strings" "^5.6.0" - "@ethersproject/transactions" "^5.6.0" +"@eslint/js@8.56.0": + version "8.56.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.56.0.tgz#ef20350fec605a7f7035a01764731b2de0f3782b" + integrity sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A== + +"@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.6.0", "@ethersproject/keccak256@>=5.0.0-beta.127", "@ethersproject/keccak256@^5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.6.0.tgz" - integrity sha512-tk56BJ96mdj/ksi7HWZVWGjCq0WVl/QvfhFQNeL8fxhBlGoP+L80uDCiQcpJPd+2XxkivS3lwRm3E0CXTfol0w== +"@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.6.0" + "@ethersproject/bytes" "^5.7.0" js-sha3 "0.8.0" -"@ethersproject/logger@5.6.0", "@ethersproject/logger@>=5.0.0-beta.129", "@ethersproject/logger@^5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.6.0.tgz" - integrity sha512-BiBWllUROH9w+P21RzoxJKzqoqpkyM1pRnEKG69bulE9TSQD8SAIvTQqIMZmmCO8pUNkgLP1wndX1gKghSpBmg== - -"@ethersproject/networks@5.6.1", "@ethersproject/networks@^5.6.0": - version "5.6.1" - resolved "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.6.1.tgz" - integrity sha512-b2rrupf3kCTcc3jr9xOWBuHylSFtbpJf79Ga7QR98ienU2UqGimPGEsYMgbI29KHJfA5Us89XwGVmxrlxmSrMg== - dependencies: - "@ethersproject/logger" "^5.6.0" - -"@ethersproject/pbkdf2@5.6.0", "@ethersproject/pbkdf2@^5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.6.0.tgz" - integrity sha512-Wu1AxTgJo3T3H6MIu/eejLFok9TYoSdgwRr5oGY1LTLfmGesDoSx05pemsbrPT2gG4cQME+baTSCp5sEo2erZQ== - dependencies: - "@ethersproject/bytes" "^5.6.0" - "@ethersproject/sha2" "^5.6.0" - -"@ethersproject/properties@5.6.0", "@ethersproject/properties@>=5.0.0-beta.131", "@ethersproject/properties@^5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.6.0.tgz" - integrity sha512-szoOkHskajKePTJSZ46uHUWWkbv7TzP2ypdEK6jGMqJaEt2sb0jCgfBo0gH0m2HBpRixMuJ6TBRaQCF7a9DoCg== - dependencies: - "@ethersproject/logger" "^5.6.0" - -"@ethersproject/providers@5.6.2": - version "5.6.2" - resolved "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.6.2.tgz" - integrity sha512-6/EaFW/hNWz+224FXwl8+HdMRzVHt8DpPmu5MZaIQqx/K/ELnC9eY236SMV7mleCM3NnEArFwcAAxH5kUUgaRg== - dependencies: - "@ethersproject/abstract-provider" "^5.6.0" - "@ethersproject/abstract-signer" "^5.6.0" - "@ethersproject/address" "^5.6.0" - "@ethersproject/basex" "^5.6.0" - "@ethersproject/bignumber" "^5.6.0" - "@ethersproject/bytes" "^5.6.0" - "@ethersproject/constants" "^5.6.0" - "@ethersproject/hash" "^5.6.0" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/networks" "^5.6.0" - "@ethersproject/properties" "^5.6.0" - "@ethersproject/random" "^5.6.0" - "@ethersproject/rlp" "^5.6.0" - "@ethersproject/sha2" "^5.6.0" - "@ethersproject/strings" "^5.6.0" - "@ethersproject/transactions" "^5.6.0" - "@ethersproject/web" "^5.6.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": + 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.6.0", "@ethersproject/random@^5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/random/-/random-5.6.0.tgz" - integrity sha512-si0PLcLjq+NG/XHSZz90asNf+YfKEqJGVdxoEkSukzbnBgC8rydbgbUgBbBGLeHN4kAJwUFEKsu3sCXT93YMsw== +"@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.6.0" - "@ethersproject/logger" "^5.6.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" -"@ethersproject/rlp@5.6.0", "@ethersproject/rlp@^5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.6.0.tgz" - integrity sha512-dz9WR1xpcTL+9DtOT/aDO+YyxSSdO8YIS0jyZwHHSlAmnxA6cKU3TrTd4Xc/bHayctxTgGLYNuVVoiXE4tTq1g== +"@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.6.0" - "@ethersproject/logger" "^5.6.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" -"@ethersproject/sha2@5.6.0", "@ethersproject/sha2@^5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.6.0.tgz" - integrity sha512-1tNWCPFLu1n3JM9t4/kytz35DkuF9MxqkGGEHNauEbaARdm2fafnOyw1s0tIQDPKF/7bkP1u3dbrmjpn5CelyA== +"@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.6.0" - "@ethersproject/logger" "^5.6.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" hash.js "1.1.7" -"@ethersproject/signing-key@5.6.0", "@ethersproject/signing-key@^5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.6.0.tgz" - integrity sha512-S+njkhowmLeUu/r7ir8n78OUKx63kBdMCPssePS89So1TH4hZqnWFsThEd/GiXYp9qMxVrydf7KdM9MTGPFukA== +"@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.6.0" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/properties" "^5.6.0" - bn.js "^4.11.9" + "@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.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.6.0.tgz" - integrity sha512-YwF52vTNd50kjDzqKaoNNbC/r9kMDPq3YzDWmsjFTRBcIF1y4JCQJ8gB30wsTfHbaxgxelI5BfxQSxD/PbJOww== - dependencies: - "@ethersproject/bignumber" "^5.6.0" - "@ethersproject/bytes" "^5.6.0" - "@ethersproject/keccak256" "^5.6.0" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/sha2" "^5.6.0" - "@ethersproject/strings" "^5.6.0" - -"@ethersproject/strings@5.6.0", "@ethersproject/strings@>=5.0.0-beta.130", "@ethersproject/strings@^5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.6.0.tgz" - integrity sha512-uv10vTtLTZqrJuqBZR862ZQjTIa724wGPWQqZrofaPI/kUsf53TBG0I0D+hQ1qyNtllbNzaW+PDPHHUI6/65Mg== - dependencies: - "@ethersproject/bytes" "^5.6.0" - "@ethersproject/constants" "^5.6.0" - "@ethersproject/logger" "^5.6.0" - -"@ethersproject/transactions@5.6.0", "@ethersproject/transactions@^5.0.0-beta.135", "@ethersproject/transactions@^5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.6.0.tgz" - integrity sha512-4HX+VOhNjXHZyGzER6E/LVI2i6lf9ejYeWD6l4g50AdmimyuStKc39kvKf1bXWQMg7QNVh+uC7dYwtaZ02IXeg== - dependencies: - "@ethersproject/address" "^5.6.0" - "@ethersproject/bignumber" "^5.6.0" - "@ethersproject/bytes" "^5.6.0" - "@ethersproject/constants" "^5.6.0" - "@ethersproject/keccak256" "^5.6.0" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/properties" "^5.6.0" - "@ethersproject/rlp" "^5.6.0" - "@ethersproject/signing-key" "^5.6.0" - -"@ethersproject/units@5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/units/-/units-5.6.0.tgz" - integrity sha512-tig9x0Qmh8qbo1w8/6tmtyrm/QQRviBh389EQ+d8fP4wDsBrJBf08oZfoiz1/uenKK9M78yAP4PoR7SsVoTjsw== - dependencies: - "@ethersproject/bignumber" "^5.6.0" - "@ethersproject/constants" "^5.6.0" - "@ethersproject/logger" "^5.6.0" - -"@ethersproject/wallet@5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.6.0.tgz" - integrity sha512-qMlSdOSTyp0MBeE+r7SUhr1jjDlC1zAXB8VD84hCnpijPQiSNbxr6GdiLXxpUs8UKzkDiNYYC5DRI3MZr+n+tg== - dependencies: - "@ethersproject/abstract-provider" "^5.6.0" - "@ethersproject/abstract-signer" "^5.6.0" - "@ethersproject/address" "^5.6.0" - "@ethersproject/bignumber" "^5.6.0" - "@ethersproject/bytes" "^5.6.0" - "@ethersproject/hash" "^5.6.0" - "@ethersproject/hdnode" "^5.6.0" - "@ethersproject/json-wallets" "^5.6.0" - "@ethersproject/keccak256" "^5.6.0" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/properties" "^5.6.0" - "@ethersproject/random" "^5.6.0" - "@ethersproject/signing-key" "^5.6.0" - "@ethersproject/transactions" "^5.6.0" - "@ethersproject/wordlists" "^5.6.0" - -"@ethersproject/web@5.6.0", "@ethersproject/web@^5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/web/-/web-5.6.0.tgz" - integrity sha512-G/XHj0hV1FxI2teHRfCGvfBUHFmU+YOSbCxlAMqJklxSa7QMiHFQfAxvwY2PFqgvdkxEKwRNr/eCjfAPEm2Ctg== - dependencies: - "@ethersproject/base64" "^5.6.0" - "@ethersproject/bytes" "^5.6.0" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/properties" "^5.6.0" - "@ethersproject/strings" "^5.6.0" - -"@ethersproject/wordlists@5.6.0", "@ethersproject/wordlists@^5.6.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.6.0.tgz" - integrity sha512-q0bxNBfIX3fUuAo9OmjlEYxP40IB8ABgb7HjEZCL5IKubzV3j30CWi2rqQbjTS2HfoyQbfINoKcTVWP4ejwR7Q== - dependencies: - "@ethersproject/bytes" "^5.6.0" - "@ethersproject/hash" "^5.6.0" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/properties" "^5.6.0" - "@ethersproject/strings" "^5.6.0" - -"@humanwhocodes/config-array@^0.9.2": - version "0.9.5" - resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz" - integrity sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw== - dependencies: - "@humanwhocodes/object-schema" "^1.2.1" - debug "^4.1.1" - minimatch "^3.0.4" +"@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" + +"@humanwhocodes/config-array@^0.11.13": + version "0.11.14" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" + integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== + dependencies: + "@humanwhocodes/object-schema" "^2.0.2" + debug "^4.3.1" + minimatch "^3.0.5" -"@humanwhocodes/object-schema@^1.2.1": - version "1.2.1" - resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz" - integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@metamask/eth-sig-util@^4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@metamask/eth-sig-util/-/eth-sig-util-4.0.0.tgz" - integrity sha512-LczOjjxY4A7XYloxzyxJIHONELmUxVZncpOLoClpEcTiebiVdM46KRPYXGuULro9oNNR2xdVx3yoKiQjdfWmoA== +"@humanwhocodes/object-schema@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917" + integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@jridgewell/gen-mapping@^0.3.2": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== dependencies: - ethereumjs-abi "^0.6.8" - ethereumjs-util "^6.2.1" - ethjs-util "^0.1.6" - tweetnacl "^1.0.3" - tweetnacl-util "^0.15.1" + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" -"@noble/hashes@1.0.0", "@noble/hashes@~1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.0.0.tgz" - integrity sha512-DZVbtY62kc3kkBtMHqwCOfXrT/hnoORy5BJ4+HU1IR59X0KWAOqsfzQPcUl/lQLlG7qXbe/fZ3r/emxtAl+sqg== +"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": + 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/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + 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" -"@noble/secp256k1@1.5.5", "@noble/secp256k1@~1.5.2": - version "1.5.5" - resolved "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.5.5.tgz" - integrity sha512-sZ1W6gQzYnu45wPrWx8D3kwI2/U29VYTx9OjbDAd7jwRItJ0cSTMPRL/C8AWZFn9kWFLQGqEXVEE86w4Z8LpIQ== +"@jridgewell/trace-mapping@^0.3.9": + version "0.3.22" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz#72a621e5de59f5f1ef792d0793a82ee20f645e4c" + integrity sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" "@nodelib/fs.scandir@2.1.5": version "2.1.5" - resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== dependencies: "@nodelib/fs.stat" "2.0.5" @@ -593,639 +496,242 @@ "@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": version "2.0.5" - resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== -"@nodelib/fs.walk@^1.2.3": +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": version "1.2.8" - resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== dependencies: "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@nomiclabs/hardhat-ethers@^2.0.5": - version "2.0.5" - resolved "https://registry.npmjs.org/@nomiclabs/hardhat-ethers/-/hardhat-ethers-2.0.5.tgz" - integrity sha512-A2gZAGB6kUvLx+kzM92HKuUF33F1FSe90L0TmkXkT2Hh0OKRpvWZURUSU2nghD2yC4DzfEZ3DftfeHGvZ2JTUw== - -"@nomiclabs/hardhat-etherscan@^3.0.3": - version "3.0.3" - resolved "https://registry.npmjs.org/@nomiclabs/hardhat-etherscan/-/hardhat-etherscan-3.0.3.tgz" - integrity sha512-OfNtUKc/ZwzivmZnnpwWREfaYncXteKHskn3yDnz+fPBZ6wfM4GR+d5RwjREzYFWE+o5iR9ruXhWw/8fejWM9g== - dependencies: - "@ethersproject/abi" "^5.1.2" - "@ethersproject/address" "^5.0.2" - cbor "^5.0.2" - debug "^4.1.1" - fs-extra "^7.0.1" - semver "^6.3.0" - undici "^4.14.1" - -"@nomiclabs/hardhat-waffle@^2.0.3": - version "2.0.3" - resolved "https://registry.npmjs.org/@nomiclabs/hardhat-waffle/-/hardhat-waffle-2.0.3.tgz" - integrity sha512-049PHSnI1CZq6+XTbrMbMv5NaL7cednTfPenx02k3cEh8wBMLa6ys++dBETJa6JjfwgA9nBhhHQ173LJv6k2Pg== - dependencies: - "@types/sinon-chai" "^3.2.3" - "@types/web3" "1.0.19" - -"@openzeppelin/contracts-upgradeable@4.5.1": - version "4.5.1" - resolved "https://registry.npmjs.org/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.5.1.tgz" - integrity sha512-xcKycsSyFauIGMhSeeTJW/Jzz9jZUJdiFNP9Wo/9VhMhw8t5X0M92RY6x176VfcIWsxURMHFWOJVTlFA78HI/w== - -"@openzeppelin/contracts-upgradeable@^4.4.2": - version "4.6.0" - resolved "https://registry.npmjs.org/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.6.0.tgz" - integrity sha512-5OnVuO4HlkjSCJO165a4i2Pu1zQGzMs//o54LPrwUgxvEO2P3ax1QuaSI0cEHHTveA77guS0PnNugpR2JMsPfA== - -"@openzeppelin/contracts@4.5.0": - version "4.5.0" - resolved "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.5.0.tgz" - integrity sha512-fdkzKPYMjrRiPK6K4y64e6GzULR7R7RwxSigHS8DDp7aWDeoReqsQI+cxHV1UuhAqX69L1lAaWDxenfP+xiqzA== - -"@openzeppelin/contracts@^4.4.2": - version "4.7.0" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.7.0.tgz#3092d70ea60e3d1835466266b1d68ad47035a2d5" - integrity sha512-52Qb+A1DdOss8QvJrijYYPSf32GUg2pGaG/yCxtaA3cu4jduouTdg4XZSMLW9op54m1jH7J8hoajhHKOPsoJFw== - -"@primitivefi/hardhat-dodoc@^0.1.3": - version "0.1.3" - resolved "https://registry.npmjs.org/@primitivefi/hardhat-dodoc/-/hardhat-dodoc-0.1.3.tgz" - integrity sha512-IM2rwyk9SHxnifHnoCKmB1K1su/d1BvF5C0zspCWH8rVrrNpS1NzLTjisDNJmbM69/cWcEX0vfk449LuTsQVaw== - dependencies: - squirrelly "^8.0.8" - -"@resolver-engine/core@^0.3.3": - version "0.3.3" - resolved "https://registry.npmjs.org/@resolver-engine/core/-/core-0.3.3.tgz" - integrity sha512-eB8nEbKDJJBi5p5SrvrvILn4a0h42bKtbCTri3ZxCGt6UvoQyp7HnGOfki944bUjBSHKK3RvgfViHn+kqdXtnQ== - dependencies: - debug "^3.1.0" - is-url "^1.2.4" - request "^2.85.0" +"@openzeppelin/contracts-upgradeable@^4.4.2", "@openzeppelin/contracts-upgradeable@^4.9.6": + version "4.9.6" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.9.6.tgz#38b21708a719da647de4bb0e4802ee235a0d24df" + integrity sha512-m4iHazOsOCv1DgM7eD7GupTJ+NFVujRZt1wzddDPSVGpWdKq1SKkla5htKG7+IS4d2XOCtzkUNwRZ7Vq5aEUMA== -"@resolver-engine/fs@^0.3.3": - version "0.3.3" - resolved "https://registry.npmjs.org/@resolver-engine/fs/-/fs-0.3.3.tgz" - integrity sha512-wQ9RhPUcny02Wm0IuJwYMyAG8fXVeKdmhm8xizNByD4ryZlx6PP6kRen+t/haF43cMfmaV7T3Cx6ChOdHEhFUQ== - dependencies: - "@resolver-engine/core" "^0.3.3" - debug "^3.1.0" +"@openzeppelin/contracts@^4.4.2", "@openzeppelin/contracts@^4.9.6": + version "4.9.6" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.9.6.tgz#2a880a24eb19b4f8b25adc2a5095f2aa27f39677" + integrity sha512-xSmezSupL+y9VkHZJGDoCBpmnB2ogM13ccaYDWqJTfS3dbuHkgjuwDFUmaFauBCboQMGB/S5UqUl2y54X99BmA== -"@resolver-engine/imports-fs@^0.3.3": - version "0.3.3" - resolved "https://registry.npmjs.org/@resolver-engine/imports-fs/-/imports-fs-0.3.3.tgz" - integrity sha512-7Pjg/ZAZtxpeyCFlZR5zqYkz+Wdo84ugB5LApwriT8XFeQoLwGUj4tZFFvvCuxaNCcqZzCYbonJgmGObYBzyCA== - dependencies: - "@resolver-engine/fs" "^0.3.3" - "@resolver-engine/imports" "^0.3.3" - debug "^3.1.0" +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@resolver-engine/imports@^0.3.3": - version "0.3.3" - resolved "https://registry.npmjs.org/@resolver-engine/imports/-/imports-0.3.3.tgz" - integrity sha512-anHpS4wN4sRMwsAbMXhMfOD/y4a4Oo0Cw/5+rue7hSwGWsDOQaAU1ClK1OxjUC35/peazxEl8JaSRRS+Xb8t3Q== +"@solidity-parser/parser@^0.16.0": + version "0.16.2" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.16.2.tgz#42cb1e3d88b3e8029b0c9befff00b634cd92d2fa" + integrity sha512-PI9NfoA3P8XK2VBkK5oIfRgKDsicwDZfkVq9ZTBCQYGOP1N2owgY2dyLGyU5/J/hQs8KRk55kdmvTLjy3Mu3vg== dependencies: - "@resolver-engine/core" "^0.3.3" - debug "^3.1.0" - hosted-git-info "^2.6.0" - path-browserify "^1.0.0" - url "^0.11.0" - -"@scure/base@~1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@scure/base/-/base-1.0.0.tgz" - integrity sha512-gIVaYhUsy+9s58m/ETjSJVKHhKTBMmcRb9cEV5/5dwvfDlfORjKrFsDeDHWRrm6RjcPvCLZFwGJjAjLj1gg4HA== + antlr4ts "^0.5.0-alpha.4" -"@scure/bip32@1.0.1": - version "1.0.1" - resolved "https://registry.npmjs.org/@scure/bip32/-/bip32-1.0.1.tgz" - integrity sha512-AU88KKTpQ+YpTLoicZ/qhFhRRIo96/tlb+8YmDDHR9yiKVjSsFZiefJO4wjS2PMTkz5/oIcw84uAq/8pleQURA== - dependencies: - "@noble/hashes" "~1.0.0" - "@noble/secp256k1" "~1.5.2" - "@scure/base" "~1.0.0" +"@solidity-parser/parser@^0.17.0": + version "0.17.0" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.17.0.tgz#52a2fcc97ff609f72011014e4c5b485ec52243ef" + integrity sha512-Nko8R0/kUo391jsEHHxrGM07QFdnPGvlmox4rmH0kNiNAashItAilhy4Mv4pK5gQmW5f4sXAF58fwJbmlkGcVw== -"@scure/bip39@1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@scure/bip39/-/bip39-1.0.0.tgz" - integrity sha512-HrtcikLbd58PWOkl02k9V6nXWQyoa7A0+Ek9VF7z17DDk9XZAFUcIdqfh0jJXLypmizc5/8P6OxoUeKliiWv4w== - dependencies: - "@noble/hashes" "~1.0.0" - "@scure/base" "~1.0.0" - -"@sentry/core@5.30.0": - version "5.30.0" - resolved "https://registry.npmjs.org/@sentry/core/-/core-5.30.0.tgz" - integrity sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg== - dependencies: - "@sentry/hub" "5.30.0" - "@sentry/minimal" "5.30.0" - "@sentry/types" "5.30.0" - "@sentry/utils" "5.30.0" - tslib "^1.9.3" - -"@sentry/hub@5.30.0": - version "5.30.0" - resolved "https://registry.npmjs.org/@sentry/hub/-/hub-5.30.0.tgz" - integrity sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ== - dependencies: - "@sentry/types" "5.30.0" - "@sentry/utils" "5.30.0" - tslib "^1.9.3" - -"@sentry/minimal@5.30.0": - version "5.30.0" - resolved "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.30.0.tgz" - integrity sha512-BwWb/owZKtkDX+Sc4zCSTNcvZUq7YcH3uAVlmh/gtR9rmUvbzAA3ewLuB3myi4wWRAMEtny6+J/FN/x+2wn9Xw== - dependencies: - "@sentry/hub" "5.30.0" - "@sentry/types" "5.30.0" - tslib "^1.9.3" - -"@sentry/node@^5.18.1": - version "5.30.0" - resolved "https://registry.npmjs.org/@sentry/node/-/node-5.30.0.tgz" - integrity sha512-Br5oyVBF0fZo6ZS9bxbJZG4ApAjRqAnqFFurMVJJdunNb80brh7a5Qva2kjhm+U6r9NJAB5OmDyPkA1Qnt+QVg== - dependencies: - "@sentry/core" "5.30.0" - "@sentry/hub" "5.30.0" - "@sentry/tracing" "5.30.0" - "@sentry/types" "5.30.0" - "@sentry/utils" "5.30.0" - cookie "^0.4.1" - https-proxy-agent "^5.0.0" - lru_map "^0.3.3" - tslib "^1.9.3" - -"@sentry/tracing@5.30.0": - version "5.30.0" - resolved "https://registry.npmjs.org/@sentry/tracing/-/tracing-5.30.0.tgz" - integrity sha512-dUFowCr0AIMwiLD7Fs314Mdzcug+gBVo/+NCMyDw8tFxJkwWAKl7Qa2OZxLQ0ZHjakcj1hNKfCQJ9rhyfOl4Aw== - dependencies: - "@sentry/hub" "5.30.0" - "@sentry/minimal" "5.30.0" - "@sentry/types" "5.30.0" - "@sentry/utils" "5.30.0" - tslib "^1.9.3" - -"@sentry/types@5.30.0": - version "5.30.0" - resolved "https://registry.npmjs.org/@sentry/types/-/types-5.30.0.tgz" - integrity sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw== - -"@sentry/utils@5.30.0": - version "5.30.0" - resolved "https://registry.npmjs.org/@sentry/utils/-/utils-5.30.0.tgz" - integrity sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww== - dependencies: - "@sentry/types" "5.30.0" - tslib "^1.9.3" - -"@sindresorhus/is@^0.14.0": - version "0.14.0" - resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz" - integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== - -"@solidity-parser/parser@^0.14.0", "@solidity-parser/parser@^0.14.1": - version "0.14.1" - resolved "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.14.1.tgz" - integrity sha512-eLjj2L6AuQjBB6s/ibwCAc0DwrR5Ge+ys+wgWo+bviU7fV2nTMQhU63CGaDKXg9iTmMxwhkyoggdIR7ZGRfMgw== - dependencies: - antlr4ts "^0.5.0-alpha.4" +"@thirdweb-dev/dynamic-contracts@^1.2.4": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@thirdweb-dev/dynamic-contracts/-/dynamic-contracts-1.2.5.tgz#f9735c0d46198e7bf2f98c277f0a9a79c54da1e8" + integrity sha512-YVsz+jUWbwj+6aF2eTZGMfyw47a1HRmgNl4LQ3gW9gwYL5y5+OX/yOzv6aV5ibvoqCk/k10aIVK2eFrcpMubQA== -"@szmarczak/http-timer@^1.1.2": - version "1.1.2" - resolved "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz" - integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== +"@thirdweb-dev/merkletree@^0.2.6": + version "0.2.6" + resolved "https://registry.yarnpkg.com/@thirdweb-dev/merkletree/-/merkletree-0.2.6.tgz#874f6d6d98988150785d51c3ce616a06ae2f7563" + integrity sha512-dLw8sxzHSsMxuxwBDzkhwl4ksBKuB3Em7W/u7/2S5Ag0DsBmrrOZQz/+3Nf88mxCvq435PqyQsMPYfY2zJ22QA== dependencies: - defer-to-connect "^1.0.1" + buffer "^6.0.3" + buffer-reverse "^1.0.1" + treeify "^1.1.0" "@tsconfig/node10@^1.0.7": - version "1.0.8" - resolved "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz" - integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg== + 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.9" - resolved "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz" - integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw== + 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.1" - resolved "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz" - integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== + 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.2" - resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz" - integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== -"@typechain/ethers-v5@^10.0.0": - version "10.0.0" - resolved "https://registry.npmjs.org/@typechain/ethers-v5/-/ethers-v5-10.0.0.tgz" - integrity sha512-Kot7fwAqnH96ZbI8xrRgj5Kpv9yCEdjo7mxRqrH7bYpEgijT5MmuOo8IVsdhOu7Uog4ONg7k/d5UdbAtTKUgsA== +"@typechain/ethers-v5@^10.2.1": + version "10.2.1" + resolved "https://registry.yarnpkg.com/@typechain/ethers-v5/-/ethers-v5-10.2.1.tgz#50241e6957683281ecfa03fb5a6724d8a3ce2391" + integrity sha512-n3tQmCZjRE6IU4h6lqUGiQ1j866n5MTCBJreNEHHVWXa2u9GJTaeYyU1/k+1qLutkyw+sS6VAN+AbeiTqsxd/A== dependencies: lodash "^4.17.15" ts-essentials "^7.0.1" -"@typechain/ethers-v5@^2.0.0": - version "2.0.0" - resolved "https://registry.npmjs.org/@typechain/ethers-v5/-/ethers-v5-2.0.0.tgz" - integrity sha512-0xdCkyGOzdqh4h5JSf+zoWx85IusEjDcPIwNEHP8mrWSnCae4rvrqB+/gtpdNfX7zjlFlZiMeePn2r63EI3Lrw== - dependencies: - ethers "^5.0.2" - -"@typechain/hardhat@^4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@typechain/hardhat/-/hardhat-4.0.0.tgz" - integrity sha512-SeEKtiHu4Io3LHhE8VV3orJbsj7dwJZX8pzSTv7WQR38P18vOLm2M52GrykVinMpkLK0uVc88ICT58emvfn74w== - dependencies: - fs-extra "^9.1.0" - -"@types/abstract-leveldown@*": - version "7.2.0" - resolved "https://registry.npmjs.org/@types/abstract-leveldown/-/abstract-leveldown-7.2.0.tgz" - integrity sha512-q5veSX6zjUy/DlDhR4Y4cU0k2Ar+DT2LUraP00T19WLmTO6Se1djepCCaqU6nQrwcJ5Hyo/CWqxTzrrFg8eqbQ== - -"@types/bn.js@*", "@types/bn.js@^5.1.0": - version "5.1.0" - resolved "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.0.tgz" - integrity sha512-QSSVYj7pYFN49kW77o2s9xTCwZ8F2xLbjLLSEVh8D2F4JUhZtPAGOFLTD+ffqksBx/u4cE/KImFjyhqCjn/LIA== - dependencies: - "@types/node" "*" - -"@types/bn.js@^4.11.3", "@types/bn.js@^4.11.5": - version "4.11.6" - resolved "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz" - integrity sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg== - dependencies: - "@types/node" "*" - -"@types/chai@*": - version "4.3.0" - resolved "https://registry.npmjs.org/@types/chai/-/chai-4.3.0.tgz" - integrity sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw== - -"@types/concat-stream@^1.6.0": - version "1.6.1" - resolved "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.1.tgz" - integrity sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA== - dependencies: - "@types/node" "*" - -"@types/form-data@0.0.33": - version "0.0.33" - resolved "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz" - integrity sha1-yayFsqX9GENbjIXZ7LUObWyJP/g= - dependencies: - "@types/node" "*" - "@types/fs-extra@^9.0.13": version "9.0.13" - resolved "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.13.tgz#7594fbae04fe7f1918ce8b3d213f74ff44ac1f45" integrity sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA== dependencies: "@types/node" "*" "@types/json-schema@^7.0.9": - version "7.0.11" - resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz" - integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== - -"@types/level-errors@*": - version "3.0.0" - resolved "https://registry.npmjs.org/@types/level-errors/-/level-errors-3.0.0.tgz" - integrity sha512-/lMtoq/Cf/2DVOm6zE6ORyOM+3ZVm/BvzEZVxUhf6bgh8ZHglXlBqxbxSlJeVp8FCbD3IVvk/VbsaNmDjrQvqQ== - -"@types/levelup@^4.3.0": - version "4.3.3" - resolved "https://registry.npmjs.org/@types/levelup/-/levelup-4.3.3.tgz" - integrity sha512-K+OTIjJcZHVlZQN1HmU64VtrC0jC3dXWQozuEIR9zVvltIk90zaGPM2AgT+fIkChpzHhFE3YnvFLCbLtzAmexA== - dependencies: - "@types/abstract-leveldown" "*" - "@types/level-errors" "*" - "@types/node" "*" - -"@types/lru-cache@^5.1.0": - version "5.1.1" - resolved "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.1.tgz" - integrity sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw== + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== -"@types/mkdirp@^0.5.2": - version "0.5.2" - resolved "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-0.5.2.tgz" - integrity sha512-U5icWpv7YnZYGsN4/cmh3WD2onMY0aJIiTE6+51TwJCttdHvtCYmkBNOobHlXwrJRL0nkH9jH4kD+1FAdMN4Tg== - dependencies: - "@types/node" "*" +"@types/mocha@^9.1.1": + version "9.1.1" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" + integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== -"@types/mocha@^9.1.0": - version "9.1.0" - resolved "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.0.tgz" - integrity sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg== - -"@types/node-fetch@^2.5.5": - version "2.6.1" - resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.1.tgz" - integrity sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA== +"@types/node@*": + version "20.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.6.tgz#6adf4241460e28be53836529c033a41985f85b6e" + integrity sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q== dependencies: - "@types/node" "*" - form-data "^3.0.0" + undici-types "~5.26.4" -"@types/node@*", "@types/node@^17.0.21": - version "17.0.23" - resolved "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz" - integrity sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw== - -"@types/node@^10.0.3": - version "10.17.60" - resolved "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz" - integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== - -"@types/node@^12.12.6": - version "12.20.47" - resolved "https://registry.npmjs.org/@types/node/-/node-12.20.47.tgz" - integrity sha512-BzcaRsnFuznzOItW1WpQrDHM7plAa7GIDMZ6b5pnMbkqEtM/6WCOhvZar39oeMQP79gwvFUWjjptE7/KGcNqFg== - -"@types/node@^8.0.0": - version "8.10.66" - resolved "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz" - integrity sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw== - -"@types/pbkdf2@^3.0.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.1.0.tgz" - integrity sha512-Cf63Rv7jCQ0LaL8tNXmEyqTHuIJxRdlS5vMh1mj5voN4+QFhVZnlZruezqpWYDiJ8UTzhP0VmeLXCmBk66YrMQ== - dependencies: - "@types/node" "*" +"@types/node@^17.0.45": + version "17.0.45" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" + integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== "@types/prettier@^2.1.1": - version "2.6.0" - resolved "https://registry.npmjs.org/@types/prettier/-/prettier-2.6.0.tgz" - integrity sha512-G/AdOadiZhnJp0jXCaBQU449W2h716OW/EoXeYkCytxKL06X1WCXB4DZpp8TpZ8eyIJVS1cw4lrlkkSYU21cDw== - -"@types/qs@^6.2.31": - version "6.9.7" - resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz" - integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== - -"@types/resolve@^0.0.8": - version "0.0.8" - resolved "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz" - integrity sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ== - dependencies: - "@types/node" "*" - -"@types/secp256k1@^4.0.1": - version "4.0.3" - resolved "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-4.0.3.tgz" - integrity sha512-Da66lEIFeIz9ltsdMZcpQvmrmmoqrfju8pm1BH8WbYjZSwUgCwXLb9C+9XYogwBITnbsSaMdVPb2ekf7TV+03w== - dependencies: - "@types/node" "*" - -"@types/sinon-chai@^3.2.3": - version "3.2.8" - resolved "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.8.tgz" - integrity sha512-d4ImIQbT/rKMG8+AXpmcan5T2/PNeSjrYhvkwet6z0p8kzYtfgA32xzOBlbU0yqJfq+/0Ml805iFoODO0LP5/g== - dependencies: - "@types/chai" "*" - "@types/sinon" "*" - -"@types/sinon@*": - version "10.0.11" - resolved "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.11.tgz" - integrity sha512-dmZsHlBsKUtBpHriNjlK0ndlvEh8dcb9uV9Afsbt89QIyydpC7NcR+nWlAhASfy3GHnxTl4FX/aKE7XZUt/B4g== - dependencies: - "@types/sinonjs__fake-timers" "*" - -"@types/sinonjs__fake-timers@*": - version "8.1.2" - resolved "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz" - integrity sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA== - -"@types/underscore@*": - version "1.11.4" - resolved "https://registry.npmjs.org/@types/underscore/-/underscore-1.11.4.tgz" - integrity sha512-uO4CD2ELOjw8tasUrAhvnn2W4A0ZECOvMjCivJr4gA9pGgjv+qxKWY9GLTMVEK8ej85BxQOocUyE7hImmSQYcg== - -"@types/web3@1.0.19": - version "1.0.19" - resolved "https://registry.npmjs.org/@types/web3/-/web3-1.0.19.tgz" - integrity sha512-fhZ9DyvDYDwHZUp5/STa9XW2re0E8GxoioYJ4pEUZ13YHpApSagixj7IAdoYH5uAK+UalGq6Ml8LYzmgRA/q+A== - dependencies: - "@types/bn.js" "*" - "@types/underscore" "*" - -"@typescript-eslint/eslint-plugin@^5.13.0": - version "5.19.0" - resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.19.0.tgz" - integrity sha512-w59GpFqDYGnWFim9p6TGJz7a3qWeENJuAKCqjGSx+Hq/bwq3RZwXYqy98KIfN85yDqz9mq6QXiY5h0FjGQLyEg== - dependencies: - "@typescript-eslint/scope-manager" "5.19.0" - "@typescript-eslint/type-utils" "5.19.0" - "@typescript-eslint/utils" "5.19.0" - debug "^4.3.2" - functional-red-black-tree "^1.0.1" - ignore "^5.1.8" - regexpp "^3.2.0" - semver "^7.3.5" + version "2.7.3" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.3.tgz#3e51a17e291d01d17d3fc61422015a933af7a08f" + integrity sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA== + +"@types/semver@^7.3.12": + version "7.5.6" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.6.tgz#c65b2bfce1bec346582c07724e3f8c1017a20339" + integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A== + +"@typescript-eslint/eslint-plugin@^5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db" + integrity sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag== + dependencies: + "@eslint-community/regexpp" "^4.4.0" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/type-utils" "5.62.0" + "@typescript-eslint/utils" "5.62.0" + debug "^4.3.4" + graphemer "^1.4.0" + ignore "^5.2.0" + natural-compare-lite "^1.4.0" + semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/parser@^5.13.0": - version "5.19.0" - resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.19.0.tgz" - integrity sha512-yhktJjMCJX8BSBczh1F/uY8wGRYrBeyn84kH6oyqdIJwTGKmzX5Qiq49LRQ0Jh0LXnWijEziSo6BRqny8nqLVQ== +"@typescript-eslint/parser@^5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.62.0.tgz#1b63d082d849a2fcae8a569248fbe2ee1b8a56c7" + integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA== dependencies: - "@typescript-eslint/scope-manager" "5.19.0" - "@typescript-eslint/types" "5.19.0" - "@typescript-eslint/typescript-estree" "5.19.0" - debug "^4.3.2" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/typescript-estree" "5.62.0" + debug "^4.3.4" -"@typescript-eslint/scope-manager@5.19.0": - version "5.19.0" - resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.19.0.tgz" - integrity sha512-Fz+VrjLmwq5fbQn5W7cIJZ066HxLMKvDEmf4eu1tZ8O956aoX45jAuBB76miAECMTODyUxH61AQM7q4/GOMQ5g== +"@typescript-eslint/scope-manager@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c" + integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w== dependencies: - "@typescript-eslint/types" "5.19.0" - "@typescript-eslint/visitor-keys" "5.19.0" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" -"@typescript-eslint/type-utils@5.19.0": - version "5.19.0" - resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.19.0.tgz" - integrity sha512-O6XQ4RI4rQcBGshTQAYBUIGsKqrKeuIOz9v8bckXZnSeXjn/1+BDZndHLe10UplQeJLXDNbaZYrAytKNQO2T4Q== +"@typescript-eslint/type-utils@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz#286f0389c41681376cdad96b309cedd17d70346a" + integrity sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew== dependencies: - "@typescript-eslint/utils" "5.19.0" - debug "^4.3.2" + "@typescript-eslint/typescript-estree" "5.62.0" + "@typescript-eslint/utils" "5.62.0" + debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.19.0": - version "5.19.0" - resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.19.0.tgz" - integrity sha512-zR1ithF4Iyq1wLwkDcT+qFnhs8L5VUtjgac212ftiOP/ZZUOCuuF2DeGiZZGQXGoHA50OreZqLH5NjDcDqn34w== +"@typescript-eslint/types@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" + integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== -"@typescript-eslint/typescript-estree@5.19.0": - version "5.19.0" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.19.0.tgz" - integrity sha512-dRPuD4ocXdaE1BM/dNR21elSEUPKaWgowCA0bqJ6YbYkvtrPVEvZ+zqcX5a8ECYn3q5iBSSUcBBD42ubaOp0Hw== +"@typescript-eslint/typescript-estree@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" + integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== dependencies: - "@typescript-eslint/types" "5.19.0" - "@typescript-eslint/visitor-keys" "5.19.0" - debug "^4.3.2" - globby "^11.0.4" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" + debug "^4.3.4" + globby "^11.1.0" is-glob "^4.0.3" - semver "^7.3.5" + semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.19.0": - version "5.19.0" - resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.19.0.tgz" - integrity sha512-ZuEckdupXpXamKvFz/Ql8YnePh2ZWcwz7APICzJL985Rp5C2AYcHO62oJzIqNhAMtMK6XvrlBTZeNG8n7gS3lQ== +"@typescript-eslint/utils@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86" + integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ== dependencies: + "@eslint-community/eslint-utils" "^4.2.0" "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.19.0" - "@typescript-eslint/types" "5.19.0" - "@typescript-eslint/typescript-estree" "5.19.0" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/typescript-estree" "5.62.0" eslint-scope "^5.1.1" - eslint-utils "^3.0.0" + semver "^7.3.7" -"@typescript-eslint/visitor-keys@5.19.0": - version "5.19.0" - resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.19.0.tgz" - integrity sha512-Ym7zZoMDZcAKWsULi2s7UMLREdVQdScPQ/fKWMYefarCztWlHPFVJo8racf8R0Gc8FAEJ2eD4of8As1oFtnQlQ== +"@typescript-eslint/visitor-keys@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" + integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== dependencies: - "@typescript-eslint/types" "5.19.0" - eslint-visitor-keys "^3.0.0" + "@typescript-eslint/types" "5.62.0" + eslint-visitor-keys "^3.3.0" "@ungap/promise-all-settled@1.1.2": version "1.1.2" - resolved "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz" + resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== -"@yarnpkg/lockfile@^1.1.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz" - integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== - -abort-controller@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz" - integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== - dependencies: - event-target-shim "^5.0.0" - -abstract-leveldown@3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-3.0.0.tgz" - integrity sha512-KUWx9UWGQD12zsmLNj64/pndaz4iJh/Pj7nopgkfDG6RlCcbMZvT6+9l7dchK4idog2Is8VdC/PvNbFuFmalIQ== - dependencies: - xtend "~4.0.0" - -abstract-leveldown@^2.4.1, abstract-leveldown@~2.7.1: - version "2.7.2" - resolved "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-2.7.2.tgz" - integrity sha512-+OVvxH2rHVEhWLdbudP6p0+dNMXu8JA1CbhP19T8paTYAcX7oJ4OVjT+ZUVpv7mITxXHqDMej+GdqXBmXkw09w== - dependencies: - xtend "~4.0.0" - -abstract-leveldown@^5.0.0, abstract-leveldown@~5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-5.0.0.tgz" - integrity sha512-5mU5P1gXtsMIXg65/rsYGsi93+MlogXZ9FA8JnwKurHQg64bfXwGYVdVdijNTVNOlAsuIiOwHdvFFD5JqCJQ7A== - dependencies: - xtend "~4.0.0" +"@ungap/structured-clone@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -abstract-leveldown@^6.2.1: - version "6.3.0" - resolved "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.3.0.tgz" - integrity sha512-TU5nlYgta8YrBMNpc9FwQzRbiXsj49gsALsXadbGHt9CROPzX5fB0rWDR5mtdpOOKa5XqRFpbj1QroPAoPzVjQ== - dependencies: - buffer "^5.5.0" - immediate "^3.2.3" - level-concat-iterator "~2.0.0" - level-supports "~1.0.0" - xtend "~4.0.0" - -abstract-leveldown@~2.6.0: - version "2.6.3" - resolved "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-2.6.3.tgz" - integrity sha512-2++wDf/DYqkPR3o5tbfdhF96EfMApo1GpPfzOsR/ZYXdkSmELlvOOEAl9iKkRsktMPHdGjO4rtkBpf2I7TiTeA== - dependencies: - xtend "~4.0.0" - -abstract-leveldown@~6.2.1: - version "6.2.3" - resolved "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz" - integrity sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ== - dependencies: - buffer "^5.5.0" - immediate "^3.2.3" - level-concat-iterator "~2.0.0" - level-supports "~1.0.0" - xtend "~4.0.0" - -accepts@~1.3.8: - version "1.3.8" - resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" - integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== - dependencies: - mime-types "~2.1.34" - negotiator "0.6.3" - -acorn-jsx@^5.0.0, acorn-jsx@^5.3.1: +acorn-jsx@^5.3.2: version "5.3.2" - resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== acorn-walk@^8.1.1: - version "8.2.0" - resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz" - integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== - -acorn@^6.0.7: - version "6.4.2" - resolved "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz" - integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== - -acorn@^8.4.1, acorn@^8.7.0: - version "8.7.0" - resolved "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz" - integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== + version "8.3.2" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa" + integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== -adm-zip@^0.4.16: - version "0.4.16" - resolved "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz" - integrity sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg== +acorn@^8.4.1, acorn@^8.9.0: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== aes-js@3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz" - integrity sha1-4h3xCtbCBTKVvLuNq0Cwnb6ofk0= - -aes-js@^3.1.1: - version "3.1.2" - resolved "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz" - integrity sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ== - -agent-base@6: - version "6.0.2" - resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== - dependencies: - debug "4" - -aggregate-error@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz" - integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" + resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" + integrity sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw== -ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.6.1, ajv@^6.9.1: +ajv@^6.12.4, ajv@^6.12.6: version "6.12.6" - resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + 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" @@ -1233,1282 +739,225 @@ ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.6.1, ajv@^6.9.1: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ansi-colors@3.2.3: - version "3.2.3" - resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz" - integrity sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw== - -ansi-colors@4.1.1, ansi-colors@^4.1.0, ansi-colors@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz" - integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== - -ansi-escapes@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz" - integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== - -ansi-escapes@^4.3.0: - version "4.3.2" - resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz" - integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== +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: - type-fest "^0.21.3" - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz" - integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw== + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" -ansi-regex@^4.1.0: +ansi-colors@4.1.1: version "4.1.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz" - integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== ansi-regex@^5.0.1: version "5.0.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= +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.0, ansi-styles@^3.2.1: +ansi-styles@^3.2.1: version "3.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz" + 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.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: color-convert "^2.0.1" -antlr4@4.7.1: - version "4.7.1" - resolved "https://registry.npmjs.org/antlr4/-/antlr4-4.7.1.tgz" - integrity sha512-haHyTW7Y9joE5MVs37P2lNYfU2RWBLfcRDD8OWldcdZm5TiCE91B5Xl1oWSwiDUSd4rlExpt2pu1fksYQjRBYQ== +ansi-styles@^6.1.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.1" + resolved "https://registry.yarnpkg.com/antlr4/-/antlr4-4.13.1.tgz#1e0a1830a08faeb86217cb2e6c34716004e4253d" + integrity sha512-kiXTspaRYvnIArgE97z5YVVf/cDVQABr3abFRR6mE7yesLMkgu4ujuyV/sgxafQ8wgve0DJQUJ38Z8tkgA2izA== antlr4ts@^0.5.0-alpha.4: version "0.5.0-alpha.4" - resolved "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz" + resolved "https://registry.yarnpkg.com/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz#71702865a87478ed0b40c0709f422cf14d51652a" integrity sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ== any-promise@^1.0.0: version "1.3.0" - resolved "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz" - integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== -anymatch@~3.1.1, anymatch@~3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz" - integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" arg@^4.1.0: version "4.1.3" - resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - argparse@^2.0.1: version "2.0.1" - resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -arr-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz" - integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= - -arr-flatten@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== - -arr-union@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz" - integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= - -array-back@^1.0.3, array-back@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz" - integrity sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs= - dependencies: - typical "^2.6.0" - -array-back@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz" - integrity sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw== - dependencies: - typical "^2.6.1" - array-back@^3.0.1, array-back@^3.1.0: version "3.1.0" - resolved "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0" integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q== -array-back@^4.0.1: +array-back@^4.0.1, array-back@^4.0.2: version "4.0.2" - resolved "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.2.tgz#8004e999a6274586beeb27342168652fdb89fa1e" integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg== -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" - integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= - array-union@^2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -array-uniq@1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz" - integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= - -array-unique@^0.3.2: - version "0.3.2" - resolved "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz" - integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= - -asap@~2.0.6: - version "2.0.6" - resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz" - integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= - -asn1.js@^5.2.0: - version "5.4.1" - resolved "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz" - integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - safer-buffer "^2.1.0" - -asn1@~0.2.3: - version "0.2.6" - resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz" - integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== - dependencies: - safer-buffer "~2.1.0" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" - integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= - -assign-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz" - integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= - -ast-parents@0.0.1: +ast-parents@^0.0.1: version "0.0.1" - resolved "https://registry.npmjs.org/ast-parents/-/ast-parents-0.0.1.tgz" - integrity sha1-UI/Q8F0MSHddnszaLhdEIyYejdM= - -astral-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz" - integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== - -async-eventemitter@^0.2.2, async-eventemitter@^0.2.4: - version "0.2.4" - resolved "https://registry.npmjs.org/async-eventemitter/-/async-eventemitter-0.2.4.tgz" - integrity sha512-pd20BwL7Yt1zwDFy+8MX8F1+WCT8aQeKj0kQnTrH9WaeRETlRamVhD0JtRPmrV4GfOJ2F9CvdQkZeZhnh2TuHw== - dependencies: - async "^2.4.0" - -async-limiter@~1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz" - integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + resolved "https://registry.yarnpkg.com/ast-parents/-/ast-parents-0.0.1.tgz#508fd0f05d0c48775d9eccda2e174423261e8dd3" + integrity sha512-XHusKxKz3zoYk1ic8Un640joHbFMhbqneyoZfoKnEGtf2ey9Uh/IdpcQplODdO/kENaMIWsD0nJm4+wX3UNLHA== -async@2.6.2: - version "2.6.2" - resolved "https://registry.npmjs.org/async/-/async-2.6.2.tgz" - integrity sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg== - dependencies: - lodash "^4.17.11" - -async@^1.4.2: - version "1.5.2" - resolved "https://registry.npmjs.org/async/-/async-1.5.2.tgz" - integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= - -async@^2.0.1, async@^2.1.2, async@^2.4.0, async@^2.5.0, async@^2.6.1: - version "2.6.3" - resolved "https://registry.npmjs.org/async/-/async-2.6.3.tgz" - integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== - dependencies: - lodash "^4.17.14" - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= - -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - -atob@^2.1.2: - version "2.1.2" - resolved "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz" - integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= - -aws4@^1.8.0: - version "1.11.0" - resolved "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz" - integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== - -babel-code-frame@^6.26.0: - version "6.26.0" - resolved "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz" - integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s= - dependencies: - chalk "^1.1.3" - esutils "^2.0.2" - js-tokens "^3.0.2" - -babel-core@^6.0.14, babel-core@^6.26.0: - version "6.26.3" - resolved "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz" - integrity sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA== - dependencies: - babel-code-frame "^6.26.0" - babel-generator "^6.26.0" - babel-helpers "^6.24.1" - babel-messages "^6.23.0" - babel-register "^6.26.0" - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - convert-source-map "^1.5.1" - debug "^2.6.9" - json5 "^0.5.1" - lodash "^4.17.4" - minimatch "^3.0.4" - path-is-absolute "^1.0.1" - private "^0.1.8" - slash "^1.0.0" - source-map "^0.5.7" - -babel-generator@^6.26.0: - version "6.26.1" - resolved "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz" - integrity sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA== - dependencies: - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - detect-indent "^4.0.0" - jsesc "^1.3.0" - lodash "^4.17.4" - source-map "^0.5.7" - trim-right "^1.0.1" - -babel-helper-builder-binary-assignment-operator-visitor@^6.24.1: - version "6.24.1" - resolved "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz" - integrity sha1-zORReto1b0IgvK6KAsKzRvmlZmQ= - dependencies: - babel-helper-explode-assignable-expression "^6.24.1" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-helper-call-delegate@^6.24.1: - version "6.24.1" - resolved "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz" - integrity sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340= - dependencies: - babel-helper-hoist-variables "^6.24.1" - babel-runtime "^6.22.0" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-define-map@^6.24.1: - version "6.26.0" - resolved "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz" - integrity sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8= - dependencies: - babel-helper-function-name "^6.24.1" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - lodash "^4.17.4" - -babel-helper-explode-assignable-expression@^6.24.1: - version "6.24.1" - resolved "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz" - integrity sha1-8luCz33BBDPFX3BZLVdGQArCLKo= - dependencies: - babel-runtime "^6.22.0" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-function-name@^6.24.1: - version "6.24.1" - resolved "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz" - integrity sha1-00dbjAPtmCQqJbSDUasYOZ01gKk= - dependencies: - babel-helper-get-function-arity "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-get-function-arity@^6.24.1: - version "6.24.1" - resolved "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz" - integrity sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-helper-hoist-variables@^6.24.1: - version "6.24.1" - resolved "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz" - integrity sha1-HssnaJydJVE+rbyZFKc/VAi+enY= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-helper-optimise-call-expression@^6.24.1: - version "6.24.1" - resolved "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz" - integrity sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-helper-regex@^6.24.1: - version "6.26.0" - resolved "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz" - integrity sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI= - dependencies: - babel-runtime "^6.26.0" - babel-types "^6.26.0" - lodash "^4.17.4" - -babel-helper-remap-async-to-generator@^6.24.1: - version "6.24.1" - resolved "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz" - integrity sha1-XsWBgnrXI/7N04HxySg5BnbkVRs= - dependencies: - babel-helper-function-name "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-replace-supers@^6.24.1: - version "6.24.1" - resolved "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz" - integrity sha1-v22/5Dk40XNpohPKiov3S2qQqxo= - dependencies: - babel-helper-optimise-call-expression "^6.24.1" - babel-messages "^6.23.0" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helpers@^6.24.1: - version "6.24.1" - resolved "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz" - integrity sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI= - dependencies: - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-messages@^6.23.0: - version "6.23.0" - resolved "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz" - integrity sha1-8830cDhYA1sqKVHG7F7fbGLyYw4= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-check-es2015-constants@^6.22.0: - version "6.22.0" - resolved "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz" - integrity sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-syntax-async-functions@^6.8.0: - version "6.13.0" - resolved "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz" - integrity sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU= - -babel-plugin-syntax-exponentiation-operator@^6.8.0: - version "6.13.0" - resolved "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz" - integrity sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4= - -babel-plugin-syntax-trailing-function-commas@^6.22.0: - version "6.22.0" - resolved "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz" - integrity sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM= - -babel-plugin-transform-async-to-generator@^6.22.0: - version "6.24.1" - resolved "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz" - integrity sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E= - dependencies: - babel-helper-remap-async-to-generator "^6.24.1" - babel-plugin-syntax-async-functions "^6.8.0" - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-arrow-functions@^6.22.0: - version "6.22.0" - resolved "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz" - integrity sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-block-scoped-functions@^6.22.0: - version "6.22.0" - resolved "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz" - integrity sha1-u8UbSflk1wy42OC5ToICRs46YUE= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-block-scoping@^6.23.0: - version "6.26.0" - resolved "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz" - integrity sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8= - dependencies: - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - lodash "^4.17.4" - -babel-plugin-transform-es2015-classes@^6.23.0: - version "6.24.1" - resolved "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz" - integrity sha1-WkxYpQyclGHlZLSyo7+ryXolhNs= - dependencies: - babel-helper-define-map "^6.24.1" - babel-helper-function-name "^6.24.1" - babel-helper-optimise-call-expression "^6.24.1" - babel-helper-replace-supers "^6.24.1" - babel-messages "^6.23.0" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-computed-properties@^6.22.0: - version "6.24.1" - resolved "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz" - integrity sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM= - dependencies: - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-destructuring@^6.23.0: - version "6.23.0" - resolved "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz" - integrity sha1-mXux8auWf2gtKwh2/jWNYOdlxW0= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-duplicate-keys@^6.22.0: - version "6.24.1" - resolved "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz" - integrity sha1-c+s9MQypaePvnskcU3QabxV2Qj4= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-for-of@^6.23.0: - version "6.23.0" - resolved "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz" - integrity sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-function-name@^6.22.0: - version "6.24.1" - resolved "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz" - integrity sha1-g0yJhTvDaxrw86TF26qU/Y6sqos= - dependencies: - babel-helper-function-name "^6.24.1" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-literals@^6.22.0: - version "6.22.0" - resolved "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz" - integrity sha1-T1SgLWzWbPkVKAAZox0xklN3yi4= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.24.1: - version "6.24.1" - resolved "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz" - integrity sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ= - dependencies: - babel-plugin-transform-es2015-modules-commonjs "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1: - version "6.26.2" - resolved "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz" - integrity sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q== - dependencies: - babel-plugin-transform-strict-mode "^6.24.1" - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-types "^6.26.0" - -babel-plugin-transform-es2015-modules-systemjs@^6.23.0: - version "6.24.1" - resolved "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz" - integrity sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM= - dependencies: - babel-helper-hoist-variables "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-modules-umd@^6.23.0: - version "6.24.1" - resolved "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz" - integrity sha1-rJl+YoXNGO1hdq22B9YCNErThGg= - dependencies: - babel-plugin-transform-es2015-modules-amd "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-object-super@^6.22.0: - version "6.24.1" - resolved "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz" - integrity sha1-JM72muIcuDp/hgPa0CH1cusnj40= - dependencies: - babel-helper-replace-supers "^6.24.1" - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-parameters@^6.23.0: - version "6.24.1" - resolved "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz" - integrity sha1-V6w1GrScrxSpfNE7CfZv3wpiXys= - dependencies: - babel-helper-call-delegate "^6.24.1" - babel-helper-get-function-arity "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-shorthand-properties@^6.22.0: - version "6.24.1" - resolved "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz" - integrity sha1-JPh11nIch2YbvZmkYi5R8U3jiqA= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-spread@^6.22.0: - version "6.22.0" - resolved "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz" - integrity sha1-1taKmfia7cRTbIGlQujdnxdG+NE= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-sticky-regex@^6.22.0: - version "6.24.1" - resolved "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz" - integrity sha1-AMHNsaynERLN8M9hJsLta0V8zbw= - dependencies: - babel-helper-regex "^6.24.1" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-template-literals@^6.22.0: - version "6.22.0" - resolved "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz" - integrity sha1-qEs0UPfp+PH2g51taH2oS7EjbY0= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-typeof-symbol@^6.23.0: - version "6.23.0" - resolved "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz" - integrity sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-unicode-regex@^6.22.0: - version "6.24.1" - resolved "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz" - integrity sha1-04sS9C6nMj9yk4fxinxa4frrNek= - dependencies: - babel-helper-regex "^6.24.1" - babel-runtime "^6.22.0" - regexpu-core "^2.0.0" - -babel-plugin-transform-exponentiation-operator@^6.22.0: - version "6.24.1" - resolved "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz" - integrity sha1-KrDJx/MJj6SJB3cruBP+QejeOg4= - dependencies: - babel-helper-builder-binary-assignment-operator-visitor "^6.24.1" - babel-plugin-syntax-exponentiation-operator "^6.8.0" - babel-runtime "^6.22.0" - -babel-plugin-transform-regenerator@^6.22.0: - version "6.26.0" - resolved "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz" - integrity sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8= - dependencies: - regenerator-transform "^0.10.0" - -babel-plugin-transform-strict-mode@^6.24.1: - version "6.24.1" - resolved "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz" - integrity sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-preset-env@^1.7.0: - version "1.7.0" - resolved "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.7.0.tgz" - integrity sha512-9OR2afuKDneX2/q2EurSftUYM0xGu4O2D9adAhVfADDhrYDaxXV0rBbevVYoY9n6nyX1PmQW/0jtpJvUNr9CHg== - dependencies: - babel-plugin-check-es2015-constants "^6.22.0" - babel-plugin-syntax-trailing-function-commas "^6.22.0" - babel-plugin-transform-async-to-generator "^6.22.0" - babel-plugin-transform-es2015-arrow-functions "^6.22.0" - babel-plugin-transform-es2015-block-scoped-functions "^6.22.0" - babel-plugin-transform-es2015-block-scoping "^6.23.0" - babel-plugin-transform-es2015-classes "^6.23.0" - babel-plugin-transform-es2015-computed-properties "^6.22.0" - babel-plugin-transform-es2015-destructuring "^6.23.0" - babel-plugin-transform-es2015-duplicate-keys "^6.22.0" - babel-plugin-transform-es2015-for-of "^6.23.0" - babel-plugin-transform-es2015-function-name "^6.22.0" - babel-plugin-transform-es2015-literals "^6.22.0" - babel-plugin-transform-es2015-modules-amd "^6.22.0" - babel-plugin-transform-es2015-modules-commonjs "^6.23.0" - babel-plugin-transform-es2015-modules-systemjs "^6.23.0" - babel-plugin-transform-es2015-modules-umd "^6.23.0" - babel-plugin-transform-es2015-object-super "^6.22.0" - babel-plugin-transform-es2015-parameters "^6.23.0" - babel-plugin-transform-es2015-shorthand-properties "^6.22.0" - babel-plugin-transform-es2015-spread "^6.22.0" - babel-plugin-transform-es2015-sticky-regex "^6.22.0" - babel-plugin-transform-es2015-template-literals "^6.22.0" - babel-plugin-transform-es2015-typeof-symbol "^6.23.0" - babel-plugin-transform-es2015-unicode-regex "^6.22.0" - babel-plugin-transform-exponentiation-operator "^6.22.0" - babel-plugin-transform-regenerator "^6.22.0" - browserslist "^3.2.6" - invariant "^2.2.2" - semver "^5.3.0" - -babel-register@^6.26.0: - version "6.26.0" - resolved "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz" - integrity sha1-btAhFz4vy0htestFxgCahW9kcHE= - dependencies: - babel-core "^6.26.0" - babel-runtime "^6.26.0" - core-js "^2.5.0" - home-or-tmp "^2.0.0" - lodash "^4.17.4" - mkdirp "^0.5.1" - source-map-support "^0.4.15" - -babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: - version "6.26.0" - resolved "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz" - integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= - dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.11.0" - -babel-template@^6.24.1, babel-template@^6.26.0: - version "6.26.0" - resolved "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz" - integrity sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI= - dependencies: - babel-runtime "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - lodash "^4.17.4" - -babel-traverse@^6.24.1, babel-traverse@^6.26.0: - version "6.26.0" - resolved "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz" - integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4= - dependencies: - babel-code-frame "^6.26.0" - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - debug "^2.6.8" - globals "^9.18.0" - invariant "^2.2.2" - lodash "^4.17.4" - -babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0: - version "6.26.0" - resolved "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz" - integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc= - dependencies: - babel-runtime "^6.26.0" - esutils "^2.0.2" - lodash "^4.17.4" - to-fast-properties "^1.0.3" - -babelify@^7.3.0: - version "7.3.0" - resolved "https://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz" - integrity sha1-qlau3nBn/XvVSWZu4W3ChQh+iOU= - dependencies: - babel-core "^6.0.14" - object-assign "^4.0.0" - -babylon@^6.18.0: - version "6.18.0" - resolved "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz" - integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== - -backoff@^2.5.0: - version "2.5.0" - resolved "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz" - integrity sha1-9hbtqdPktmuMp/ynn2lXIsX44m8= - dependencies: - precond "0.2" +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.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base-x@^3.0.2, base-x@^3.0.8: - version "3.0.9" - resolved "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz" - 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.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -base@^0.11.1: - version "0.11.2" - resolved "https://registry.npmjs.org/base/-/base-0.11.2.tgz" - integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== - dependencies: - cache-base "^1.0.1" - class-utils "^0.3.5" - component-emitter "^1.2.1" - define-property "^1.0.0" - isobject "^3.0.1" - mixin-deep "^1.2.0" - pascalcase "^0.1.1" - -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz" - integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= - dependencies: - tweetnacl "^0.14.3" - bech32@1.1.4: version "1.1.4" - resolved "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz" + resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== -bignumber.js@^9.0.0, bignumber.js@^9.0.1: - version "9.0.2" - resolved "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz" - integrity sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw== - binary-extensions@^2.0.0: version "2.2.0" - resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -bip39@2.5.0: - version "2.5.0" - resolved "https://registry.npmjs.org/bip39/-/bip39-2.5.0.tgz" - integrity sha512-xwIx/8JKoT2+IPJpFEfXoWdYwP7UVAoUxxLNfGCfVowaJE7yg1Y5B1BVPqlUNsBq5/nGwmFkwRJ8xDW4sX8OdA== - dependencies: - create-hash "^1.1.0" - pbkdf2 "^3.0.9" - randombytes "^2.0.1" - safe-buffer "^5.0.1" - unorm "^1.3.3" - -blakejs@^1.1.0: - version "1.2.1" - resolved "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz" - integrity sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ== - -bluebird@^3.5.0, bluebird@^3.5.2: - version "3.7.2" - resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz" - integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== - -bn.js@4.11.6: - version "4.11.6" - resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz" - integrity sha1-UzRK2xRhehP26N0s4okF0cC6MhU= - -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.10.0, bn.js@^4.11.0, bn.js@^4.11.6, bn.js@^4.11.8, bn.js@^4.11.9, bn.js@^4.8.0: +bn.js@^4.11.9: version "4.12.0" - resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== -bn.js@^5.0.0, bn.js@^5.1.1, bn.js@^5.1.2: - version "5.2.0" - resolved "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz" - integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw== - -bn.js@^5.2.0: +bn.js@^5.2.0, bn.js@^5.2.1: version "5.2.1" - resolved "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== -body-parser@1.19.2: - version "1.19.2" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz" - integrity sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw== - dependencies: - bytes "3.1.2" - content-type "~1.0.4" - debug "2.6.9" - depd "~1.1.2" - http-errors "1.8.1" - iconv-lite "0.4.24" - on-finished "~2.3.0" - qs "6.9.7" - raw-body "2.4.3" - type-is "~1.6.18" - -body-parser@^1.16.0: - version "1.20.0" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz" - integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg== - dependencies: - bytes "3.1.2" - content-type "~1.0.4" - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - http-errors "2.0.0" - iconv-lite "0.4.24" - on-finished "2.4.1" - qs "6.10.3" - raw-body "2.5.1" - type-is "~1.6.18" - unpipe "1.0.0" - brace-expansion@^1.1.7: version "1.1.11" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^2.3.1: - version "2.3.2" - resolved "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz" - integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== - dependencies: - arr-flatten "^1.1.0" - array-unique "^0.3.2" - extend-shallow "^2.0.1" - fill-range "^4.0.0" - isobject "^3.0.1" - repeat-element "^1.1.2" - snapdragon "^0.8.1" - snapdragon-node "^2.0.1" - split-string "^3.0.2" - to-regex "^3.0.1" +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, braces@~3.0.2: version "3.0.2" - resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" + 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.0.1, brorand@^1.1.0: +brorand@^1.1.0: version "1.1.0" - resolved "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz" - integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== browser-stdout@1.3.1: version "1.3.1" - resolved "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== -browserify-aes@^1.0.0, browserify-aes@^1.0.4, browserify-aes@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz" - 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" - -browserify-cipher@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz" - integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz" - integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: - version "4.1.0" - resolved "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz" - integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog== - dependencies: - bn.js "^5.0.0" - randombytes "^2.0.1" - -browserify-sign@^4.0.0: - version "4.2.1" - resolved "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz" - integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg== - dependencies: - bn.js "^5.1.1" - browserify-rsa "^4.0.1" - create-hash "^1.2.0" - create-hmac "^1.1.7" - elliptic "^6.5.3" - inherits "^2.0.4" - parse-asn1 "^5.1.5" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -browserslist@^3.2.6: - version "3.2.8" - resolved "https://registry.npmjs.org/browserslist/-/browserslist-3.2.8.tgz" - integrity sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ== - dependencies: - caniuse-lite "^1.0.30000844" - electron-to-chromium "^1.3.47" - -bs58@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz" - integrity sha1-vhYedsNU9veIrkBx9j806MTwpCo= - dependencies: - base-x "^3.0.2" - -bs58check@^2.1.2: - version "2.1.2" - resolved "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz" - integrity sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA== - dependencies: - bs58 "^4.0.0" - create-hash "^1.1.0" - safe-buffer "^5.1.2" - -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - buffer-reverse@^1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/buffer-reverse/-/buffer-reverse-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/buffer-reverse/-/buffer-reverse-1.0.1.tgz#49283c8efa6f901bc01fa3304d06027971ae2f60" integrity sha512-M87YIUBsZ6N924W57vDwT/aOu8hw7ZgdByz6ijksLjmHJELBASmYTTlNHRgjE+pTsT9oJXGaDSgqqwfdHotDUg== -buffer-to-arraybuffer@^0.0.5: - version "0.0.5" - resolved "https://registry.npmjs.org/buffer-to-arraybuffer/-/buffer-to-arraybuffer-0.0.5.tgz" - integrity sha1-YGSkD6dutDxyOrqe+PbhIW0QURo= - -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz" - integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= - -buffer-xor@^2.0.1: - version "2.0.2" - resolved "https://registry.npmjs.org/buffer-xor/-/buffer-xor-2.0.2.tgz" - integrity sha512-eHslX0bin3GB+Lx2p7lEYRShRewuNZL3fUl4qlVJGGiwoPGftmt8JQgk2Y9Ji5/01TnVDo33E5b5O3vUB1HdqQ== - dependencies: - safe-buffer "^5.1.1" - -buffer@^5.0.5, buffer@^5.2.1, buffer@^5.5.0, buffer@^5.6.0: - version "5.7.1" - resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - buffer@^6.0.3: version "6.0.3" - resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz" + 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" -bufferutil@^4.0.1: - version "4.0.6" - resolved "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.6.tgz" - integrity sha512-jduaYOYtnio4aIAyc6UbvPCVcgq7nYpVnucyxr6eCYg/Woad9Hf/oxxBRDnGGjPfjUm6j5O/uBWhIu4iLebFaw== - dependencies: - node-gyp-build "^4.3.0" - bundle-require@^3.0.2: - version "3.0.4" - resolved "https://registry.npmjs.org/bundle-require/-/bundle-require-3.0.4.tgz" - integrity sha512-VXG6epB1yrLAvWVQpl92qF347/UXmncQj7J3U8kZEbdVZ1ZkQyr4hYeL/9RvcE8vVVdp53dY78Fd/3pqfRqI1A== - dependencies: - load-tsconfig "^0.2.0" - -bytes@3.1.2: version "3.1.2" - resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" - integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== - -bytewise-core@^1.2.2: - version "1.2.3" - resolved "https://registry.npmjs.org/bytewise-core/-/bytewise-core-1.2.3.tgz" - integrity sha1-P7QQx+kVWOsasiqCg0V3qmvWHUI= - dependencies: - typewise-core "^1.2" - -bytewise@~1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/bytewise/-/bytewise-1.1.0.tgz" - integrity sha1-HRPL/3F65xWAlKqIGzXQgbOHJT4= + resolved "https://registry.yarnpkg.com/bundle-require/-/bundle-require-3.1.2.tgz#1374a7bdcb8b330a7ccc862ccbf7c137cc43ad27" + integrity sha512-Of6l6JBAxiyQ5axFxUM6dYeP/W7X2Sozeo/4EYB9sJhL+dqL7TKjg+shwxp6jlu/6ZSERfsYtIpSJ1/x3XkAEA== dependencies: - bytewise-core "^1.2.2" - typewise "^1.0.3" + load-tsconfig "^0.2.0" cac@^6.7.12: - version "6.7.12" - resolved "https://registry.npmjs.org/cac/-/cac-6.7.12.tgz" - integrity sha512-rM7E2ygtMkJqD9c7WnFU6fruFcN3xe4FM5yUmgxhZzIKJk4uHl9U/fhwdajGFQbQuv43FAUo1Fe8gX/oIKDeSA== - -cache-base@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz" - integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== - dependencies: - collection-visit "^1.0.0" - component-emitter "^1.2.1" - get-value "^2.0.6" - has-value "^1.0.0" - isobject "^3.0.1" - set-value "^2.0.0" - to-object-path "^0.3.0" - union-value "^1.0.0" - unset-value "^1.0.0" - -cacheable-request@^6.0.0: - version "6.1.0" - resolved "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz" - integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== - dependencies: - clone-response "^1.0.2" - get-stream "^5.1.0" - http-cache-semantics "^4.0.0" - keyv "^3.0.0" - lowercase-keys "^2.0.0" - normalize-url "^4.1.0" - responselike "^1.0.2" - -cachedown@1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/cachedown/-/cachedown-1.0.0.tgz" - integrity sha1-1D8DbkUQaWsxJG19sx6/D3rDLRU= - dependencies: - abstract-leveldown "^2.4.1" - lru-cache "^3.2.0" - -call-bind@^1.0.0, call-bind@^1.0.2, call-bind@~1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== - dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" - -caller-callsite@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz" - integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ= - dependencies: - callsites "^2.0.0" - -caller-path@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz" - integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ= - dependencies: - caller-callsite "^2.0.0" - -callsites@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz" - integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= + version "6.7.14" + resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" + integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== callsites@^3.0.0: version "3.1.0" - resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camelcase@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz" - integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo= - -camelcase@^5.0.0: - version "5.3.1" - resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - camelcase@^6.0.0: version "6.3.0" - resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30000844: - version "1.0.30001328" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001328.tgz" - integrity sha512-Ue55jHkR/s4r00FLNiX+hGMMuwml/QGqqzVeMQ5thUewznU2EdULFvI3JR7JJid6OrjJNfFvHY2G2dIjmRaDDQ== - -caseless@^0.12.0, caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= - -cbor@^5.0.2: - version "5.2.0" - resolved "https://registry.npmjs.org/cbor/-/cbor-5.2.0.tgz" - integrity sha512-5IMhi9e1QU76ppa5/ajP1BmMWZ2FHkhAhjeVKQ/EFCgYSEaeVaoGtL7cxJskf9oCCk+XjzaIdc3IuU/dbA/o2A== - dependencies: - bignumber.js "^9.0.1" - nofilter "^1.0.4" - -chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chalk@^2.0.0, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: +chalk@^2.4.2: version "2.4.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" + 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.0.0, chalk@^4.1.0: +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + 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" -chardet@^0.7.0: - version "0.7.0" - resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz" - integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== - -"charenc@>= 0.0.1": - version "0.0.2" - resolved "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz" - integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= - -checkpoint-store@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/checkpoint-store/-/checkpoint-store-1.1.0.tgz" - integrity sha1-BOTLUWuRQziTWB5tRgGnjpVS6gY= - dependencies: - functional-red-black-tree "^1.0.1" - -chokidar@3.3.0: - version "3.3.0" - resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz" - integrity sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A== - dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.2.0" - optionalDependencies: - fsevents "~2.1.1" - -chokidar@3.5.3, chokidar@^3.4.0, chokidar@^3.5.1: +chokidar@3.5.3, chokidar@^3.5.1: version "3.5.3" - resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== dependencies: anymatch "~3.1.2" @@ -2521,191 +970,42 @@ chokidar@3.5.3, chokidar@^3.4.0, chokidar@^3.5.1: optionalDependencies: fsevents "~2.3.2" -chownr@^1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz" - integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== - -ci-info@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz" - integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== - -cids@^0.7.1: - version "0.7.5" - resolved "https://registry.npmjs.org/cids/-/cids-0.7.5.tgz" - integrity sha512-zT7mPeghoWAu+ppn8+BS1tQ5qGmbMfB4AregnQjA/qHY3GC1m1ptI9GkWNlgeu38r7CuRdXB47uY2XgAYt6QVA== - dependencies: - buffer "^5.5.0" - class-is "^1.1.0" - multibase "~0.6.0" - multicodec "^1.0.0" - multihashes "~0.4.15" - -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.4" - resolved "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz" - integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -class-is@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/class-is/-/class-is-1.1.0.tgz" - integrity sha512-rhjH9AG1fvabIDoGRVH587413LPjTZgmDF9fOFCbFJQV4yuocX1mHxxvXI4g3cGwbVY9wAYIoKlg1N79frJKQw== - -class-utils@^0.3.5: - version "0.3.6" - resolved "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz" - integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== - dependencies: - arr-union "^3.1.0" - define-property "^0.2.5" - isobject "^3.0.0" - static-extend "^0.1.1" - -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== - -cli-cursor@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz" - integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= - dependencies: - restore-cursor "^2.0.0" - -cli-table3@^0.5.0: - version "0.5.1" - resolved "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz" - integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw== - dependencies: - object-assign "^4.1.0" - string-width "^2.1.1" - optionalDependencies: - colors "^1.1.2" - -cli-table3@^0.6.0: - version "0.6.1" - resolved "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.1.tgz" - integrity sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA== - dependencies: - string-width "^4.2.0" - optionalDependencies: - colors "1.4.0" - -cli-width@^2.0.0: - version "2.2.1" - resolved "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz" - integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw== - -cliui@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz" - integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - wrap-ansi "^2.0.0" - -cliui@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz" - integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== - dependencies: - string-width "^3.1.0" - strip-ansi "^5.2.0" - wrap-ansi "^5.1.0" - cliui@^7.0.2: version "7.0.4" - resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== dependencies: string-width "^4.2.0" strip-ansi "^6.0.0" wrap-ansi "^7.0.0" -clone-response@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz" - integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= - dependencies: - mimic-response "^1.0.0" - -clone@2.1.2, clone@^2.0.0: - version "2.1.2" - resolved "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz" - integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= - -collection-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz" - integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= - dependencies: - map-visit "^1.0.0" - object-visit "^1.0.0" - color-convert@^1.9.0: version "1.9.3" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" + 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.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + 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.npmjs.org/color-name/-/color-name-1.1.3.tgz" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + 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.npmjs.org/color-name/-/color-name-1.1.4.tgz" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -colors@1.4.0, colors@^1.1.2: - version "1.4.0" - resolved "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz" - integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== - -combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: - version "1.0.8" - resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -command-exists@^1.2.8: - version "1.2.9" - resolved "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz" - integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w== - -command-line-args@^4.0.7: - version "4.0.7" - resolved "https://registry.npmjs.org/command-line-args/-/command-line-args-4.0.7.tgz" - integrity sha512-aUdPvQRAyBvQd2n7jXcsMDz68ckBJELXNzBybCHOibUWEg0mWTnaYCSRU8h9R+aNRSvDihJtssSRCiDRpLaezA== - dependencies: - array-back "^2.0.0" - find-replace "^1.0.3" - typical "^2.6.1" - command-line-args@^5.1.1: version "5.2.1" - resolved "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz" + resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.1.tgz#c44c32e437a57d7c51157696893c5909e9cec42e" integrity sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg== dependencies: array-back "^3.1.0" @@ -2714,519 +1014,120 @@ command-line-args@^5.1.1: typical "^4.0.0" command-line-usage@^6.1.0: - version "6.1.2" - resolved "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.2.tgz" - integrity sha512-I+0XN613reAhpBQ6icsPOTwu9cvhc9NtLtUcY2fGYuwm9JZiWBzFDA8w0PHqQjru7Xth7fM/y9TJ13+VKdjh7Q== + version "6.1.3" + resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-6.1.3.tgz#428fa5acde6a838779dfa30e44686f4b6761d957" + integrity sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw== dependencies: - array-back "^4.0.1" + array-back "^4.0.2" chalk "^2.4.2" - table-layout "^1.0.1" + table-layout "^1.0.2" typical "^5.2.0" -commander@2.18.0: - version "2.18.0" - resolved "https://registry.npmjs.org/commander/-/commander-2.18.0.tgz" - integrity sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ== - -commander@3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz" - integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== +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== commander@^4.0.0: version "4.1.1" - resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== -component-emitter@^1.2.1: - version "1.3.0" - resolved "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz" - integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== - concat-map@0.0.1: version "0.0.1" - resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -concat-stream@^1.5.1, concat-stream@^1.6.0, concat-stream@^1.6.2: - version "1.6.2" - resolved "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz" - integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - -content-disposition@0.5.4: - version "0.5.4" - resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" - integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== - dependencies: - safe-buffer "5.2.1" - -content-hash@^2.5.2: - version "2.5.2" - resolved "https://registry.npmjs.org/content-hash/-/content-hash-2.5.2.tgz" - integrity sha512-FvIQKy0S1JaWV10sMsA7TRx8bpU+pqPkhbsfvOJAdjRXvYxEckAwQWGwtRjiaJfh+E0DvcWUGqcdjwMGFjsSdw== - dependencies: - cids "^0.7.1" - multicodec "^0.5.5" - multihashes "^0.4.15" - -content-type@~1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz" - integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== - -convert-source-map@^1.5.1: - version "1.8.0" - resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz" - integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== - dependencies: - safe-buffer "~5.1.1" - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" - integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= - -cookie@0.4.2, cookie@^0.4.1: - version "0.4.2" - resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz" - integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== - -cookiejar@^2.1.1: - version "2.1.3" - resolved "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz" - integrity sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ== - -copy-descriptor@^0.1.0: - version "0.1.1" - resolved "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz" - integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= - -core-js-pure@^3.0.1: - version "3.21.1" - resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.21.1.tgz" - integrity sha512-12VZfFIu+wyVbBebyHmRTuEE/tZrB4tJToWcwAMcsp3h4+sHR+fMJWbKpYiCRWlhFBq+KNyO8rIV9rTkeVmznQ== - -core-js@^2.4.0, core-js@^2.5.0: - version "2.6.12" - resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz" - integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== - -core-util-is@1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -core-util-is@~1.0.0: - version "1.0.3" - resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" - integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== - -cors@^2.8.1: - version "2.8.5" - resolved "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz" - integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== - dependencies: - object-assign "^4" - vary "^1" - -cosmiconfig@^5.0.7: - version "5.2.1" - resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz" - integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== - dependencies: - import-fresh "^2.0.0" - is-directory "^0.3.1" - js-yaml "^3.13.1" - parse-json "^4.0.0" - -crc-32@^1.2.0: - version "1.2.2" - resolved "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz" - integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== - -create-ecdh@^4.0.0: - version "4.0.4" - resolved "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz" - integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== - dependencies: - bn.js "^4.1.0" - elliptic "^6.5.3" - -create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz" - 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" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: - version "1.1.7" - resolved "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz" - integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== +cosmiconfig@^8.0.0: + version "8.3.6" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" + integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== 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" + import-fresh "^3.3.0" + js-yaml "^4.1.0" + parse-json "^5.2.0" + path-type "^4.0.0" create-require@^1.1.0: version "1.1.1" - resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-fetch@^2.1.0, cross-fetch@^2.1.1: - version "2.2.6" - resolved "https://registry.npmjs.org/cross-fetch/-/cross-fetch-2.2.6.tgz" - integrity sha512-9JZz+vXCmfKUZ68zAptS7k4Nu8e2qcibe7WVZYps7sAgk5R8GYTc+T1WR0v1rlP9HxgARmOX1UTIJZFytajpNA== - dependencies: - node-fetch "^2.6.7" - whatwg-fetch "^2.0.4" - -cross-spawn@^6.0.5: - version "6.0.5" - resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" - -cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" - resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" + 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" -"crypt@>= 0.0.1": - version "0.0.2" - resolved "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz" - integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= - -crypto-browserify@3.12.0: - version "3.12.0" - resolved "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz" - integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== - dependencies: - browserify-cipher "^1.0.0" - browserify-sign "^4.0.0" - create-ecdh "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.0" - diffie-hellman "^5.0.0" - inherits "^2.0.1" - pbkdf2 "^3.0.3" - public-encrypt "^4.0.0" - randombytes "^2.0.0" - randomfill "^1.0.3" - -crypto-js@^3.1.9-1: - version "3.3.0" - resolved "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz" - integrity sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q== - -d@1, d@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/d/-/d-1.0.1.tgz" - integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== - dependencies: - es5-ext "^0.10.50" - type "^1.0.1" - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz" - integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= - dependencies: - assert-plus "^1.0.0" - -debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: - version "2.6.9" - resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@3.2.6: - version "3.2.6" - resolved "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== - dependencies: - ms "^2.1.1" - -debug@4, debug@^4.0.1, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3: - version "4.3.4" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - debug@4.3.3: version "4.3.3" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== dependencies: ms "2.1.2" -debug@^3.1.0: - version "3.2.7" - resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== +debug@^4.3.1, debug@^4.3.2, 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.1" - -decamelize@^1.1.1, decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + ms "2.1.2" decamelize@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= - -decompress-response@^3.2.0, decompress-response@^3.3.0: - version "3.3.0" - resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz" - integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M= - dependencies: - mimic-response "^1.0.0" - -deep-equal@~1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz" - integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== - dependencies: - is-arguments "^1.0.4" - is-date-object "^1.0.1" - is-regex "^1.0.4" - object-is "^1.0.1" - object-keys "^1.1.1" - regexp.prototype.flags "^1.2.0" - deep-extend@~0.6.0: version "0.6.0" - resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== -deep-is@^0.1.3, deep-is@~0.1.3: +deep-is@^0.1.3: version "0.1.4" - resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== -defer-to-connect@^1.0.1: - version "1.1.3" - resolved "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz" - integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== - -deferred-leveldown@~1.2.1: - version "1.2.2" - resolved "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-1.2.2.tgz" - integrity sha512-uukrWD2bguRtXilKt6cAWKyoXrTSMo5m7crUdLfWQmu8kIm88w3QZoUL+6nhpfKVmhHANER6Re3sKoNoZ3IKMA== - dependencies: - abstract-leveldown "~2.6.0" +diff@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== -deferred-leveldown@~4.0.0: +diff@^4.0.1: version "4.0.2" - resolved "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-4.0.2.tgz" - integrity sha512-5fMC8ek8alH16QiV0lTCis610D1Zt1+LA4MS4d63JgS32lrCjTFDUFz2ao09/j2I4Bqb5jL4FZYwu7Jz0XO1ww== - dependencies: - abstract-leveldown "~5.0.0" - inherits "^2.0.3" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -deferred-leveldown@~5.3.0: - version "5.3.0" - resolved "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-5.3.0.tgz" - integrity sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw== +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== dependencies: - abstract-leveldown "~6.2.1" - inherits "^2.0.3" + path-type "^4.0.0" -define-properties@^1.1.2, define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== dependencies: - object-keys "^1.0.12" + esutils "^2.0.2" -define-property@^0.2.5: - version "0.2.5" - resolved "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz" - integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= - dependencies: - is-descriptor "^0.1.0" +dotenv@^16.3.1: + version "16.4.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.1.tgz#1d9931f1d3e5d2959350d1250efab299561f7f11" + integrity sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ== -define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz" - integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= - dependencies: - is-descriptor "^1.0.0" +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== -define-property@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz" - integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== - dependencies: - is-descriptor "^1.0.2" - isobject "^3.0.1" - -defined@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz" - integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= - -delete-empty@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/delete-empty/-/delete-empty-3.0.0.tgz" - integrity sha512-ZUyiwo76W+DYnKsL3Kim6M/UOavPdBJgDYWOmuQhYaZvJH0AXAHbUNyEDtRbBra8wqqr686+63/0azfEk1ebUQ== - dependencies: - ansi-colors "^4.1.0" - minimist "^1.2.0" - path-starts-with "^2.0.0" - rimraf "^2.6.2" - -depd@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= - -des.js@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz" - integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -destroy@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" - integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== - -destroy@~1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz" - integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= - -detect-indent@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz" - integrity sha1-920GQ1LN9Docts5hnE7jqUdd4gg= - dependencies: - repeating "^2.0.0" - -diff@3.5.0: - version "3.5.0" - resolved "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz" - integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== - -diff@5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz" - integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== - -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - -diffie-hellman@^5.0.0: - version "5.0.3" - resolved "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz" - integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - -dom-walk@^0.1.0: - version "0.1.2" - resolved "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz" - integrity sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w== - -dotenv@^16.0.0: - version "16.0.0" - resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.0.0.tgz" - integrity sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q== - -dotignore@~0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/dotignore/-/dotignore-0.1.2.tgz" - integrity sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw== - dependencies: - minimatch "^3.0.4" - -duplexer3@^0.1.4: - version "0.1.4" - resolved "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz" - integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= - -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz" - integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" - integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= - -electron-to-chromium@^1.3.47: - version "1.4.107" - resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.107.tgz" - integrity sha512-Huen6taaVrUrSy8o7mGStByba8PfOWWluHNxSHGBrCgEdFVLtvdQDBr9LBCF9Uci8SYxh28QNNMO0oC17wbGAg== - -elliptic@6.5.4, elliptic@^6.4.0, elliptic@^6.5.2, elliptic@^6.5.3, elliptic@^6.5.4: +elliptic@6.5.4: version "6.5.4" - resolved "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz" + 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" @@ -3237,76 +1138,19 @@ elliptic@6.5.4, elliptic@^6.4.0, elliptic@^6.5.2, elliptic@^6.5.3, elliptic@^6.5 minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" -emoji-regex@^10.0.0: - version "10.1.0" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.1.0.tgz" - integrity sha512-xAEnNCT3w2Tg6MA7ly6QqYJvEoY1tm9iIjJ3yMKK9JPlWuRHAMoe5iETwQnx3M9TVbFMfsrBgWKR+IsmswwNjg== - -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== - emoji-regex@^8.0.0: version "8.0.0" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" - integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= - -encoding-down@5.0.4, encoding-down@~5.0.0: - version "5.0.4" - resolved "https://registry.npmjs.org/encoding-down/-/encoding-down-5.0.4.tgz" - integrity sha512-8CIZLDcSKxgzT+zX8ZVfgNbu8Md2wq/iqa1Y7zyVR18QBEAc0Nmzuvj/N5ykSKpfGzjM8qxbaFntLPwnVoUhZw== - dependencies: - abstract-leveldown "^5.0.0" - inherits "^2.0.3" - level-codec "^9.0.0" - level-errors "^2.0.0" - xtend "^4.0.1" - -encoding-down@^6.3.0: - version "6.3.0" - resolved "https://registry.npmjs.org/encoding-down/-/encoding-down-6.3.0.tgz" - integrity sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw== - dependencies: - abstract-leveldown "^6.2.1" - inherits "^2.0.3" - level-codec "^9.0.0" - level-errors "^2.0.0" - -encoding@^0.1.11: - version "0.1.13" - resolved "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz" - integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== - dependencies: - iconv-lite "^0.6.2" - -end-of-stream@^1.1.0: - version "1.4.4" - resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -enquirer@^2.3.0: - version "2.3.6" - resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz" - integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== - dependencies: - ansi-colors "^4.1.1" - -env-paths@^2.2.0: - version "2.2.1" - resolved "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz" - integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== +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@^3.3.0: version "3.3.0" - resolved "https://registry.npmjs.org/erc721a-upgradeable/-/erc721a-upgradeable-3.3.0.tgz" + resolved "https://registry.yarnpkg.com/erc721a-upgradeable/-/erc721a-upgradeable-3.3.0.tgz#c7b481668694756120868261fe98ab3a245a06b4" integrity sha512-ILE0SjKuvhx+PABG0A/41QUp0MFiYmzrgo71htQ0Ov6JfDOmgUzGxDW8gZuYfKrdlYjNwSAqMpUFWBbyW3sWBA== dependencies: "@openzeppelin/contracts-upgradeable" "^4.4.2" @@ -3318,929 +1162,302 @@ erc721a@3.3.0: dependencies: "@openzeppelin/contracts" "^4.4.2" -errno@~0.1.1: - version "0.1.8" - resolved "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz" - integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== - dependencies: - prr "~1.0.1" - -error-ex@^1.2.0, error-ex@^1.3.1: +error-ex@^1.3.1: version "1.3.2" - resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" + 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" -es-abstract@^1.19.1: - version "1.19.4" - resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.4.tgz" - integrity sha512-flV8e5g9/xulChMG48Fygk1ptpo4lQRJ0eJYtxJFgi7pklLx7EFcOJ34jnvr8pbWlaFN/AT1cZpe0hiFel9Hqg== - dependencies: - call-bind "^1.0.2" - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - get-intrinsic "^1.1.1" - get-symbol-description "^1.0.0" - has "^1.0.3" - has-symbols "^1.0.3" - internal-slot "^1.0.3" - is-callable "^1.2.4" - is-negative-zero "^2.0.2" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.2" - is-string "^1.0.7" - is-weakref "^1.0.2" - object-inspect "^1.12.0" - object-keys "^1.1.1" - object.assign "^4.1.2" - string.prototype.trimend "^1.0.4" - string.prototype.trimstart "^1.0.4" - unbox-primitive "^1.0.1" - -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -es5-ext@^0.10.35, es5-ext@^0.10.50: - version "0.10.60" - resolved "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.60.tgz" - integrity sha512-jpKNXIt60htYG59/9FGf2PYT3pwMpnEbNKysU+k/4FGwyGtMotOvcZOuW+EmXXYASRqYSXQfGL5cVIthOTgbkg== - dependencies: - es6-iterator "^2.0.3" - es6-symbol "^3.1.3" - next-tick "^1.1.0" - -es6-iterator@^2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz" - integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= - dependencies: - d "1" - es5-ext "^0.10.35" - es6-symbol "^3.1.1" - -es6-symbol@^3.1.1, es6-symbol@^3.1.3: - version "3.1.3" - resolved "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz" - integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== - dependencies: - d "^1.0.1" - ext "^1.1.2" - -esbuild-android-64@0.14.36: - version "0.14.36" - resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.36.tgz#fc5f95ce78c8c3d790fa16bc71bd904f2bb42aa1" - integrity sha512-jwpBhF1jmo0tVCYC/ORzVN+hyVcNZUWuozGcLHfod0RJCedTDTvR4nwlTXdx1gtncDqjk33itjO+27OZHbiavw== - -esbuild-android-arm64@0.14.36: - version "0.14.36" - resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.36.tgz#44356fbb9f8de82a5cdf11849e011dfb3ad0a8a8" - integrity sha512-/hYkyFe7x7Yapmfv4X/tBmyKnggUmdQmlvZ8ZlBnV4+PjisrEhAvC3yWpURuD9XoB8Wa1d5dGkTsF53pIvpjsg== - -esbuild-darwin-64@0.14.36: - version "0.14.36" - resolved "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.36.tgz" - integrity sha512-kkl6qmV0dTpyIMKagluzYqlc1vO0ecgpviK/7jwPbRDEv5fejRTaBBEE2KxEQbTHcLhiiDbhG7d5UybZWo/1zQ== - -esbuild-darwin-arm64@0.14.36: - version "0.14.36" - resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.36.tgz#2a8040c2e465131e5281034f3c72405e643cb7b2" - integrity sha512-q8fY4r2Sx6P0Pr3VUm//eFYKVk07C5MHcEinU1BjyFnuYz4IxR/03uBbDwluR6ILIHnZTE7AkTUWIdidRi1Jjw== - -esbuild-freebsd-64@0.14.36: - version "0.14.36" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.36.tgz#d82c387b4d01fe9e8631f97d41eb54f2dbeb68a3" - integrity sha512-Hn8AYuxXXRptybPqoMkga4HRFE7/XmhtlQjXFHoAIhKUPPMeJH35GYEUWGbjteai9FLFvBAjEAlwEtSGxnqWww== - -esbuild-freebsd-arm64@0.14.36: - version "0.14.36" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.36.tgz#e8ce2e6c697da6c7ecd0cc0ac821d47c5ab68529" - integrity sha512-S3C0attylLLRiCcHiJd036eDEMOY32+h8P+jJ3kTcfhJANNjP0TNBNL30TZmEdOSx/820HJFgRrqpNAvTbjnDA== - -esbuild-linux-32@0.14.36: - version "0.14.36" - resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.36.tgz#a4a261e2af91986ea62451f2db712a556cb38a15" - integrity sha512-Eh9OkyTrEZn9WGO4xkI3OPPpUX7p/3QYvdG0lL4rfr73Ap2HAr6D9lP59VMF64Ex01LhHSXwIsFG/8AQjh6eNw== - -esbuild-linux-64@0.14.36: - version "0.14.36" - resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.36.tgz#4a9500f9197e2c8fcb884a511d2c9d4c2debde72" - integrity sha512-vFVFS5ve7PuwlfgoWNyRccGDi2QTNkQo/2k5U5ttVD0jRFaMlc8UQee708fOZA6zTCDy5RWsT5MJw3sl2X6KDg== - -esbuild-linux-arm64@0.14.36: - version "0.14.36" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.36.tgz#c91c21e25b315464bd7da867365dd1dae14ca176" - integrity sha512-24Vq1M7FdpSmaTYuu1w0Hdhiqkbto1I5Pjyi+4Cdw5fJKGlwQuw+hWynTcRI/cOZxBcBpP21gND7W27gHAiftw== - -esbuild-linux-arm@0.14.36: - version "0.14.36" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.36.tgz#90e23bca2e6e549affbbe994f80ba3bb6c4d934a" - integrity sha512-NhgU4n+NCsYgt7Hy61PCquEz5aevI6VjQvxwBxtxrooXsxt5b2xtOUXYZe04JxqQo+XZk3d1gcr7pbV9MAQ/Lg== - -esbuild-linux-mips64le@0.14.36: - version "0.14.36" - resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.36.tgz#40e11afb08353ff24709fc89e4db0f866bc131d2" - integrity sha512-hZUeTXvppJN+5rEz2EjsOFM9F1bZt7/d2FUM1lmQo//rXh1RTFYzhC0txn7WV0/jCC7SvrGRaRz0NMsRPf8SIA== - -esbuild-linux-ppc64le@0.14.36: - version "0.14.36" - resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.36.tgz#9e8a588c513d06cc3859f9dcc52e5fdfce8a1a5e" - integrity sha512-1Bg3QgzZjO+QtPhP9VeIBhAduHEc2kzU43MzBnMwpLSZ890azr4/A9Dganun8nsqD/1TBcqhId0z4mFDO8FAvg== - -esbuild-linux-riscv64@0.14.36: - version "0.14.36" - resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.36.tgz#e578c09b23b3b97652e60e3692bfda628b541f06" - integrity sha512-dOE5pt3cOdqEhaufDRzNCHf5BSwxgygVak9UR7PH7KPVHwSTDAZHDoEjblxLqjJYpc5XaU9+gKJ9F8mp9r5I4A== - -esbuild-linux-s390x@0.14.36: - version "0.14.36" - resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.36.tgz#3c9dab40d0d69932ffded0fd7317bb403626c9bc" - integrity sha512-g4FMdh//BBGTfVHjF6MO7Cz8gqRoDPzXWxRvWkJoGroKA18G9m0wddvPbEqcQf5Tbt2vSc1CIgag7cXwTmoTXg== - -esbuild-netbsd-64@0.14.36: - version "0.14.36" - resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.36.tgz#e27847f6d506218291619b8c1e121ecd97628494" - integrity sha512-UB2bVImxkWk4vjnP62ehFNZ73lQY1xcnL5ZNYF3x0AG+j8HgdkNF05v67YJdCIuUJpBuTyCK8LORCYo9onSW+A== - -esbuild-openbsd-64@0.14.36: - version "0.14.36" - resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.36.tgz#c94c04c557fae516872a586eae67423da6d2fabb" - integrity sha512-NvGB2Chf8GxuleXRGk8e9zD3aSdRO5kLt9coTQbCg7WMGXeX471sBgh4kSg8pjx0yTXRt0MlrUDnjVYnetyivg== - -esbuild-sunos-64@0.14.36: - version "0.14.36" - resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.36.tgz#9b79febc0df65a30f1c9bd63047d1675511bf99d" - integrity sha512-VkUZS5ftTSjhRjuRLp+v78auMO3PZBXu6xl4ajomGenEm2/rGuWlhFSjB7YbBNErOchj51Jb2OK8lKAo8qdmsQ== - -esbuild-windows-32@0.14.36: - version "0.14.36" - resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.36.tgz#910d11936c8d2122ffdd3275e5b28d8a4e1240ec" - integrity sha512-bIar+A6hdytJjZrDxfMBUSEHHLfx3ynoEZXx/39nxy86pX/w249WZm8Bm0dtOAByAf4Z6qV0LsnTIJHiIqbw0w== - -esbuild-windows-64@0.14.36: - version "0.14.36" - resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.36.tgz#21b4ce8b42a4efc63f4b58ec617f1302448aad26" - integrity sha512-+p4MuRZekVChAeueT1Y9LGkxrT5x7YYJxYE8ZOTcEfeUUN43vktSn6hUNsvxzzATrSgq5QqRdllkVBxWZg7KqQ== - -esbuild-windows-arm64@0.14.36: - version "0.14.36" - resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.36.tgz#ba21546fecb7297667d0052d00150de22c044b24" - integrity sha512-fBB4WlDqV1m18EF/aheGYQkQZHfPHiHJSBYzXIo8yKehek+0BtBwo/4PNwKGJ5T0YK0oc8pBKjgwPbzSrPLb+Q== +esbuild-android-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz#505f41832884313bbaffb27704b8bcaa2d8616be" + integrity sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ== + +esbuild-android-arm64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz#8ce69d7caba49646e009968fe5754a21a9871771" + integrity sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg== + +esbuild-darwin-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz#24ba67b9a8cb890a3c08d9018f887cc221cdda25" + integrity sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug== + +esbuild-darwin-arm64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz#3f7cdb78888ee05e488d250a2bdaab1fa671bf73" + integrity sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw== + +esbuild-freebsd-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz#09250f997a56ed4650f3e1979c905ffc40bbe94d" + integrity sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg== + +esbuild-freebsd-arm64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz#bafb46ed04fc5f97cbdb016d86947a79579f8e48" + integrity sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q== + +esbuild-linux-32@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz#e2a8c4a8efdc355405325033fcebeb941f781fe5" + integrity sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw== + +esbuild-linux-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz#de5fdba1c95666cf72369f52b40b03be71226652" + integrity sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg== + +esbuild-linux-arm64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz#dae4cd42ae9787468b6a5c158da4c84e83b0ce8b" + integrity sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig== + +esbuild-linux-arm@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz#a2c1dff6d0f21dbe8fc6998a122675533ddfcd59" + integrity sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw== + +esbuild-linux-mips64le@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz#d9918e9e4cb972f8d6dae8e8655bf9ee131eda34" + integrity sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw== + +esbuild-linux-ppc64le@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz#3f9a0f6d41073fb1a640680845c7de52995f137e" + integrity sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ== + +esbuild-linux-riscv64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz#618853c028178a61837bc799d2013d4695e451c8" + integrity sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg== + +esbuild-linux-s390x@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz#d1885c4c5a76bbb5a0fe182e2c8c60eb9e29f2a6" + integrity sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA== + +esbuild-netbsd-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz#69ae917a2ff241b7df1dbf22baf04bd330349e81" + integrity sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w== + +esbuild-openbsd-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz#db4c8495287a350a6790de22edea247a57c5d47b" + integrity sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw== + +esbuild-sunos-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz#54287ee3da73d3844b721c21bc80c1dc7e1bf7da" + integrity sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw== + +esbuild-windows-32@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz#f8aaf9a5667630b40f0fb3aa37bf01bbd340ce31" + integrity sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w== + +esbuild-windows-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz#bf54b51bd3e9b0f1886ffdb224a4176031ea0af4" + integrity sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ== + +esbuild-windows-arm64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz#937d15675a15e4b0e4fafdbaa3a01a776a2be982" + integrity sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg== esbuild@^0.14.25: - version "0.14.36" - resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.14.36.tgz" - integrity sha512-HhFHPiRXGYOCRlrhpiVDYKcFJRdO0sBElZ668M4lh2ER0YgnkLxECuFe7uWCf23FrcLc59Pqr7dHkTqmRPDHmw== + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.54.tgz#8b44dcf2b0f1a66fc22459943dccf477535e9aa2" + integrity sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA== optionalDependencies: - esbuild-android-64 "0.14.36" - esbuild-android-arm64 "0.14.36" - esbuild-darwin-64 "0.14.36" - esbuild-darwin-arm64 "0.14.36" - esbuild-freebsd-64 "0.14.36" - esbuild-freebsd-arm64 "0.14.36" - esbuild-linux-32 "0.14.36" - esbuild-linux-64 "0.14.36" - esbuild-linux-arm "0.14.36" - esbuild-linux-arm64 "0.14.36" - esbuild-linux-mips64le "0.14.36" - esbuild-linux-ppc64le "0.14.36" - esbuild-linux-riscv64 "0.14.36" - esbuild-linux-s390x "0.14.36" - esbuild-netbsd-64 "0.14.36" - esbuild-openbsd-64 "0.14.36" - esbuild-sunos-64 "0.14.36" - esbuild-windows-32 "0.14.36" - esbuild-windows-64 "0.14.36" - esbuild-windows-arm64 "0.14.36" + "@esbuild/linux-loong64" "0.14.54" + esbuild-android-64 "0.14.54" + esbuild-android-arm64 "0.14.54" + esbuild-darwin-64 "0.14.54" + esbuild-darwin-arm64 "0.14.54" + esbuild-freebsd-64 "0.14.54" + esbuild-freebsd-arm64 "0.14.54" + esbuild-linux-32 "0.14.54" + esbuild-linux-64 "0.14.54" + esbuild-linux-arm "0.14.54" + esbuild-linux-arm64 "0.14.54" + esbuild-linux-mips64le "0.14.54" + esbuild-linux-ppc64le "0.14.54" + esbuild-linux-riscv64 "0.14.54" + esbuild-linux-s390x "0.14.54" + esbuild-netbsd-64 "0.14.54" + esbuild-openbsd-64 "0.14.54" + esbuild-sunos-64 "0.14.54" + esbuild-windows-32 "0.14.54" + esbuild-windows-64 "0.14.54" + esbuild-windows-arm64 "0.14.54" escalade@^3.1.1: version "3.1.1" - resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" - integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= - -escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-config-prettier@^8.5.0: - version "8.5.0" - resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz" - integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q== +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== -eslint-scope@^4.0.3: - version "4.0.3" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz" - integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== - dependencies: - esrecurse "^4.1.0" - estraverse "^4.1.1" +eslint-config-prettier@^8.10.0: + version "8.10.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz#3a06a662130807e2502fc3ff8b4143d8a0658e11" + integrity sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg== eslint-scope@^5.1.1: version "5.1.1" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== dependencies: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-scope@^7.1.1: - version "7.1.1" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz" - integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-utils@^1.3.1: - version "1.4.3" - resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz" - integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== - dependencies: - eslint-visitor-keys "^1.1.0" - -eslint-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz" - integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== - dependencies: - eslint-visitor-keys "^2.0.0" - -eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: - version "1.3.0" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz" - integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== - -eslint-visitor-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz" - integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== - -eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0: - version "3.3.0" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" - integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== - -eslint@^5.6.0: - version "5.16.0" - resolved "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz" - integrity sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg== - dependencies: - "@babel/code-frame" "^7.0.0" - ajv "^6.9.1" - chalk "^2.1.0" - cross-spawn "^6.0.5" - debug "^4.0.1" - doctrine "^3.0.0" - eslint-scope "^4.0.3" - eslint-utils "^1.3.1" - eslint-visitor-keys "^1.0.0" - espree "^5.0.1" - esquery "^1.0.1" - esutils "^2.0.2" - file-entry-cache "^5.0.1" - functional-red-black-tree "^1.0.1" - glob "^7.1.2" - globals "^11.7.0" - ignore "^4.0.6" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - inquirer "^6.2.2" - js-yaml "^3.13.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.3.0" - lodash "^4.17.11" - minimatch "^3.0.4" - mkdirp "^0.5.1" - natural-compare "^1.4.0" - optionator "^0.8.2" - path-is-inside "^1.0.2" - progress "^2.0.0" - regexpp "^2.0.1" - semver "^5.5.1" - strip-ansi "^4.0.0" - strip-json-comments "^2.0.1" - table "^5.2.3" - text-table "^0.2.0" - -eslint@^8.10.0: - version "8.13.0" - resolved "https://registry.npmjs.org/eslint/-/eslint-8.13.0.tgz" - integrity sha512-D+Xei61eInqauAyTJ6C0q6x9mx7kTUC1KZ0m0LSEexR0V+e94K12LmWX076ZIsldwfQ2RONdaJe0re0TRGQbRQ== - dependencies: - "@eslint/eslintrc" "^1.2.1" - "@humanwhocodes/config-array" "^0.9.2" - ajv "^6.10.0" +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint@^8.54.0: + version "8.56.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.56.0.tgz#4957ce8da409dc0809f99ab07a1b94832ab74b15" + integrity sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.56.0" + "@humanwhocodes/config-array" "^0.11.13" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" chalk "^4.0.0" cross-spawn "^7.0.2" debug "^4.3.2" doctrine "^3.0.0" escape-string-regexp "^4.0.0" - eslint-scope "^7.1.1" - eslint-utils "^3.0.0" - eslint-visitor-keys "^3.3.0" - espree "^9.3.1" - esquery "^1.4.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" file-entry-cache "^6.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^6.0.1" - globals "^13.6.0" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" ignore "^5.2.0" - import-fresh "^3.0.0" imurmurhash "^0.1.4" is-glob "^4.0.0" + is-path-inside "^3.0.3" js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" lodash.merge "^4.6.2" - minimatch "^3.0.4" + minimatch "^3.1.2" natural-compare "^1.4.0" - optionator "^0.9.1" - regexpp "^3.2.0" + optionator "^0.9.3" strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" text-table "^0.2.0" - v8-compile-cache "^2.0.3" - -espree@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz" - integrity sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A== - dependencies: - acorn "^6.0.7" - acorn-jsx "^5.0.0" - eslint-visitor-keys "^1.0.0" -espree@^9.3.1: - version "9.3.1" - resolved "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz" - integrity sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ== +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== dependencies: - acorn "^8.7.0" - acorn-jsx "^5.3.1" - eslint-visitor-keys "^3.3.0" - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" -esquery@^1.0.1, esquery@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz" - integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== +esquery@^1.4.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" + integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== dependencies: estraverse "^5.1.0" -esrecurse@^4.1.0, esrecurse@^4.3.0: +esrecurse@^4.3.0: version "4.3.0" - resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== dependencies: estraverse "^5.2.0" estraverse@^4.1.1: version "4.3.0" - resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== estraverse@^5.1.0, estraverse@^5.2.0: version "5.3.0" - resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== esutils@^2.0.2: version "2.0.3" - resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" - integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= - -eth-block-tracker@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/eth-block-tracker/-/eth-block-tracker-3.0.1.tgz" - integrity sha512-WUVxWLuhMmsfenfZvFO5sbl1qFY2IqUlw/FPVmjjdElpqLsZtSG+wPe9Dz7W/sB6e80HgFKknOmKk2eNlznHug== - dependencies: - eth-query "^2.1.0" - ethereumjs-tx "^1.3.3" - ethereumjs-util "^5.1.3" - ethjs-util "^0.1.3" - json-rpc-engine "^3.6.0" - pify "^2.3.0" - tape "^4.6.3" - -eth-ens-namehash@2.0.8, eth-ens-namehash@^2.0.8: - version "2.0.8" - resolved "https://registry.npmjs.org/eth-ens-namehash/-/eth-ens-namehash-2.0.8.tgz" - integrity sha1-IprEbsqG1S4MmR58sq74P/D2i88= - dependencies: - idna-uts46-hx "^2.3.1" - js-sha3 "^0.5.7" - -eth-gas-reporter@^0.2.24: - version "0.2.25" - resolved "https://registry.npmjs.org/eth-gas-reporter/-/eth-gas-reporter-0.2.25.tgz" - integrity sha512-1fRgyE4xUB8SoqLgN3eDfpDfwEfRxh2Sz1b7wzFbyQA+9TekMmvSjjoRu9SKcSVyK+vLkLIsVbJDsTWjw195OQ== - dependencies: - "@ethersproject/abi" "^5.0.0-beta.146" - "@solidity-parser/parser" "^0.14.0" - cli-table3 "^0.5.0" - colors "1.4.0" - ethereum-cryptography "^1.0.3" - ethers "^4.0.40" - fs-readdir-recursive "^1.1.0" - lodash "^4.17.14" - markdown-table "^1.1.3" - mocha "^7.1.1" - req-cwd "^2.0.0" - request "^2.88.0" - request-promise-native "^1.0.5" - sha1 "^1.1.1" - sync-request "^6.0.0" - -eth-json-rpc-infura@^3.1.0: - version "3.2.1" - resolved "https://registry.npmjs.org/eth-json-rpc-infura/-/eth-json-rpc-infura-3.2.1.tgz" - integrity sha512-W7zR4DZvyTn23Bxc0EWsq4XGDdD63+XPUCEhV2zQvQGavDVC4ZpFDK4k99qN7bd7/fjj37+rxmuBOBeIqCA5Mw== - dependencies: - cross-fetch "^2.1.1" - eth-json-rpc-middleware "^1.5.0" - json-rpc-engine "^3.4.0" - json-rpc-error "^2.0.0" - -eth-json-rpc-middleware@^1.5.0: - version "1.6.0" - resolved "https://registry.npmjs.org/eth-json-rpc-middleware/-/eth-json-rpc-middleware-1.6.0.tgz" - integrity sha512-tDVCTlrUvdqHKqivYMjtFZsdD7TtpNLBCfKAcOpaVs7orBMS/A8HWro6dIzNtTZIR05FAbJ3bioFOnZpuCew9Q== - dependencies: - async "^2.5.0" - eth-query "^2.1.2" - eth-tx-summary "^3.1.2" - ethereumjs-block "^1.6.0" - ethereumjs-tx "^1.3.3" - ethereumjs-util "^5.1.2" - ethereumjs-vm "^2.1.0" - fetch-ponyfill "^4.0.0" - json-rpc-engine "^3.6.0" - json-rpc-error "^2.0.0" - json-stable-stringify "^1.0.1" - promise-to-callback "^1.0.0" - tape "^4.6.3" - -eth-lib@0.2.8: - version "0.2.8" - resolved "https://registry.npmjs.org/eth-lib/-/eth-lib-0.2.8.tgz" - integrity sha512-ArJ7x1WcWOlSpzdoTBX8vkwlkSQ85CjjifSZtV4co64vWxSV8geWfPI9x4SVYu3DSxnX4yWFVTtGL+j9DUFLNw== - dependencies: - bn.js "^4.11.6" - elliptic "^6.4.0" - xhr-request-promise "^0.1.2" - -eth-lib@^0.1.26: - version "0.1.29" - resolved "https://registry.npmjs.org/eth-lib/-/eth-lib-0.1.29.tgz" - integrity sha512-bfttrr3/7gG4E02HoWTDUcDDslN003OlOoBxk9virpAZQ1ja/jDgwkWB8QfJF7ojuEowrqy+lzp9VcJG7/k5bQ== - dependencies: - bn.js "^4.11.6" - elliptic "^6.4.0" - nano-json-stream-parser "^0.1.2" - servify "^0.1.12" - ws "^3.0.0" - xhr-request-promise "^0.1.2" - -eth-query@^2.0.2, eth-query@^2.1.0, eth-query@^2.1.2: - version "2.1.2" - resolved "https://registry.npmjs.org/eth-query/-/eth-query-2.1.2.tgz" - integrity sha1-1nQdkAAQa1FRDHLbktY2VFam2l4= - dependencies: - json-rpc-random-id "^1.0.0" - xtend "^4.0.1" - -eth-sig-util@3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-3.0.0.tgz" - integrity sha512-4eFkMOhpGbTxBQ3AMzVf0haUX2uTur7DpWiHzWyTURa28BVJJtOkcb9Ok5TV0YvEPG61DODPW7ZUATbJTslioQ== - dependencies: - buffer "^5.2.1" - elliptic "^6.4.0" - ethereumjs-abi "0.6.5" - ethereumjs-util "^5.1.1" - tweetnacl "^1.0.0" - tweetnacl-util "^0.15.0" - -eth-sig-util@^1.4.2: - version "1.4.2" - resolved "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz" - integrity sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA= - dependencies: - ethereumjs-abi "git+https://github.com/ethereumjs/ethereumjs-abi.git" - ethereumjs-util "^5.1.1" - -eth-tx-summary@^3.1.2: - version "3.2.4" - resolved "https://registry.npmjs.org/eth-tx-summary/-/eth-tx-summary-3.2.4.tgz" - integrity sha512-NtlDnaVZah146Rm8HMRUNMgIwG/ED4jiqk0TME9zFheMl1jOp6jL1m0NKGjJwehXQ6ZKCPr16MTr+qspKpEXNg== - dependencies: - async "^2.1.2" - clone "^2.0.0" - concat-stream "^1.5.1" - end-of-stream "^1.1.0" - eth-query "^2.0.2" - ethereumjs-block "^1.4.1" - ethereumjs-tx "^1.1.1" - ethereumjs-util "^5.0.1" - ethereumjs-vm "^2.6.0" - through2 "^2.0.3" - -ethashjs@~0.0.7: - version "0.0.8" - resolved "https://registry.npmjs.org/ethashjs/-/ethashjs-0.0.8.tgz" - integrity sha512-/MSbf/r2/Ld8o0l15AymjOTlPqpN8Cr4ByUEA9GtR4x0yAh3TdtDzEg29zMjXCNPI7u6E5fOQdj/Cf9Tc7oVNw== - dependencies: - async "^2.1.2" - buffer-xor "^2.0.1" - ethereumjs-util "^7.0.2" - miller-rabin "^4.0.0" - -ethereum-bloom-filters@^1.0.6: - version "1.0.10" - resolved "https://registry.npmjs.org/ethereum-bloom-filters/-/ethereum-bloom-filters-1.0.10.tgz" - integrity sha512-rxJ5OFN3RwjQxDcFP2Z5+Q9ho4eIdEmSc2ht0fCu8Se9nbXjZ7/031uXoUYJ87KHCOdVeiUuwSnoS7hmYAGVHA== - dependencies: - js-sha3 "^0.8.0" - -ethereum-common@0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.2.0.tgz" - integrity sha512-XOnAR/3rntJgbCdGhqdaLIxDLWKLmsZOGhHdBKadEr6gEnJLH52k93Ou+TUdFaPN3hJc3isBZBal3U/XZ15abA== - -ethereum-common@^0.0.18: - version "0.0.18" - resolved "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.0.18.tgz" - integrity sha1-L9w1dvIykDNYl26znaeDIT/5Uj8= - -ethereum-cryptography@^0.1.2, ethereum-cryptography@^0.1.3: - version "0.1.3" - resolved "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-0.1.3.tgz" - 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" - -ethereum-cryptography@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.0.3.tgz" - integrity sha512-NQLTW0x0CosoVb/n79x/TRHtfvS3hgNUPTUSCu0vM+9k6IIhHFFrAOJReneexjZsoZxMjJHnJn4lrE8EbnSyqQ== - dependencies: - "@noble/hashes" "1.0.0" - "@noble/secp256k1" "1.5.5" - "@scure/bip32" "1.0.1" - "@scure/bip39" "1.0.0" - -ethereum-waffle@^3.4.0: - version "3.4.4" - resolved "https://registry.npmjs.org/ethereum-waffle/-/ethereum-waffle-3.4.4.tgz" - integrity sha512-PA9+jCjw4WC3Oc5ocSMBj5sXvueWQeAbvCA+hUlb6oFgwwKyq5ka3bWQ7QZcjzIX+TdFkxP4IbFmoY2D8Dkj9Q== - dependencies: - "@ethereum-waffle/chai" "^3.4.4" - "@ethereum-waffle/compiler" "^3.4.4" - "@ethereum-waffle/mock-contract" "^3.4.4" - "@ethereum-waffle/provider" "^3.4.4" - ethers "^5.0.1" - -ethereumjs-abi@0.6.5: - version "0.6.5" - resolved "https://registry.npmjs.org/ethereumjs-abi/-/ethereumjs-abi-0.6.5.tgz" - integrity sha1-WmN+8Wq0NHP6cqKa2QhxQFs/UkE= - dependencies: - bn.js "^4.10.0" - ethereumjs-util "^4.3.0" - -ethereumjs-abi@0.6.8, ethereumjs-abi@^0.6.8: - version "0.6.8" - resolved "https://registry.npmjs.org/ethereumjs-abi/-/ethereumjs-abi-0.6.8.tgz" - integrity sha512-Tx0r/iXI6r+lRsdvkFDlut0N08jWMnKRZ6Gkq+Nmw75lZe4e6o3EkSnkaBP5NF6+m5PTGAr9JP43N3LyeoglsA== - dependencies: - bn.js "^4.11.8" - ethereumjs-util "^6.0.0" - -"ethereumjs-abi@git+https://github.com/ethereumjs/ethereumjs-abi.git": - version "0.6.8" - resolved "git+https://github.com/ethereumjs/ethereumjs-abi.git#ee3994657fa7a427238e6ba92a84d0b529bbcde0" - dependencies: - bn.js "^4.11.8" - ethereumjs-util "^6.0.0" - -ethereumjs-account@3.0.0, ethereumjs-account@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/ethereumjs-account/-/ethereumjs-account-3.0.0.tgz" - integrity sha512-WP6BdscjiiPkQfF9PVfMcwx/rDvfZTjFKY0Uwc09zSQr9JfIVH87dYIJu0gNhBhpmovV4yq295fdllS925fnBA== - dependencies: - ethereumjs-util "^6.0.0" - rlp "^2.2.1" - safe-buffer "^5.1.1" - -ethereumjs-account@^2.0.3: - version "2.0.5" - resolved "https://registry.npmjs.org/ethereumjs-account/-/ethereumjs-account-2.0.5.tgz" - integrity sha512-bgDojnXGjhMwo6eXQC0bY6UK2liSFUSMwwylOmQvZbSl/D7NXQ3+vrGO46ZeOgjGfxXmgIeVNDIiHw7fNZM4VA== - dependencies: - ethereumjs-util "^5.0.0" - rlp "^2.0.0" - safe-buffer "^5.1.1" - -ethereumjs-block@2.2.2, ethereumjs-block@^2.2.2, ethereumjs-block@~2.2.0, ethereumjs-block@~2.2.2: - version "2.2.2" - resolved "https://registry.npmjs.org/ethereumjs-block/-/ethereumjs-block-2.2.2.tgz" - integrity sha512-2p49ifhek3h2zeg/+da6XpdFR3GlqY3BIEiqxGF8j9aSRIgkb7M1Ky+yULBKJOu8PAZxfhsYA+HxUk2aCQp3vg== - dependencies: - async "^2.0.1" - ethereumjs-common "^1.5.0" - ethereumjs-tx "^2.1.1" - ethereumjs-util "^5.0.0" - merkle-patricia-tree "^2.1.2" - -ethereumjs-block@^1.2.2, ethereumjs-block@^1.4.1, ethereumjs-block@^1.6.0: - version "1.7.1" - resolved "https://registry.npmjs.org/ethereumjs-block/-/ethereumjs-block-1.7.1.tgz" - integrity sha512-B+sSdtqm78fmKkBq78/QLKJbu/4Ts4P2KFISdgcuZUPDm9x+N7qgBPIIFUGbaakQh8bzuquiRVbdmvPKqbILRg== - dependencies: - async "^2.0.1" - ethereum-common "0.2.0" - ethereumjs-tx "^1.2.2" - ethereumjs-util "^5.0.0" - merkle-patricia-tree "^2.1.2" - -ethereumjs-blockchain@^4.0.3: - version "4.0.4" - resolved "https://registry.npmjs.org/ethereumjs-blockchain/-/ethereumjs-blockchain-4.0.4.tgz" - integrity sha512-zCxaRMUOzzjvX78DTGiKjA+4h2/sF0OYL1QuPux0DHpyq8XiNoF5GYHtb++GUxVlMsMfZV7AVyzbtgcRdIcEPQ== - dependencies: - async "^2.6.1" - ethashjs "~0.0.7" - ethereumjs-block "~2.2.2" - ethereumjs-common "^1.5.0" - ethereumjs-util "^6.1.0" - flow-stoplight "^1.0.0" - level-mem "^3.0.1" - lru-cache "^5.1.1" - rlp "^2.2.2" - semaphore "^1.1.0" - -ethereumjs-common@1.5.0: - version "1.5.0" - resolved "https://registry.npmjs.org/ethereumjs-common/-/ethereumjs-common-1.5.0.tgz" - integrity sha512-SZOjgK1356hIY7MRj3/ma5qtfr/4B5BL+G4rP/XSMYr2z1H5el4RX5GReYCKmQmYI/nSBmRnwrZ17IfHuG0viQ== - -ethereumjs-common@^1.1.0, ethereumjs-common@^1.3.2, ethereumjs-common@^1.5.0: - version "1.5.2" - resolved "https://registry.npmjs.org/ethereumjs-common/-/ethereumjs-common-1.5.2.tgz" - integrity sha512-hTfZjwGX52GS2jcVO6E2sx4YuFnf0Fhp5ylo4pEPhEffNln7vS59Hr5sLnp3/QCazFLluuBZ+FZ6J5HTp0EqCA== - -ethereumjs-tx@2.1.2, ethereumjs-tx@^2.1.1, ethereumjs-tx@^2.1.2: - version "2.1.2" - resolved "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-2.1.2.tgz" - integrity sha512-zZEK1onCeiORb0wyCXUvg94Ve5It/K6GD1K+26KfFKodiBiS6d9lfCXlUKGBBdQ+bv7Day+JK0tj1K+BeNFRAw== - dependencies: - ethereumjs-common "^1.5.0" - ethereumjs-util "^6.0.0" - -ethereumjs-tx@^1.1.1, ethereumjs-tx@^1.2.0, ethereumjs-tx@^1.2.2, ethereumjs-tx@^1.3.3: - version "1.3.7" - resolved "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-1.3.7.tgz" - integrity sha512-wvLMxzt1RPhAQ9Yi3/HKZTn0FZYpnsmQdbKYfUUpi4j1SEIcbkd9tndVjcPrufY3V7j2IebOpC00Zp2P/Ay2kA== - dependencies: - ethereum-common "^0.0.18" - ethereumjs-util "^5.0.0" - -ethereumjs-util@6.2.1, ethereumjs-util@^6.0.0, ethereumjs-util@^6.1.0, ethereumjs-util@^6.2.0, ethereumjs-util@^6.2.1: - version "6.2.1" - resolved "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.2.1.tgz" - integrity sha512-W2Ktez4L01Vexijrm5EB6w7dg4n/TgpoYU4avuT5T3Vmnw/eCRtiBrJfQYS/DCSvDIOLn2k57GcHdeBcgVxAqw== - dependencies: - "@types/bn.js" "^4.11.3" - bn.js "^4.11.0" - create-hash "^1.1.2" - elliptic "^6.5.2" - ethereum-cryptography "^0.1.3" - ethjs-util "0.1.6" - rlp "^2.2.3" - -ethereumjs-util@^4.3.0: - version "4.5.1" - resolved "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-4.5.1.tgz" - integrity sha512-WrckOZ7uBnei4+AKimpuF1B3Fv25OmoRgmYCpGsP7u8PFxXAmAgiJSYT2kRWnt6fVIlKaQlZvuwXp7PIrmn3/w== - dependencies: - bn.js "^4.8.0" - create-hash "^1.1.2" - elliptic "^6.5.2" - ethereum-cryptography "^0.1.3" - rlp "^2.0.0" - -ethereumjs-util@^5.0.0, ethereumjs-util@^5.0.1, ethereumjs-util@^5.1.1, ethereumjs-util@^5.1.2, ethereumjs-util@^5.1.3, ethereumjs-util@^5.1.5, ethereumjs-util@^5.2.0: - version "5.2.1" - resolved "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz" - integrity sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ== - dependencies: - bn.js "^4.11.0" - create-hash "^1.1.2" - elliptic "^6.5.2" - ethereum-cryptography "^0.1.3" - ethjs-util "^0.1.3" - rlp "^2.0.0" - safe-buffer "^5.1.1" - -ethereumjs-util@^7.0.2, ethereumjs-util@^7.1.0, ethereumjs-util@^7.1.1, ethereumjs-util@^7.1.3, ethereumjs-util@^7.1.4: - version "7.1.4" - resolved "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.4.tgz" - integrity sha512-p6KmuPCX4mZIqsQzXfmSx9Y0l2hqf+VkAiwSisW3UKUFdk8ZkAt+AYaor83z2nSi6CU2zSsXMlD80hAbNEGM0A== - 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" - -ethereumjs-vm@4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/ethereumjs-vm/-/ethereumjs-vm-4.2.0.tgz" - integrity sha512-X6qqZbsY33p5FTuZqCnQ4+lo957iUJMM6Mpa6bL4UW0dxM6WmDSHuI4j/zOp1E2TDKImBGCJA9QPfc08PaNubA== - dependencies: - async "^2.1.2" - async-eventemitter "^0.2.2" - core-js-pure "^3.0.1" - ethereumjs-account "^3.0.0" - ethereumjs-block "^2.2.2" - ethereumjs-blockchain "^4.0.3" - ethereumjs-common "^1.5.0" - ethereumjs-tx "^2.1.2" - ethereumjs-util "^6.2.0" - fake-merkle-patricia-tree "^1.0.1" - functional-red-black-tree "^1.0.1" - merkle-patricia-tree "^2.3.2" - rustbn.js "~0.2.0" - safe-buffer "^5.1.1" - util.promisify "^1.0.0" - -ethereumjs-vm@^2.1.0, ethereumjs-vm@^2.3.4, ethereumjs-vm@^2.6.0: - version "2.6.0" - resolved "https://registry.npmjs.org/ethereumjs-vm/-/ethereumjs-vm-2.6.0.tgz" - integrity sha512-r/XIUik/ynGbxS3y+mvGnbOKnuLo40V5Mj1J25+HEO63aWYREIqvWeRO/hnROlMBE5WoniQmPmhiaN0ctiHaXw== - dependencies: - async "^2.1.2" - async-eventemitter "^0.2.2" - ethereumjs-account "^2.0.3" - ethereumjs-block "~2.2.0" - ethereumjs-common "^1.1.0" - ethereumjs-util "^6.0.0" - fake-merkle-patricia-tree "^1.0.1" - functional-red-black-tree "^1.0.1" - merkle-patricia-tree "^2.3.2" - rustbn.js "~0.2.0" - safe-buffer "^5.1.1" - -ethereumjs-wallet@0.6.5: - version "0.6.5" - resolved "https://registry.npmjs.org/ethereumjs-wallet/-/ethereumjs-wallet-0.6.5.tgz" - integrity sha512-MDwjwB9VQVnpp/Dc1XzA6J1a3wgHQ4hSvA1uWNatdpOrtCbPVuQSKSyRnjLvS0a+KKMw2pvQ9Ybqpb3+eW8oNA== - dependencies: - aes-js "^3.1.1" - bs58check "^2.1.2" - ethereum-cryptography "^0.1.3" - ethereumjs-util "^6.0.0" - randombytes "^2.0.6" - safe-buffer "^5.1.2" - scryptsy "^1.2.1" - utf8 "^3.0.0" - uuid "^3.3.2" - -ethers@^4.0.40: - version "4.0.49" - resolved "https://registry.npmjs.org/ethers/-/ethers-4.0.49.tgz" - integrity sha512-kPltTvWiyu+OktYy1IStSO16i2e7cS9D9OxZ81q2UUaiNPVrm/RTcbxamCXF9VUSKzJIdJV68EAIhTEVBalRWg== - dependencies: - aes-js "3.0.0" - bn.js "^4.11.9" - elliptic "6.5.4" - hash.js "1.1.3" - js-sha3 "0.5.7" - scrypt-js "2.0.4" - setimmediate "1.0.4" - uuid "2.0.1" - xmlhttprequest "1.8.0" - -ethers@^5.0.1, ethers@^5.0.2, ethers@^5.5.2: - version "5.6.2" - resolved "https://registry.npmjs.org/ethers/-/ethers-5.6.2.tgz" - integrity sha512-EzGCbns24/Yluu7+ToWnMca3SXJ1Jk1BvWB7CCmVNxyOeM4LLvw2OLuIHhlkhQk1dtOcj9UMsdkxUh8RiG1dxQ== - dependencies: - "@ethersproject/abi" "5.6.0" - "@ethersproject/abstract-provider" "5.6.0" - "@ethersproject/abstract-signer" "5.6.0" - "@ethersproject/address" "5.6.0" - "@ethersproject/base64" "5.6.0" - "@ethersproject/basex" "5.6.0" - "@ethersproject/bignumber" "5.6.0" - "@ethersproject/bytes" "5.6.1" - "@ethersproject/constants" "5.6.0" - "@ethersproject/contracts" "5.6.0" - "@ethersproject/hash" "5.6.0" - "@ethersproject/hdnode" "5.6.0" - "@ethersproject/json-wallets" "5.6.0" - "@ethersproject/keccak256" "5.6.0" - "@ethersproject/logger" "5.6.0" - "@ethersproject/networks" "5.6.1" - "@ethersproject/pbkdf2" "5.6.0" - "@ethersproject/properties" "5.6.0" - "@ethersproject/providers" "5.6.2" - "@ethersproject/random" "5.6.0" - "@ethersproject/rlp" "5.6.0" - "@ethersproject/sha2" "5.6.0" - "@ethersproject/signing-key" "5.6.0" - "@ethersproject/solidity" "5.6.0" - "@ethersproject/strings" "5.6.0" - "@ethersproject/transactions" "5.6.0" - "@ethersproject/units" "5.6.0" - "@ethersproject/wallet" "5.6.0" - "@ethersproject/web" "5.6.0" - "@ethersproject/wordlists" "5.6.0" - -ethjs-unit@0.1.6: - version "0.1.6" - resolved "https://registry.npmjs.org/ethjs-unit/-/ethjs-unit-0.1.6.tgz" - integrity sha1-xmWSHkduh7ziqdWIpv4EBbLEFpk= - dependencies: - bn.js "4.11.6" - number-to-bn "1.7.0" - -ethjs-util@0.1.6, ethjs-util@^0.1.3, ethjs-util@^0.1.6: - version "0.1.6" - resolved "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.6.tgz" - integrity sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w== - dependencies: - is-hex-prefixed "1.0.0" - strip-hex-prefix "1.0.0" - -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - -eventemitter3@4.0.4: - version "4.0.4" - resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.4.tgz" - integrity sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ== - -events@^3.0.0: - version "3.3.0" - resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== - -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz" - integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" +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" execa@^5.0.0: version "5.1.1" - resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== dependencies: cross-spawn "^7.0.3" @@ -4253,136 +1470,20 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" -expand-brackets@^2.1.4: - version "2.1.4" - resolved "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz" - integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= - dependencies: - debug "^2.3.3" - define-property "^0.2.5" - extend-shallow "^2.0.1" - posix-character-classes "^0.1.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -express@^4.14.0: - version "4.17.3" - resolved "https://registry.npmjs.org/express/-/express-4.17.3.tgz" - integrity sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg== - dependencies: - accepts "~1.3.8" - array-flatten "1.1.1" - body-parser "1.19.2" - content-disposition "0.5.4" - content-type "~1.0.4" - cookie "0.4.2" - cookie-signature "1.0.6" - debug "2.6.9" - depd "~1.1.2" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "~1.1.2" - fresh "0.5.2" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.3" - path-to-regexp "0.1.7" - proxy-addr "~2.0.7" - qs "6.9.7" - range-parser "~1.2.1" - safe-buffer "5.2.1" - send "0.17.2" - serve-static "1.14.2" - setprototypeof "1.2.0" - statuses "~1.5.0" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -ext@^1.1.2: - version "1.6.0" - resolved "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz" - integrity sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg== - dependencies: - type "^2.5.0" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz" - integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= - dependencies: - is-extendable "^0.1.0" - -extend-shallow@^3.0.0, extend-shallow@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz" - integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= - dependencies: - assign-symbols "^1.0.0" - is-extendable "^1.0.1" - -extend@~3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -external-editor@^3.0.3: - version "3.1.0" - resolved "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz" - integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== - dependencies: - chardet "^0.7.0" - iconv-lite "^0.4.24" - tmp "^0.0.33" - -extglob@^2.0.4: - version "2.0.4" - resolved "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz" - integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== - dependencies: - array-unique "^0.3.2" - define-property "^1.0.0" - expand-brackets "^2.1.4" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz" - integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= - -extsprintf@^1.2.0: - version "1.4.1" - resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz" - integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== - -fake-merkle-patricia-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/fake-merkle-patricia-tree/-/fake-merkle-patricia-tree-1.0.1.tgz" - integrity sha1-S4w6z7Ugr635hgsfFM2M40As3dM= - dependencies: - checkpoint-store "^1.1.0" - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" - resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + 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.1.2: - version "1.2.0" - resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz" - integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== +fast-diff@^1.1.2, 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-glob@^3.2.9: - version "3.2.11" - resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz" - integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -4392,467 +1493,133 @@ fast-glob@^3.2.9: fast-json-stable-stringify@^2.0.0: version "2.1.0" - resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: +fast-levenshtein@^2.0.6: version "2.0.6" - resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" - integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== fastq@^1.6.0: - version "1.13.0" - resolved "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz" - integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== + version "1.16.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.16.0.tgz#83b9a9375692db77a822df081edb6a9cf6839320" + integrity sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA== dependencies: reusify "^1.0.4" -fetch-ponyfill@^4.0.0: - version "4.1.0" - resolved "https://registry.npmjs.org/fetch-ponyfill/-/fetch-ponyfill-4.1.0.tgz" - integrity sha1-rjzl9zLGReq4fkroeTQUcJsjmJM= - dependencies: - node-fetch "~1.7.1" - -figures@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz" - integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= - dependencies: - escape-string-regexp "^1.0.5" - -file-entry-cache@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz" - integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== - dependencies: - flat-cache "^2.0.1" - file-entry-cache@^6.0.1: version "6.0.1" - resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== dependencies: flat-cache "^3.0.4" -fill-range@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz" - integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= - dependencies: - extend-shallow "^2.0.1" - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range "^2.1.0" - fill-range@^7.0.1: version "7.0.1" - resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" + 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" -finalhandler@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz" - integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== - dependencies: - debug "2.6.9" - encodeurl "~1.0.2" - escape-html "~1.0.3" - on-finished "~2.3.0" - parseurl "~1.3.3" - statuses "~1.5.0" - unpipe "~1.0.0" - -find-replace@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/find-replace/-/find-replace-1.0.3.tgz" - integrity sha1-uI5zZNLZyVlVnziMZmcNYTBEH6A= - dependencies: - array-back "^1.0.4" - test-value "^2.1.0" - find-replace@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38" integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ== dependencies: array-back "^3.0.1" -find-up@3.0.0, find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - -find-up@5.0.0: +find-up@5.0.0, find-up@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== dependencies: locate-path "^6.0.0" path-exists "^4.0.0" -find-up@^1.0.0: - version "1.1.2" - resolved "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz" - integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= +flat-cache@^3.0.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== dependencies: - path-exists "^2.0.0" - pinkie-promise "^2.0.0" + flatted "^3.2.9" + keyv "^4.5.3" + rimraf "^3.0.2" -find-up@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz" - integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= - dependencies: - locate-path "^2.0.0" +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== -find-yarn-workspace-root@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-1.2.1.tgz" - integrity sha512-dVtfb0WuQG+8Ag2uWkbG79hOUzEsRrhBzgfn86g2sJPkzmcpGdghbNTfUKGTxymFrY/tLIodDzLoW9nOJ4FY8Q== - dependencies: - fs-extra "^4.0.3" - micromatch "^3.1.4" +flatted@^3.2.9: + version "3.2.9" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" + integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== -find-yarn-workspace-root@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz" - integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ== +foreground-child@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" + integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== dependencies: - micromatch "^4.0.2" + cross-spawn "^7.0.0" + signal-exit "^4.0.1" -flat-cache@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz" - integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== +fs-extra@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== dependencies: - flatted "^2.0.0" - rimraf "2.6.3" - write "1.0.3" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" -flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== - dependencies: - flatted "^3.1.0" - rimraf "^3.0.2" - -flat@^4.1.0: - version "4.1.1" - resolved "https://registry.npmjs.org/flat/-/flat-4.1.1.tgz" - integrity sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA== - dependencies: - is-buffer "~2.0.3" - -flat@^5.0.2: - version "5.0.2" - resolved "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz" - integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== - -flatted@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz" - integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== - -flatted@^3.1.0: - version "3.2.5" - resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz" - integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== - -flow-stoplight@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/flow-stoplight/-/flow-stoplight-1.0.0.tgz" - integrity sha1-SiksW8/4s5+mzAyxqFPYbyfu/3s= - -follow-redirects@^1.12.1: - version "1.14.9" - resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz" - integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== - -for-each@^0.3.3, for-each@~0.3.3: - version "0.3.3" - resolved "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz" - integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== - dependencies: - is-callable "^1.1.3" - -for-in@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz" - integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= - -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" - integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= - -form-data@^2.2.0: - version "2.5.1" - resolved "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz" - integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - -form-data@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz" - integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - -forwarded@0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" - integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== - -fp-ts@1.19.3: - version "1.19.3" - resolved "https://registry.npmjs.org/fp-ts/-/fp-ts-1.19.3.tgz" - integrity sha512-H5KQDspykdHuztLTg+ajGN0Z2qUjcEf3Ybxc6hLt0k7/zPkn29XnKnxlBPyW2XIddWrGaJBzBl4VLYOtk39yZg== - -fp-ts@^1.0.0: - version "1.19.5" - resolved "https://registry.npmjs.org/fp-ts/-/fp-ts-1.19.5.tgz" - integrity sha512-wDNqTimnzs8QqpldiId9OavWK2NptormjXnRJTQecNjzwfyp6P/8s/zG8e4h3ja3oqkKaY72UlTjQYt/1yXf9A== - -fragment-cache@^0.2.1: - version "0.2.1" - resolved "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz" - integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= - dependencies: - map-cache "^0.2.2" - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" - integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= - -fs-extra@^0.30.0: - version "0.30.0" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz" - integrity sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A= - dependencies: - graceful-fs "^4.1.2" - jsonfile "^2.1.0" - klaw "^1.0.0" - path-is-absolute "^1.0.0" - rimraf "^2.2.8" - -fs-extra@^10.0.1: - version "10.0.1" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz" - integrity sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs-extra@^4.0.2, fs-extra@^4.0.3: - version "4.0.3" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz" - integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg== - dependencies: - graceful-fs "^4.1.2" - jsonfile "^4.0.0" - universalify "^0.1.0" - -fs-extra@^7.0.0, fs-extra@^7.0.1: +fs-extra@^7.0.0: version "7.0.1" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== dependencies: graceful-fs "^4.1.2" jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^9.1.0: - version "9.1.0" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs-minipass@^1.2.7: - version "1.2.7" - resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz" - integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== - dependencies: - minipass "^2.6.0" - -fs-readdir-recursive@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz" - integrity sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA== - fs.realpath@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -fsevents@~2.1.1: - version "2.1.3" - resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz" - integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -functional-red-black-tree@^1.0.1, functional-red-black-tree@~1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= - -ganache-core@^2.13.2: - version "2.13.2" - resolved "https://registry.npmjs.org/ganache-core/-/ganache-core-2.13.2.tgz" - integrity sha512-tIF5cR+ANQz0+3pHWxHjIwHqFXcVo0Mb+kcsNhglNFALcYo49aQpnS9dqHartqPfMFjiHh/qFoD3mYK0d/qGgw== - dependencies: - abstract-leveldown "3.0.0" - async "2.6.2" - bip39 "2.5.0" - cachedown "1.0.0" - clone "2.1.2" - debug "3.2.6" - encoding-down "5.0.4" - eth-sig-util "3.0.0" - ethereumjs-abi "0.6.8" - ethereumjs-account "3.0.0" - ethereumjs-block "2.2.2" - ethereumjs-common "1.5.0" - ethereumjs-tx "2.1.2" - ethereumjs-util "6.2.1" - ethereumjs-vm "4.2.0" - heap "0.2.6" - keccak "3.0.1" - level-sublevel "6.6.4" - levelup "3.1.1" - lodash "4.17.20" - lru-cache "5.1.1" - merkle-patricia-tree "3.0.0" - patch-package "6.2.2" - seedrandom "3.0.1" - source-map-support "0.5.12" - tmp "0.1.0" - web3-provider-engine "14.2.1" - websocket "1.0.32" - optionalDependencies: - ethereumjs-wallet "0.6.5" - web3 "1.2.11" - -get-caller-file@^1.0.1: - version "1.0.3" - resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz" - integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== -get-caller-file@^2.0.1, get-caller-file@^2.0.5: +get-caller-file@^2.0.5: version "2.0.5" - resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz" - integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - -get-port@^3.1.0: - version "3.2.0" - resolved "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz" - integrity sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw= - -get-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz" - integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= - -get-stream@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz" - integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== - dependencies: - pump "^3.0.0" - -get-stream@^5.1.0: - version "5.2.0" - resolved "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz" - integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== - dependencies: - pump "^3.0.0" - get-stream@^6.0.0: version "6.0.1" - resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== -get-symbol-description@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz" - integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.1" - -get-value@^2.0.3, get-value@^2.0.6: - version "2.0.6" - resolved "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz" - integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= - -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz" - integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= - dependencies: - assert-plus "^1.0.0" - -glob-parent@^5.1.2, glob-parent@~5.1.0, glob-parent@~5.1.2: +glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" -glob-parent@^6.0.1: +glob-parent@^6.0.2: version "6.0.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== dependencies: is-glob "^4.0.3" -glob@7.1.3: - version "7.1.3" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz" - integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== +glob@7.1.7: + version "7.1.7" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" + integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -4861,10 +1628,10 @@ glob@7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" -glob@7.1.6: - version "7.1.6" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== +glob@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -4873,58 +1640,50 @@ glob@7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" -glob@7.1.7: - version "7.1.7" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== +glob@^10.3.10: + version "10.3.10" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b" + integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== + dependencies: + foreground-child "^3.1.0" + jackspeak "^2.3.5" + minimatch "^9.0.1" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-scurry "^1.10.1" + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.0.4" + minimatch "^3.1.1" once "^1.3.0" path-is-absolute "^1.0.0" -glob@7.2.0, glob@^7.1.2, glob@^7.1.3, glob@~7.2.0: - version "7.2.0" - resolved "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz" - integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== +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 "^3.0.4" + minimatch "^5.0.1" once "^1.3.0" - path-is-absolute "^1.0.0" -global@~4.4.0: - version "4.4.0" - resolved "https://registry.npmjs.org/global/-/global-4.4.0.tgz" - integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w== - dependencies: - min-document "^2.19.0" - process "^0.11.10" - -globals@^11.7.0: - version "11.12.0" - resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - -globals@^13.6.0, globals@^13.9.0: - version "13.13.0" - resolved "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz" - integrity sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A== +globals@^13.19.0: + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== dependencies: type-fest "^0.20.2" -globals@^9.18.0: - version "9.18.0" - resolved "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz" - integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== - -globby@^11.0.3, globby@^11.0.4: +globby@^11.0.3, globby@^11.1.0: version "11.1.0" - resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== dependencies: array-union "^2.1.0" @@ -4934,249 +1693,34 @@ globby@^11.0.3, globby@^11.0.4: merge2 "^1.4.1" slash "^3.0.0" -got@9.6.0: - version "9.6.0" - resolved "https://registry.npmjs.org/got/-/got-9.6.0.tgz" - integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== - dependencies: - "@sindresorhus/is" "^0.14.0" - "@szmarczak/http-timer" "^1.1.2" - cacheable-request "^6.0.0" - decompress-response "^3.3.0" - duplexer3 "^0.1.4" - get-stream "^4.1.0" - lowercase-keys "^1.0.1" - mimic-response "^1.0.1" - p-cancelable "^1.0.0" - to-readable-stream "^1.0.0" - url-parse-lax "^3.0.0" - -got@^7.1.0: - version "7.1.0" - resolved "https://registry.npmjs.org/got/-/got-7.1.0.tgz" - integrity sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw== - dependencies: - decompress-response "^3.2.0" - duplexer3 "^0.1.4" - get-stream "^3.0.0" - is-plain-obj "^1.1.0" - is-retry-allowed "^1.0.0" - is-stream "^1.0.0" - isurl "^1.0.0-alpha5" - lowercase-keys "^1.0.0" - p-cancelable "^0.3.0" - p-timeout "^1.1.1" - safe-buffer "^5.0.1" - timed-out "^4.0.0" - url-parse-lax "^1.0.0" - url-to-options "^1.0.1" - -graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0: - version "4.2.10" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== growl@1.10.5: version "1.10.5" - resolved "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= - -har-validator@~5.1.3: - version "5.1.5" - resolved "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz" - integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== - dependencies: - ajv "^6.12.3" - har-schema "^2.0.0" - -hardhat-abi-exporter@^2.4.1: - version "2.8.0" - resolved "https://registry.npmjs.org/hardhat-abi-exporter/-/hardhat-abi-exporter-2.8.0.tgz" - integrity sha512-HQwd9Agr2O5znUg9Dzicw8grsXacoMSQsS5ZhBBNyaxKeVbvxL1Ubm9ss8sSVGr74511a8qiR2Ljm/lsLS9Mew== - dependencies: - "@ethersproject/abi" "^5.5.0" - delete-empty "^3.0.0" - -hardhat-contract-sizer@^2.5.0: - version "2.5.1" - resolved "https://registry.npmjs.org/hardhat-contract-sizer/-/hardhat-contract-sizer-2.5.1.tgz" - integrity sha512-28yRb73e30aBVaZOOHTlHZFIdIasA/iFunIehrUviIJTubvdQjtSiQUo2wexHFtt71mQeMPP8qjw2sdbgatDnQ== - dependencies: - chalk "^4.0.0" - cli-table3 "^0.6.0" - -hardhat-gas-reporter@^1.0.8: - version "1.0.8" - resolved "https://registry.npmjs.org/hardhat-gas-reporter/-/hardhat-gas-reporter-1.0.8.tgz" - integrity sha512-1G5thPnnhcwLHsFnl759f2tgElvuwdkzxlI65fC9PwxYMEe9cmjkVAAWTf3/3y8uP6ZSPiUiOW8PgZnykmZe0g== - dependencies: - array-uniq "1.0.3" - eth-gas-reporter "^0.2.24" - sha1 "^1.1.1" - -hardhat@^2.9.0: - version "2.9.3" - resolved "https://registry.npmjs.org/hardhat/-/hardhat-2.9.3.tgz" - integrity sha512-7Vw99RbYbMZ15UzegOR/nqIYIqddZXvLwJGaX5sX4G5bydILnbjmDU6g3jMKJSiArEixS3vHAEaOs5CW1JQ3hg== - dependencies: - "@ethereumjs/block" "^3.6.0" - "@ethereumjs/blockchain" "^5.5.0" - "@ethereumjs/common" "^2.6.0" - "@ethereumjs/tx" "^3.4.0" - "@ethereumjs/vm" "^5.6.0" - "@ethersproject/abi" "^5.1.2" - "@metamask/eth-sig-util" "^4.0.0" - "@sentry/node" "^5.18.1" - "@solidity-parser/parser" "^0.14.1" - "@types/bn.js" "^5.1.0" - "@types/lru-cache" "^5.1.0" - abort-controller "^3.0.0" - adm-zip "^0.4.16" - aggregate-error "^3.0.0" - ansi-escapes "^4.3.0" - chalk "^2.4.2" - chokidar "^3.4.0" - ci-info "^2.0.0" - debug "^4.1.1" - enquirer "^2.3.0" - env-paths "^2.2.0" - ethereum-cryptography "^0.1.2" - ethereumjs-abi "^0.6.8" - ethereumjs-util "^7.1.3" - find-up "^2.1.0" - fp-ts "1.19.3" - fs-extra "^7.0.1" - glob "^7.1.3" - immutable "^4.0.0-rc.12" - io-ts "1.10.4" - lodash "^4.17.11" - merkle-patricia-tree "^4.2.2" - mnemonist "^0.38.0" - mocha "^9.2.0" - p-map "^4.0.0" - qs "^6.7.0" - raw-body "^2.4.1" - resolve "1.17.0" - semver "^6.3.0" - slash "^3.0.0" - solc "0.7.3" - source-map-support "^0.5.13" - stacktrace-parser "^0.1.10" - "true-case-path" "^2.2.1" - tsort "0.0.1" - undici "^4.14.1" - uuid "^8.3.2" - ws "^7.4.6" - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - -has-bigints@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz" - integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== - has-flag@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + 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.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbol-support-x@^1.4.1: - version "1.4.2" - resolved "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz" - integrity sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw== - -has-symbols@^1.0.0, has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -has-to-string-tag-x@^1.2.0: - version "1.4.1" - resolved "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz" - integrity sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw== - dependencies: - has-symbol-support-x "^1.4.1" - -has-tostringtag@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz" - integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== - dependencies: - has-symbols "^1.0.2" - -has-value@^0.3.1: - version "0.3.1" - resolved "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz" - integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= - dependencies: - get-value "^2.0.3" - has-values "^0.1.4" - isobject "^2.0.0" - -has-value@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz" - integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= - dependencies: - get-value "^2.0.6" - has-values "^1.0.0" - isobject "^3.0.0" - -has-values@^0.1.4: - version "0.1.4" - resolved "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz" - integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= - -has-values@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz" - integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" - -has@^1.0.3, has@~1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -hash-base@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz" - 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.3: - version "1.1.3" - resolved "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz" - integrity sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.0" - -hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: +hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3: version "1.1.7" - resolved "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== dependencies: inherits "^2.0.3" @@ -5184,169 +1728,36 @@ hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: he@1.2.0: version "1.2.0" - resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -heap@0.2.6: - version "0.2.6" - resolved "https://registry.npmjs.org/heap/-/heap-0.2.6.tgz" - integrity sha1-CH4fELBGky/IWU3Z5tN4r8nR5aw= - hmac-drbg@^1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz" - integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= + 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" -home-or-tmp@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz" - integrity sha1-42w/LSyufXRqhX440Y1fMqeILbg= - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.1" - -hosted-git-info@^2.1.4, hosted-git-info@^2.6.0: - version "2.8.9" - resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz" - integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== - -http-basic@^8.1.1: - version "8.1.3" - resolved "https://registry.npmjs.org/http-basic/-/http-basic-8.1.3.tgz" - integrity sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw== - dependencies: - caseless "^0.12.0" - concat-stream "^1.6.2" - http-response-object "^3.0.1" - parse-cache-control "^1.0.1" - -http-cache-semantics@^4.0.0: - version "4.1.0" - resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz" - integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== - -http-errors@1.8.1: - version "1.8.1" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz" - integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g== - dependencies: - depd "~1.1.2" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.1" - -http-errors@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" - integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== - dependencies: - depd "2.0.0" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses "2.0.1" - toidentifier "1.0.1" - -http-https@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/http-https/-/http-https-1.0.0.tgz" - integrity sha1-L5CN1fHbQGjAWM1ubUzjkskTOJs= - -http-response-object@^3.0.1: - version "3.0.2" - resolved "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz" - integrity sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA== - dependencies: - "@types/node" "^10.0.3" - -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -https-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== - dependencies: - agent-base "6" - debug "4" - human-signals@^2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== -iconv-lite@0.4.24, iconv-lite@^0.4.24: - version "0.4.24" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -iconv-lite@^0.6.2: - version "0.6.3" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - -idna-uts46-hx@^2.3.1: - version "2.3.1" - resolved "https://registry.npmjs.org/idna-uts46-hx/-/idna-uts46-hx-2.3.1.tgz" - integrity sha512-PWoF9Keq6laYdIRwwCdhTPl60xRqAloYNMQLiyUnG42VjT53oW07BXIRM+NK7eQjzXjAk2gUvX9caRxlnF9TAA== - dependencies: - punycode "2.1.0" - -ieee754@^1.1.13, ieee754@^1.2.1: +ieee754@^1.2.1: version "1.2.1" - resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - -ignore@^5.1.8, ignore@^5.2.0: - version "5.2.0" - resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz" - integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== - -immediate@^3.2.3: - version "3.3.0" - resolved "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz" - integrity sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q== - -immediate@~3.2.3: - version "3.2.3" - resolved "https://registry.npmjs.org/immediate/-/immediate-3.2.3.tgz" - integrity sha1-0UD6j2FGWb1lQSMwl92qwlzdmRw= - -immutable@^4.0.0-rc.12: - version "4.0.0" - resolved "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz" - integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw== - -import-fresh@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz" - integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY= - dependencies: - caller-path "^2.0.0" - resolve-from "^3.0.0" +ignore@^5.2.0, ignore@^5.2.4: + version "5.3.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" + integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== -import-fresh@^3.0.0, import-fresh@^3.2.1: +import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.0" - resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" + 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" @@ -5354,1397 +1765,332 @@ import-fresh@^3.0.0, import-fresh@^3.2.1: imurmurhash@^0.1.4: version "0.1.4" - resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== inflight@^1.0.4: version "1.0.6" - resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + 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.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: +inherits@2, inherits@^2.0.3, inherits@^2.0.4: version "2.0.4" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -inquirer@^6.2.2: - version "6.5.2" - resolved "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz" - integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ== - dependencies: - ansi-escapes "^3.2.0" - chalk "^2.4.2" - cli-cursor "^2.1.0" - cli-width "^2.0.0" - external-editor "^3.0.3" - figures "^2.0.0" - lodash "^4.17.12" - mute-stream "0.0.7" - run-async "^2.2.0" - rxjs "^6.4.0" - string-width "^2.1.0" - strip-ansi "^5.1.0" - through "^2.3.6" - -internal-slot@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz" - integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== - dependencies: - get-intrinsic "^1.1.0" - has "^1.0.3" - side-channel "^1.0.4" - -invariant@^2.2.2: - version "2.2.4" - resolved "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" - -invert-kv@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz" - integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= - -io-ts@1.10.4: - version "1.10.4" - resolved "https://registry.npmjs.org/io-ts/-/io-ts-1.10.4.tgz" - integrity sha512-b23PteSnYXSONJ6JQXRAlvJhuw8KOtkqa87W4wDtvMrud/DTJd5X+NpOOI+O/zZwVq6v0VLAaJ+1EDViKEuN9g== - dependencies: - fp-ts "^1.0.0" - -ipaddr.js@1.9.1: - version "1.9.1" - resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - -is-accessor-descriptor@^0.1.6: - version "0.1.6" - resolved "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz" - integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= - dependencies: - kind-of "^3.0.2" - -is-accessor-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz" - integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== - dependencies: - kind-of "^6.0.0" - -is-arguments@^1.0.4: - version "1.1.1" - resolved "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz" - integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - is-arrayish@^0.2.1: version "0.2.1" - resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - -is-bigint@^1.0.1: - version "1.0.4" - resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz" - integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== - dependencies: - has-bigints "^1.0.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== is-binary-path@~2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== dependencies: binary-extensions "^2.0.0" -is-boolean-object@^1.1.0: - version "1.1.2" - resolved "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz" - integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-buffer@~2.0.3: - version "2.0.5" - resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz" - integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== -is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.4: - version "1.2.4" - resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz" - integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== +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-ci@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz" - integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: - ci-info "^2.0.0" - -is-core-module@^2.8.1: - version "2.8.1" - resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz" - integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== - dependencies: - has "^1.0.3" - -is-data-descriptor@^0.1.4: - version "0.1.4" - resolved "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz" - integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= - dependencies: - kind-of "^3.0.2" - -is-data-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz" - integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== - dependencies: - kind-of "^6.0.0" - -is-date-object@^1.0.1: - version "1.0.5" - resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz" - integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== - dependencies: - has-tostringtag "^1.0.0" - -is-descriptor@^0.1.0: - version "0.1.6" - resolved "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz" - integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== - dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" - -is-descriptor@^1.0.0, is-descriptor@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz" - integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== - dependencies: - is-accessor-descriptor "^1.0.0" - is-data-descriptor "^1.0.0" - kind-of "^6.0.2" - -is-directory@^0.3.1: - version "0.3.1" - resolved "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz" - integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= - -is-docker@^2.0.0: - version "2.2.1" - resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz" - integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== - -is-extendable@^0.1.0, is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz" - integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= - -is-extendable@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz" - integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== - dependencies: - is-plain-object "^2.0.4" - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-finite@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz" - integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== - -is-fn@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-fn/-/is-fn-1.0.0.tgz" - integrity sha1-lUPV3nvPWwiiLsiiC65uKG1RDYw= - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-function@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz" - integrity sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ== - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: - version "4.0.3" - resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-hex-prefixed@1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz" - integrity sha1-fY035q135dEnFIkTxXPggtd39VQ= - -is-negative-zero@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz" - integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== - -is-number-object@^1.0.4: - version "1.0.7" - resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz" - integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== - dependencies: - has-tostringtag "^1.0.0" - -is-number@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz" - integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= - dependencies: - kind-of "^3.0.2" + is-extglob "^2.1.1" is-number@^7.0.0: version "7.0.0" - resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-object@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz" - integrity sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA== - -is-plain-obj@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz" - integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== is-plain-obj@^2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== -is-plain-object@^2.0.3, is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-regex@^1.0.4, is-regex@^1.1.4, is-regex@~1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz" - integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-retry-allowed@^1.0.0: - version "1.2.0" - resolved "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz" - integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== - -is-shared-array-buffer@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz" - integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== - dependencies: - call-bind "^1.0.2" - -is-stream@^1.0.0, is-stream@^1.0.1: - version "1.1.0" - resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= - is-stream@^2.0.0: version "2.0.1" - resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== -is-string@^1.0.5, is-string@^1.0.7: - version "1.0.7" - resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz" - integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== - dependencies: - has-tostringtag "^1.0.0" - -is-symbol@^1.0.2, is-symbol@^1.0.3: - version "1.0.4" - resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz" - integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== - dependencies: - has-symbols "^1.0.2" - -is-typedarray@^1.0.0, is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= - is-unicode-supported@^0.1.0: version "0.1.0" - resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== -is-url@^1.2.4: - version "1.2.4" - resolved "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz" - integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww== - -is-utf8@^0.2.0: - version "0.2.1" - resolved "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz" - integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= - -is-weakref@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz" - integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== - dependencies: - call-bind "^1.0.2" - -is-windows@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - -is-wsl@^2.1.1: - version "2.2.0" - resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz" - integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== - dependencies: - is-docker "^2.0.0" - -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" - integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= - -isarray@1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - isexe@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz" - integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= - dependencies: - isarray "1.0.0" - -isobject@^3.0.0, isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= - -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" - integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -isurl@^1.0.0-alpha5: - version "1.0.0" - resolved "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz" - integrity sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w== +jackspeak@^2.3.5: + version "2.3.6" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" + integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== dependencies: - has-to-string-tag-x "^1.2.0" - is-object "^1.0.1" + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" joycon@^3.0.1: version "3.1.1" - resolved "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz" + resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== -js-sha3@0.5.7, js-sha3@^0.5.7: - version "0.5.7" - resolved "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.7.tgz" - integrity sha1-DU/9gALVMzqrr0oj7tL2N0yfKOc= - js-sha3@0.8.0, js-sha3@^0.8.0: version "0.8.0" - resolved "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: +js-tokens@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-tokens@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz" - integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= - -js-yaml@3.13.1: - version "3.13.1" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz" - integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - js-yaml@4.1.0, js-yaml@^4.1.0: version "4.1.0" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== dependencies: argparse "^2.0.1" -js-yaml@^3.12.0, js-yaml@^3.13.0, js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz" - integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= - -jsesc@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz" - integrity sha1-RsP+yMGJKxKwgz25vHYiF226s0s= - -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz" - integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= - -json-buffer@3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz" - integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= - -json-parse-better-errors@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - -json-rpc-engine@^3.4.0, json-rpc-engine@^3.6.0: - version "3.8.0" - resolved "https://registry.npmjs.org/json-rpc-engine/-/json-rpc-engine-3.8.0.tgz" - integrity sha512-6QNcvm2gFuuK4TKU1uwfH0Qd/cOSb9c1lls0gbnIhciktIUQJwz6NQNAW4B1KiGPenv7IKu97V222Yo1bNhGuA== - dependencies: - async "^2.0.1" - babel-preset-env "^1.7.0" - babelify "^7.3.0" - json-rpc-error "^2.0.0" - promise-to-callback "^1.0.0" - safe-event-emitter "^1.0.1" - -json-rpc-error@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/json-rpc-error/-/json-rpc-error-2.0.0.tgz" - integrity sha1-p6+cICg4tekFxyUOVH8a/3cligI= - dependencies: - inherits "^2.0.1" +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== -json-rpc-random-id@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/json-rpc-random-id/-/json-rpc-random-id-1.0.1.tgz" - integrity sha1-uknZat7RRE27jaPSA3SKy7zeyMg= +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.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== -json-schema@0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz" - integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== +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== json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" - integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= - -json-stable-stringify@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz" - integrity sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8= - dependencies: - jsonify "~0.0.0" - -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" - integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= - -json5@^0.5.1: - version "0.5.1" - resolved "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz" - integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= - -jsonfile@^2.1.0: - version "2.4.0" - resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz" - integrity sha1-NzaitCi4e72gzIO1P6PWM6NcKug= - optionalDependencies: - graceful-fs "^4.1.6" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== jsonfile@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz" - integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== optionalDependencies: graceful-fs "^4.1.6" jsonfile@^6.0.1: version "6.1.0" - resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== dependencies: universalify "^2.0.0" optionalDependencies: graceful-fs "^4.1.6" -jsonify@~0.0.0: - version "0.0.0" - resolved "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz" - integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= - -jsprim@^1.2.2: - version "1.4.2" - resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz" - integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.4.0" - verror "1.10.0" - keccak256@^1.0.6: version "1.0.6" - resolved "https://registry.npmjs.org/keccak256/-/keccak256-1.0.6.tgz" + 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.1: - version "3.0.1" - resolved "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz" - integrity sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA== - dependencies: - node-addon-api "^2.0.0" - node-gyp-build "^4.2.0" - -keccak@^3.0.0, keccak@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/keccak/-/keccak-3.0.2.tgz" - integrity sha512-PyKKjkH53wDMLGrvmRGSNWgmSxZOUqbnXwKL9tmgbFYA1iAYqW21kfR7mZXV0MlESiefxQQE9X9fTa3X+2MPDQ== +keccak@^3.0.2: + version "3.0.4" + resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.4.tgz#edc09b89e633c0549da444432ecf062ffadee86d" + integrity sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q== dependencies: node-addon-api "^2.0.0" node-gyp-build "^4.2.0" readable-stream "^3.6.0" -keyv@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz" - integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== - dependencies: - json-buffer "3.0.0" - -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: - version "3.2.2" - resolved "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz" - integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= - dependencies: - is-buffer "^1.1.5" - -kind-of@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz" - integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= - dependencies: - is-buffer "^1.1.5" - -kind-of@^5.0.0: - version "5.1.0" - resolved "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz" - integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== - -kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -klaw-sync@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz" - integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ== - dependencies: - graceful-fs "^4.1.11" - -klaw@^1.0.0: - version "1.3.1" - resolved "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz" - integrity sha1-QIhDO0azsbolnXh4XY6W9zugJDk= - optionalDependencies: - graceful-fs "^4.1.9" - -lcid@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz" - integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU= - dependencies: - invert-kv "^1.0.0" - -level-codec@^9.0.0: - version "9.0.2" - resolved "https://registry.npmjs.org/level-codec/-/level-codec-9.0.2.tgz" - integrity sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ== - dependencies: - buffer "^5.6.0" - -level-codec@~7.0.0: - version "7.0.1" - resolved "https://registry.npmjs.org/level-codec/-/level-codec-7.0.1.tgz" - integrity sha512-Ua/R9B9r3RasXdRmOtd+t9TCOEIIlts+TN/7XTT2unhDaL6sJn83S3rUyljbr6lVtw49N3/yA0HHjpV6Kzb2aQ== - -level-concat-iterator@~2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz" - integrity sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw== - -level-errors@^1.0.3: - version "1.1.2" - resolved "https://registry.npmjs.org/level-errors/-/level-errors-1.1.2.tgz" - integrity sha512-Sw/IJwWbPKF5Ai4Wz60B52yj0zYeqzObLh8k1Tk88jVmD51cJSKWSYpRyhVIvFzZdvsPqlH5wfhp/yxdsaQH4w== - dependencies: - errno "~0.1.1" - -level-errors@^2.0.0, level-errors@~2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/level-errors/-/level-errors-2.0.1.tgz" - integrity sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw== - dependencies: - errno "~0.1.1" - -level-errors@~1.0.3: - version "1.0.5" - resolved "https://registry.npmjs.org/level-errors/-/level-errors-1.0.5.tgz" - integrity sha512-/cLUpQduF6bNrWuAC4pwtUKA5t669pCsCi2XbmojG2tFeOr9j6ShtdDCtFFQO1DRt+EVZhx9gPzP9G2bUaG4ig== - dependencies: - errno "~0.1.1" - -level-iterator-stream@^2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-2.0.3.tgz" - integrity sha512-I6Heg70nfF+e5Y3/qfthJFexhRw/Gi3bIymCoXAlijZdAcLaPuWSJs3KXyTYf23ID6g0o2QF62Yh+grOXY3Rig== - dependencies: - inherits "^2.0.1" - readable-stream "^2.0.5" - xtend "^4.0.0" - -level-iterator-stream@~1.3.0: - version "1.3.1" - resolved "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-1.3.1.tgz" - integrity sha1-5Dt4sagUPm+pek9IXrjqUwNS8u0= - dependencies: - inherits "^2.0.1" - level-errors "^1.0.3" - readable-stream "^1.0.33" - xtend "^4.0.0" - -level-iterator-stream@~3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-3.0.1.tgz" - integrity sha512-nEIQvxEED9yRThxvOrq8Aqziy4EGzrxSZK+QzEFAVuJvQ8glfyZ96GB6BoI4sBbLfjMXm2w4vu3Tkcm9obcY0g== - dependencies: - inherits "^2.0.1" - readable-stream "^2.3.6" - xtend "^4.0.0" - -level-iterator-stream@~4.0.0: - version "4.0.2" - resolved "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-4.0.2.tgz" - integrity sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q== - dependencies: - inherits "^2.0.4" - readable-stream "^3.4.0" - xtend "^4.0.2" - -level-mem@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/level-mem/-/level-mem-3.0.1.tgz" - integrity sha512-LbtfK9+3Ug1UmvvhR2DqLqXiPW1OJ5jEh0a3m9ZgAipiwpSxGj/qaVVy54RG5vAQN1nCuXqjvprCuKSCxcJHBg== - dependencies: - level-packager "~4.0.0" - memdown "~3.0.0" - -level-mem@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/level-mem/-/level-mem-5.0.1.tgz" - integrity sha512-qd+qUJHXsGSFoHTziptAKXoLX87QjR7v2KMbqncDXPxQuCdsQlzmyX+gwrEHhlzn08vkf8TyipYyMmiC6Gobzg== - dependencies: - level-packager "^5.0.3" - memdown "^5.0.0" - -level-packager@^5.0.3: - version "5.1.1" - resolved "https://registry.npmjs.org/level-packager/-/level-packager-5.1.1.tgz" - integrity sha512-HMwMaQPlTC1IlcwT3+swhqf/NUO+ZhXVz6TY1zZIIZlIR0YSn8GtAAWmIvKjNY16ZkEg/JcpAuQskxsXqC0yOQ== - dependencies: - encoding-down "^6.3.0" - levelup "^4.3.2" - -level-packager@~4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/level-packager/-/level-packager-4.0.1.tgz" - integrity sha512-svCRKfYLn9/4CoFfi+d8krOtrp6RoX8+xm0Na5cgXMqSyRru0AnDYdLl+YI8u1FyS6gGZ94ILLZDE5dh2but3Q== - dependencies: - encoding-down "~5.0.0" - levelup "^3.0.0" - -level-post@^1.0.7: - version "1.0.7" - resolved "https://registry.npmjs.org/level-post/-/level-post-1.0.7.tgz" - integrity sha512-PWYqG4Q00asOrLhX7BejSajByB4EmG2GaKHfj3h5UmmZ2duciXLPGYWIjBzLECFWUGOZWlm5B20h/n3Gs3HKew== - dependencies: - ltgt "^2.1.2" - -level-sublevel@6.6.4: - version "6.6.4" - resolved "https://registry.npmjs.org/level-sublevel/-/level-sublevel-6.6.4.tgz" - integrity sha512-pcCrTUOiO48+Kp6F1+UAzF/OtWqLcQVTVF39HLdZ3RO8XBoXt+XVPKZO1vVr1aUoxHZA9OtD2e1v7G+3S5KFDA== - dependencies: - bytewise "~1.1.0" - level-codec "^9.0.0" - level-errors "^2.0.0" - level-iterator-stream "^2.0.3" - ltgt "~2.1.1" - pull-defer "^0.2.2" - pull-level "^2.0.3" - pull-stream "^3.6.8" - typewiselite "~1.0.0" - xtend "~4.0.0" - -level-supports@~1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/level-supports/-/level-supports-1.0.1.tgz" - integrity sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg== - dependencies: - xtend "^4.0.2" - -level-ws@0.0.0: - version "0.0.0" - resolved "https://registry.npmjs.org/level-ws/-/level-ws-0.0.0.tgz" - integrity sha1-Ny5RIXeSSgBCSwtDrvK7QkltIos= - dependencies: - readable-stream "~1.0.15" - xtend "~2.1.1" - -level-ws@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/level-ws/-/level-ws-1.0.0.tgz" - integrity sha512-RXEfCmkd6WWFlArh3X8ONvQPm8jNpfA0s/36M4QzLqrLEIt1iJE9WBHLZ5vZJK6haMjJPJGJCQWfjMNnRcq/9Q== - dependencies: - inherits "^2.0.3" - readable-stream "^2.2.8" - xtend "^4.0.1" - -level-ws@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/level-ws/-/level-ws-2.0.0.tgz" - integrity sha512-1iv7VXx0G9ec1isqQZ7y5LmoZo/ewAsyDHNA8EFDW5hqH2Kqovm33nSFkSdnLLAK+I5FlT+lo5Cw9itGe+CpQA== - dependencies: - inherits "^2.0.3" - readable-stream "^3.1.0" - xtend "^4.0.1" - -levelup@3.1.1, levelup@^3.0.0: - version "3.1.1" - resolved "https://registry.npmjs.org/levelup/-/levelup-3.1.1.tgz" - integrity sha512-9N10xRkUU4dShSRRFTBdNaBxofz+PGaIZO962ckboJZiNmLuhVT6FZ6ZKAsICKfUBO76ySaYU6fJWX/jnj3Lcg== - dependencies: - deferred-leveldown "~4.0.0" - level-errors "~2.0.0" - level-iterator-stream "~3.0.0" - xtend "~4.0.0" - -levelup@^1.2.1: - version "1.3.9" - resolved "https://registry.npmjs.org/levelup/-/levelup-1.3.9.tgz" - integrity sha512-VVGHfKIlmw8w1XqpGOAGwq6sZm2WwWLmlDcULkKWQXEA5EopA8OBNJ2Ck2v6bdk8HeEZSbCSEgzXadyQFm76sQ== - dependencies: - deferred-leveldown "~1.2.1" - level-codec "~7.0.0" - level-errors "~1.0.3" - level-iterator-stream "~1.3.0" - prr "~1.0.1" - semver "~5.4.1" - xtend "~4.0.0" - -levelup@^4.3.2: - version "4.4.0" - resolved "https://registry.npmjs.org/levelup/-/levelup-4.4.0.tgz" - integrity sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ== - dependencies: - deferred-leveldown "~5.3.0" - level-errors "~2.0.0" - level-iterator-stream "~4.0.0" - level-supports "~1.0.0" - xtend "~4.0.0" - -levn@^0.3.0, levn@~0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz" - integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" + json-buffer "3.0.1" levn@^0.4.1: version "0.4.1" - resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== dependencies: prelude-ls "^1.2.1" type-check "~0.4.0" lilconfig@^2.0.5: - version "2.0.5" - resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz" - integrity sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg== + 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.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -load-json-file@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz" - integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" - load-tsconfig@^0.2.0: - version "0.2.3" - resolved "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.3.tgz" - integrity sha512-iyT2MXws+dc2Wi6o3grCFtGXpeMvHmJqS27sMPGtV2eUu4PeFnG+33I8BlFK1t1NWMjOpcx9bridn5yxLDX2gQ== - -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz" - integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" - -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" + version "0.2.5" + resolved "https://registry.yarnpkg.com/load-tsconfig/-/load-tsconfig-0.2.5.tgz#453b8cd8961bfb912dea77eb6c168fe8cca3d3a1" + integrity sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg== locate-path@^6.0.0: version "6.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== dependencies: p-locate "^5.0.0" -lodash.assign@^4.0.3, lodash.assign@^4.0.6: - version "4.2.0" - resolved "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz" - integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc= - lodash.camelcase@^4.3.0: version "4.3.0" - resolved "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz" - integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== lodash.merge@^4.6.2: version "4.6.2" - resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@4.17.20: - version "4.17.20" - resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== + +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.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4: +lodash@^4.17.15, lodash@^4.17.21: version "4.17.21" - resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -log-symbols@3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz" - integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== - dependencies: - chalk "^2.4.2" - log-symbols@4.1.0: version "4.1.0" - resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== dependencies: chalk "^4.1.0" is-unicode-supported "^0.1.0" -looper@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/looper/-/looper-2.0.0.tgz" - integrity sha1-Zs0Md0rz1P7axTeU90LbVtqPCew= - -looper@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/looper/-/looper-3.0.0.tgz" - integrity sha1-LvpUw7HLq6m5Su4uWRSwvlf7t0k= - -loose-envify@^1.0.0: - version "1.4.0" - resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz" - integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== - -lowercase-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz" - integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== - -lru-cache@5.1.1, lru-cache@^5.1.1: - version "5.1.1" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - -lru-cache@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-3.2.0.tgz" - integrity sha1-cXibO39Tmb7IVl3aOKow0qCX7+4= - dependencies: - pseudomap "^1.0.1" - lru-cache@^6.0.0: version "6.0.0" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== dependencies: yallist "^4.0.0" -lru_map@^0.3.3: - version "0.3.3" - resolved "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz" - integrity sha1-tcg1G5Rky9dQM1p5ZQoOwOVhGN0= - -ltgt@^2.1.2, ltgt@~2.2.0: - version "2.2.1" - resolved "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz" - integrity sha1-81ypHEk/e3PaDgdJUwTxezH4fuU= - -ltgt@~2.1.1: - version "2.1.3" - resolved "https://registry.npmjs.org/ltgt/-/ltgt-2.1.3.tgz" - integrity sha1-EIUaBtmWS5cReEQcI8nlJpjuzjQ= +"lru-cache@^9.1.1 || ^10.0.0": + version "10.1.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.1.0.tgz#2098d41c2dc56500e6c88584aa656c84de7d0484" + integrity sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag== make-error@^1.1.1: version "1.3.6" - resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== -map-cache@^0.2.2: - version "0.2.2" - resolved "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz" - integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= - -map-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz" - integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= - dependencies: - object-visit "^1.0.0" - -markdown-table@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/markdown-table/-/markdown-table-1.1.3.tgz" - integrity sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q== - -mcl-wasm@^0.7.1: - version "0.7.9" - resolved "https://registry.npmjs.org/mcl-wasm/-/mcl-wasm-0.7.9.tgz" - integrity sha512-iJIUcQWA88IJB/5L15GnJVnSQJmf/YaxxV6zRavv83HILHaJQb6y0iFyDMdDO0gN8X37tdxmAOrH/P8B6RB8sQ== - -md5.js@^1.3.4: - version "1.3.5" - resolved "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz" - integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" - integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= - -memdown@^1.0.0: - version "1.4.1" - resolved "https://registry.npmjs.org/memdown/-/memdown-1.4.1.tgz" - integrity sha1-tOThkhdGZP+65BNhqlAPMRnv4hU= - dependencies: - abstract-leveldown "~2.7.1" - functional-red-black-tree "^1.0.1" - immediate "^3.2.3" - inherits "~2.0.1" - ltgt "~2.2.0" - safe-buffer "~5.1.1" - -memdown@^5.0.0: - version "5.1.0" - resolved "https://registry.npmjs.org/memdown/-/memdown-5.1.0.tgz" - integrity sha512-B3J+UizMRAlEArDjWHTMmadet+UKwHd3UjMgGBkZcKAxAYVPS9o0Yeiha4qvz7iGiL2Sb3igUft6p7nbFWctpw== - dependencies: - abstract-leveldown "~6.2.1" - functional-red-black-tree "~1.0.1" - immediate "~3.2.3" - inherits "~2.0.1" - ltgt "~2.2.0" - safe-buffer "~5.2.0" - -memdown@~3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/memdown/-/memdown-3.0.0.tgz" - integrity sha512-tbV02LfZMWLcHcq4tw++NuqMO+FZX8tNJEiD2aNRm48ZZusVg5N8NART+dmBkepJVye986oixErf7jfXboMGMA== - dependencies: - abstract-leveldown "~5.0.0" - functional-red-black-tree "~1.0.1" - immediate "~3.2.3" - inherits "~2.0.1" - ltgt "~2.2.0" - safe-buffer "~5.1.1" - -memorystream@^0.3.1: - version "0.3.1" - resolved "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz" - integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI= - -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" - integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= - merge-stream@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" - resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -merkle-patricia-tree@3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/merkle-patricia-tree/-/merkle-patricia-tree-3.0.0.tgz" - integrity sha512-soRaMuNf/ILmw3KWbybaCjhx86EYeBbD8ph0edQCTed0JN/rxDt1EBN52Ajre3VyGo+91f8+/rfPIRQnnGMqmQ== - dependencies: - async "^2.6.1" - ethereumjs-util "^5.2.0" - level-mem "^3.0.1" - level-ws "^1.0.0" - readable-stream "^3.0.6" - rlp "^2.0.0" - semaphore ">=1.0.1" - -merkle-patricia-tree@^2.1.2, merkle-patricia-tree@^2.3.2: - version "2.3.2" - resolved "https://registry.npmjs.org/merkle-patricia-tree/-/merkle-patricia-tree-2.3.2.tgz" - integrity sha512-81PW5m8oz/pz3GvsAwbauj7Y00rqm81Tzad77tHBwU7pIAtN+TJnMSOJhxBKflSVYhptMMb9RskhqHqrSm1V+g== - dependencies: - async "^1.4.2" - ethereumjs-util "^5.0.0" - level-ws "0.0.0" - levelup "^1.2.1" - memdown "^1.0.0" - readable-stream "^2.0.0" - rlp "^2.0.0" - semaphore ">=1.0.1" - -merkle-patricia-tree@^4.2.2, merkle-patricia-tree@^4.2.4: - version "4.2.4" - resolved "https://registry.npmjs.org/merkle-patricia-tree/-/merkle-patricia-tree-4.2.4.tgz" - integrity sha512-eHbf/BG6eGNsqqfbLED9rIqbsF4+sykEaBn6OLNs71tjclbMcMOk1tEPmJKcNcNCLkvbpY/lwyOlizWsqPNo8w== - dependencies: - "@types/levelup" "^4.3.0" - ethereumjs-util "^7.1.4" - level-mem "^5.0.1" - level-ws "^2.0.0" - readable-stream "^3.6.0" - semaphore-async-await "^1.5.1" - -merkletreejs@^0.2.31: - version "0.2.31" - resolved "https://registry.npmjs.org/merkletreejs/-/merkletreejs-0.2.31.tgz" - integrity sha512-dnK2sE43OebmMe5Qnq1wXvvMIjZjm1u6CcB2KeW6cghlN4p21OpCUr2p56KTVf20KJItNChVsGnimcscp9f+yw== - 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" - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" - integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= - -micromatch@^3.1.4: - version "3.1.10" - resolved "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz" - integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.1" - define-property "^2.0.2" - extend-shallow "^3.0.2" - extglob "^2.0.4" - fragment-cache "^0.2.1" - kind-of "^6.0.2" - nanomatch "^1.2.9" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.2" - -micromatch@^4.0.2, micromatch@^4.0.4: +micromatch@^4.0.4: version "4.0.5" - resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz" + 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" -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.12, mime-types@^2.1.16, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: - version "2.1.35" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mime@1.6.0: - version "1.6.0" - resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mimic-fn@^1.0.0: - version "1.2.0" - resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz" - integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== - mimic-fn@^2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -mimic-response@^1.0.0, mimic-response@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz" - integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== - -min-document@^2.19.0: - version "2.19.0" - resolved "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz" - integrity sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU= - dependencies: - dom-walk "^0.1.0" - minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" + 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.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz" - integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= - -minimatch@3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== minimatch@4.2.1: version "4.2.1" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-4.2.1.tgz#40d9d511a46bdc4e563c22c3080cde9c0d8299b4" integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g== dependencies: brace-expansion "^1.1.7" -minimatch@^3.0.4: +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" -minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6, minimist@~1.2.6: - version "1.2.6" - resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz" - integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== - -minipass@^2.6.0, minipass@^2.9.0: - version "2.9.0" - resolved "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz" - integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== - dependencies: - safe-buffer "^5.1.2" - yallist "^3.0.0" - -minizlib@^1.3.3: - version "1.3.3" - resolved "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz" - integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== +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: - minipass "^2.9.0" + brace-expansion "^2.0.1" -mixin-deep@^1.2.0: - version "1.3.2" - resolved "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz" - integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== +minimatch@^9.0.1: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== dependencies: - for-in "^1.0.2" - is-extendable "^1.0.1" + brace-expansion "^2.0.1" -mkdirp-promise@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/mkdirp-promise/-/mkdirp-promise-5.0.1.tgz" - integrity sha1-6bj2jlUsaKnBcTuEiD96HdA5uKE= - dependencies: - mkdirp "*" +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": + version "7.0.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" + integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== -mkdirp@*, mkdirp@^1.0.4: +mkdirp@^1.0.4: version "1.0.4" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mkdirp@0.5.5: - version "0.5.5" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" - -mkdirp@^0.5.1, mkdirp@^0.5.5: - version "0.5.6" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz" - integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== - dependencies: - minimist "^1.2.6" - -mnemonist@^0.38.0: - version "0.38.5" - resolved "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.5.tgz" - integrity sha512-bZTFT5rrPKtPJxj8KSV0WkPyNxl72vQepqqVUAW2ARUpUSF2qXMB6jZj7hW5/k7C1rtpzqbD/IIbJwLXUjCHeg== - dependencies: - obliterator "^2.0.0" - -mocha@^7.1.1: - version "7.2.0" - resolved "https://registry.npmjs.org/mocha/-/mocha-7.2.0.tgz" - integrity sha512-O9CIypScywTVpNaRrCAgoUnJgozpIofjKUYmJhiCIJMiuYnLI6otcb1/kpW9/n/tJODHGZ7i8aLQoDVsMtOKQQ== - dependencies: - ansi-colors "3.2.3" - browser-stdout "1.3.1" - chokidar "3.3.0" - debug "3.2.6" - diff "3.5.0" - escape-string-regexp "1.0.5" - find-up "3.0.0" - glob "7.1.3" - growl "1.10.5" - he "1.2.0" - js-yaml "3.13.1" - log-symbols "3.0.0" - minimatch "3.0.4" - mkdirp "0.5.5" - ms "2.1.1" - node-environment-flags "1.0.6" - object.assign "4.1.0" - strip-json-comments "2.0.1" - supports-color "6.0.0" - which "1.3.1" - wide-align "1.1.3" - yargs "13.3.2" - yargs-parser "13.1.2" - yargs-unparser "1.6.0" - -mocha@^9.2.0, mocha@^9.2.1: +mocha@^9.2.2: version "9.2.2" - resolved "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.2.2.tgz#d70db46bdb93ca57402c809333e5a84977a88fb9" integrity sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g== dependencies: "@ungap/promise-all-settled" "1.1.2" @@ -6772,2094 +2118,485 @@ mocha@^9.2.0, mocha@^9.2.1: yargs-parser "20.2.4" yargs-unparser "2.0.0" -mock-fs@^4.1.0: - version "4.14.0" - resolved "https://registry.npmjs.org/mock-fs/-/mock-fs-4.14.0.tgz" - integrity sha512-qYvlv/exQ4+svI3UOvPUpLDF0OMX5euvUH0Ny4N5QyRyhNdgAgUrVH3iUINSzEPLvx0kbo/Bp28GJKIqvE7URw== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== - ms@2.1.2: version "2.1.2" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.1.1: +ms@2.1.3: version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -multibase@^0.7.0: - version "0.7.0" - resolved "https://registry.npmjs.org/multibase/-/multibase-0.7.0.tgz" - integrity sha512-TW8q03O0f6PNFTQDvh3xxH03c8CjGaaYrjkl9UQPG6rz53TQzzxJVCIWVjzcbN/Q5Y53Zd0IBQBMVktVgNx4Fg== - dependencies: - base-x "^3.0.8" - buffer "^5.5.0" - -multibase@~0.6.0: - version "0.6.1" - resolved "https://registry.npmjs.org/multibase/-/multibase-0.6.1.tgz" - integrity sha512-pFfAwyTjbbQgNc3G7D48JkJxWtoJoBMaR4xQUOuB8RnCgRqaYmWNFeJTTvrJ2w51bjLq2zTby6Rqj9TQ9elSUw== - dependencies: - base-x "^3.0.8" - buffer "^5.5.0" - -multicodec@^0.5.5: - version "0.5.7" - resolved "https://registry.npmjs.org/multicodec/-/multicodec-0.5.7.tgz" - integrity sha512-PscoRxm3f+88fAtELwUnZxGDkduE2HD9Q6GHUOywQLjOGT/HAdhjLDYNZ1e7VR0s0TP0EwZ16LNUTFpoBGivOA== - dependencies: - varint "^5.0.0" - -multicodec@^1.0.0: - version "1.0.4" - resolved "https://registry.npmjs.org/multicodec/-/multicodec-1.0.4.tgz" - integrity sha512-NDd7FeS3QamVtbgfvu5h7fd1IlbaC4EQ0/pgU4zqE2vdHCmBGsUa0TiM8/TdSeG6BMPC92OOCf8F1ocE/Wkrrg== - dependencies: - buffer "^5.6.0" - varint "^5.0.0" - -multihashes@^0.4.15, multihashes@~0.4.15: - version "0.4.21" - resolved "https://registry.npmjs.org/multihashes/-/multihashes-0.4.21.tgz" - integrity sha512-uVSvmeCWf36pU2nB4/1kzYZjsXD9vofZKpgudqkceYY5g2aZZXJ5r9lxuzoRLl1OAp28XljXsEJ/X/85ZsKmKw== - dependencies: - buffer "^5.5.0" - multibase "^0.7.0" - varint "^5.0.0" - -mute-stream@0.0.7: - version "0.0.7" - resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz" - integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= - mz@^2.7.0: version "2.7.0" - resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== dependencies: any-promise "^1.0.0" object-assign "^4.0.1" thenify-all "^1.0.0" -nano-json-stream-parser@^0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/nano-json-stream-parser/-/nano-json-stream-parser-0.1.2.tgz" - integrity sha1-DMj20OK2IrR5xA1JnEbWS3Vcb18= - nanoid@3.3.1: version "3.3.1" - resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== -nanomatch@^1.2.9: - version "1.2.13" - resolved "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz" - integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - define-property "^2.0.2" - extend-shallow "^3.0.2" - fragment-cache "^0.2.1" - is-windows "^1.0.2" - kind-of "^6.0.2" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" +natural-compare-lite@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" + integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== natural-compare@^1.4.0: version "1.4.0" - resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" - integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= - -negotiator@0.6.3: - version "0.6.3" - resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" - integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== - -next-tick@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz" - integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== - -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== node-addon-api@^2.0.0: version "2.0.2" - resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== -node-environment-flags@1.0.6: - version "1.0.6" - resolved "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz" - integrity sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw== - dependencies: - object.getownpropertydescriptors "^2.0.3" - semver "^5.7.0" - -node-fetch@^2.6.1, node-fetch@^2.6.7: - version "2.6.7" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== - dependencies: - whatwg-url "^5.0.0" - -node-fetch@~1.7.1: - version "1.7.3" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz" - integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ== - dependencies: - encoding "^0.1.11" - is-stream "^1.0.1" - -node-gyp-build@^4.2.0, node-gyp-build@^4.3.0: - version "4.4.0" - resolved "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.4.0.tgz" - integrity sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ== - -nofilter@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/nofilter/-/nofilter-1.0.4.tgz" - integrity sha512-N8lidFp+fCz+TD51+haYdbDGrcBWwuHX40F5+z0qkUjMJ5Tp+rdSuAkMJ9N9eoolDlEVTf6u5icM+cNKkKW2mA== - -normalize-package-data@^2.3.2: - version "2.5.0" - resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== - dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" +node-gyp-build@^4.2.0: + version "4.8.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.0.tgz#3fee9c1731df4581a3f9ead74664369ff00d26dd" + integrity sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og== normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -normalize-url@^4.1.0: - version "4.5.1" - resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz" - integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== - npm-run-path@^4.0.1: version "4.0.1" - resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== dependencies: path-key "^3.0.0" -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= - -number-to-bn@1.7.0: - version "1.7.0" - resolved "https://registry.npmjs.org/number-to-bn/-/number-to-bn-1.7.0.tgz" - integrity sha1-uzYjWS9+X54AMLGXe9QaDFP+HqA= - dependencies: - bn.js "4.11.6" - strip-hex-prefix "1.0.0" - -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - -object-assign@^4, object-assign@^4.0.0, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@^4.0.1: version "4.1.1" - resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -object-copy@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz" - integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= - dependencies: - copy-descriptor "^0.1.0" - define-property "^0.2.5" - kind-of "^3.0.3" - -object-inspect@^1.12.0, object-inspect@^1.9.0, object-inspect@~1.12.0: - version "1.12.0" - resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz" - integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== - -object-is@^1.0.1: - version "1.1.5" - resolved "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz" - integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object-keys@~0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz" - integrity sha1-KKaq50KN0sOpLz2V8hM13SBOAzY= - -object-visit@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz" - integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= - dependencies: - isobject "^3.0.0" - -object.assign@4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz" - integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== - dependencies: - define-properties "^1.1.2" - function-bind "^1.1.1" - has-symbols "^1.0.0" - object-keys "^1.0.11" - -object.assign@^4.1.2: - version "4.1.2" - resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz" - integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - has-symbols "^1.0.1" - object-keys "^1.1.1" - -object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.1: - version "2.1.3" - resolved "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz" - integrity sha512-VdDoCwvJI4QdC6ndjpqFmoL3/+HxffFBbcJzKi5hwLLqqx3mdbedRpfZDdK0SrOSauj8X4GzBvnDZl4vTN7dOw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - -object.pick@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz" - integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= - dependencies: - isobject "^3.0.1" - -obliterator@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/obliterator/-/obliterator-2.0.2.tgz" - integrity sha512-g0TrA7SbUggROhDPK8cEu/qpItwH2LSKcNl4tlfBNT54XY+nOsqrs0Q68h1V9b3HOSpIWv15jb1lax2hAggdIg== - -oboe@2.1.4: - version "2.1.4" - resolved "https://registry.npmjs.org/oboe/-/oboe-2.1.4.tgz" - integrity sha1-IMiM2wwVNxuwQRklfU/dNLCqSfY= - dependencies: - http-https "^1.0.0" - -on-finished@2.4.1: - version "2.4.1" - resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" - integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== - dependencies: - ee-first "1.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" - integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= - dependencies: - ee-first "1.1.1" - -once@^1.3.0, once@^1.3.1, once@^1.4.0: +once@^1.3.0: version "1.4.0" - resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" -onetime@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz" - integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= - dependencies: - mimic-fn "^1.0.0" - onetime@^5.1.2: version "5.1.2" - resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== dependencies: mimic-fn "^2.1.0" -open@^7.4.2: - version "7.4.2" - resolved "https://registry.npmjs.org/open/-/open-7.4.2.tgz" - integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== - dependencies: - is-docker "^2.0.0" - is-wsl "^2.1.1" - -optionator@^0.8.2: - version "0.8.3" - resolved "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz" - integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.6" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - word-wrap "~1.2.3" - -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== +optionator@^0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" + integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== dependencies: + "@aashutoshrathi/word-wrap" "^1.2.3" deep-is "^0.1.3" fast-levenshtein "^2.0.6" levn "^0.4.1" prelude-ls "^1.2.1" type-check "^0.4.0" - word-wrap "^1.2.3" - -os-homedir@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz" - integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= - -os-locale@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz" - integrity sha1-IPnxeuKe00XoveWDsT0gCYA8FNk= - dependencies: - lcid "^1.0.0" - -os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= - -p-cancelable@^0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz" - integrity sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw== - -p-cancelable@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz" - integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== - -p-finally@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz" - integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= - -p-limit@^1.1.0: - version "1.3.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz" - integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== - dependencies: - p-try "^1.0.0" - -p-limit@^2.0.0: - version "2.3.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" p-limit@^3.0.2: version "3.1.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== dependencies: yocto-queue "^0.1.0" -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz" - integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= - dependencies: - p-limit "^1.1.0" - -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - p-locate@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== dependencies: p-limit "^3.0.2" -p-map@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz" - integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== - dependencies: - aggregate-error "^3.0.0" - -p-timeout@^1.1.1: - version "1.2.1" - resolved "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz" - integrity sha1-XrOzU7f86Z8QGhA4iAuwVOu+o4Y= - dependencies: - p-finally "^1.0.0" - -p-try@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz" - integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - parent-module@^1.0.0: version "1.0.1" - resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" + 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-asn1@^5.0.0, parse-asn1@^5.1.5: - version "5.1.6" - resolved "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz" - integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw== - dependencies: - asn1.js "^5.2.0" - browserify-aes "^1.0.0" - evp_bytestokey "^1.0.0" - pbkdf2 "^3.0.3" - safe-buffer "^5.1.1" - -parse-cache-control@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz" - integrity sha1-juqz5U+laSD+Fro493+iGqzC104= - -parse-headers@^2.0.0: - version "2.0.5" - resolved "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz" - integrity sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA== - -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz" - integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= - dependencies: - error-ex "^1.2.0" - -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz" - integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= +parse-json@^5.2.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-better-errors "^1.0.1" - -parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -pascalcase@^0.1.1: - version "0.1.1" - resolved "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz" - integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= - -patch-package@6.2.2: - version "6.2.2" - resolved "https://registry.npmjs.org/patch-package/-/patch-package-6.2.2.tgz" - integrity sha512-YqScVYkVcClUY0v8fF0kWOjDYopzIM8e3bj/RU1DPeEF14+dCGm6UeOYm4jvCyxqIEQ5/eJzmbWfDWnUleFNMg== - dependencies: - "@yarnpkg/lockfile" "^1.1.0" - chalk "^2.4.2" - cross-spawn "^6.0.5" - find-yarn-workspace-root "^1.2.1" - fs-extra "^7.0.1" - is-ci "^2.0.0" - klaw-sync "^6.0.0" - minimist "^1.2.0" - rimraf "^2.6.3" - semver "^5.6.0" - slash "^2.0.0" - tmp "^0.0.33" - -patch-package@^6.2.2: - version "6.4.7" - resolved "https://registry.npmjs.org/patch-package/-/patch-package-6.4.7.tgz" - integrity sha512-S0vh/ZEafZ17hbhgqdnpunKDfzHQibQizx9g8yEf5dcVk3KOflOfdufRXQX8CSEkyOQwuM/bNz1GwKvFj54kaQ== - dependencies: - "@yarnpkg/lockfile" "^1.1.0" - chalk "^2.4.2" - cross-spawn "^6.0.5" - find-yarn-workspace-root "^2.0.0" - fs-extra "^7.0.1" - is-ci "^2.0.0" - klaw-sync "^6.0.0" - minimist "^1.2.0" - open "^7.4.2" - rimraf "^2.6.3" - semver "^5.6.0" - slash "^2.0.0" - tmp "^0.0.33" - -path-browserify@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz" - integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== - -path-exists@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz" - integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= - dependencies: - pinkie-promise "^2.0.0" - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" path-exists@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== -path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: +path-is-absolute@^1.0.0: version "1.0.1" - resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-is-inside@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz" - integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= - -path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" - resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.6, path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -path-starts-with@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/path-starts-with/-/path-starts-with-2.0.0.tgz" - integrity sha512-3UHTHbJz5+NLkPafFR+2ycJOjoc4WV2e9qCZCnm71zHiWaFrm1XniLVTkZXvaRgxr1xFh9JsTdicpH2yM03nLA== - -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" - integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= - -path-type@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz" - integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= +path-scurry@^1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698" + integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== dependencies: - graceful-fs "^4.1.2" - pify "^2.0.0" - pinkie-promise "^2.0.0" + lru-cache "^9.1.1 || ^10.0.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" path-type@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -pbkdf2@^3.0.17, pbkdf2@^3.0.3, pbkdf2@^3.0.9: - version "3.1.2" - resolved "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz" - 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" - -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= - picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -pify@^2.0.0, pify@^2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= - -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz" - integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz" - integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= - pirates@^4.0.1: - version "4.0.5" - resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz" - integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== + version "4.0.6" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" + integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== -posix-character-classes@^0.1.0: - version "0.1.1" - resolved "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz" - integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= +pluralize@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== postcss-load-config@^3.0.1: version "3.1.4" - resolved "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855" integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg== dependencies: lilconfig "^2.0.5" yaml "^1.10.2" -postinstall-postinstall@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz" - integrity sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ== - -precond@0.2: - version "0.2.3" - resolved "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz" - integrity sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw= - prelude-ls@^1.2.1: version "1.2.1" - resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz" - integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= - -prepend-http@^1.0.1: - version "1.0.4" - resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz" - integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= - -prepend-http@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz" - integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= - prettier-linter-helpers@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== dependencies: fast-diff "^1.1.2" -prettier-plugin-solidity@^1.0.0-beta.19: - version "1.0.0-beta.19" - resolved "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.0.0-beta.19.tgz" - integrity sha512-xxRQ5ZiiZyUoMFLE9h7HnUDXI/daf1tnmL1msEdcKmyh7ZGQ4YklkYLC71bfBpYU2WruTb5/SFLUaEb3RApg5g== +prettier-plugin-solidity@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/prettier-plugin-solidity/-/prettier-plugin-solidity-1.3.1.tgz#59944d3155b249f7f234dee29f433524b9a4abcf" + integrity sha512-MN4OP5I2gHAzHZG1wcuJl0FsLS3c4Cc5494bbg+6oQWBPuEamjwDvmGfFMZ6NFzsh3Efd9UUxeT7ImgjNH4ozA== dependencies: - "@solidity-parser/parser" "^0.14.0" - emoji-regex "^10.0.0" - escape-string-regexp "^4.0.0" - semver "^7.3.5" - solidity-comments-extractor "^0.0.7" - string-width "^4.2.3" + "@solidity-parser/parser" "^0.17.0" + semver "^7.5.4" + solidity-comments-extractor "^0.0.8" -prettier@^1.14.3: - version "1.19.1" - resolved "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz" - integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== +prettier@^2.3.1, prettier@^2.8.3, prettier@^2.8.8: + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== -prettier@^2.1.2, prettier@^2.3.1, prettier@^2.5.1: - version "2.6.2" - resolved "https://registry.npmjs.org/prettier/-/prettier-2.6.2.tgz" - integrity sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew== - -private@^0.1.6, private@^0.1.8: - version "0.1.8" - resolved "https://registry.npmjs.org/private/-/private-0.1.8.tgz" - integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -process@^0.11.10: - version "0.11.10" - resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz" - integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= +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" -progress@^2.0.0: - version "2.0.3" - resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - -promise-to-callback@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/promise-to-callback/-/promise-to-callback-1.0.0.tgz" - integrity sha1-XSp0kBC/tn2WNZj805YHRqaP7vc= - dependencies: - is-fn "^1.0.0" - set-immediate-shim "^1.0.1" - -promise@^8.0.0: - version "8.1.0" - resolved "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz" - integrity sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q== - dependencies: - asap "~2.0.6" - -proxy-addr@~2.0.7: - version "2.0.7" - resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz" - integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== - dependencies: - forwarded "0.2.0" - ipaddr.js "1.9.1" - -prr@~1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz" - integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= - -pseudomap@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz" - integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= - -psl@^1.1.28: - version "1.8.0" - resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz" - integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== - -public-encrypt@^4.0.0: - version "4.0.3" - resolved "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz" - integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - safe-buffer "^5.1.2" - -pull-cat@^1.1.9: - version "1.1.11" - resolved "https://registry.npmjs.org/pull-cat/-/pull-cat-1.1.11.tgz" - integrity sha1-tkLdElXaN2pwa220+pYvX9t0wxs= - -pull-defer@^0.2.2: - version "0.2.3" - resolved "https://registry.npmjs.org/pull-defer/-/pull-defer-0.2.3.tgz" - integrity sha512-/An3KE7mVjZCqNhZsr22k1Tx8MACnUnHZZNPSJ0S62td8JtYr/AiRG42Vz7Syu31SoTLUzVIe61jtT/pNdjVYA== - -pull-level@^2.0.3: - version "2.0.4" - resolved "https://registry.npmjs.org/pull-level/-/pull-level-2.0.4.tgz" - integrity sha512-fW6pljDeUThpq5KXwKbRG3X7Ogk3vc75d5OQU/TvXXui65ykm+Bn+fiktg+MOx2jJ85cd+sheufPL+rw9QSVZg== - dependencies: - level-post "^1.0.7" - pull-cat "^1.1.9" - pull-live "^1.0.1" - pull-pushable "^2.0.0" - pull-stream "^3.4.0" - pull-window "^2.1.4" - stream-to-pull-stream "^1.7.1" - -pull-live@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/pull-live/-/pull-live-1.0.1.tgz" - integrity sha1-pOzuAeMwFV6RJLu89HYfIbOPUfU= - dependencies: - pull-cat "^1.1.9" - pull-stream "^3.4.0" - -pull-pushable@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/pull-pushable/-/pull-pushable-2.2.0.tgz" - integrity sha1-Xy867UethpGfAbEqLpnW8b13ZYE= - -pull-stream@^3.2.3, pull-stream@^3.4.0, pull-stream@^3.6.8: - version "3.6.14" - resolved "https://registry.npmjs.org/pull-stream/-/pull-stream-3.6.14.tgz" - integrity sha512-KIqdvpqHHaTUA2mCYcLG1ibEbu/LCKoJZsBWyv9lSYtPkJPBq8m3Hxa103xHi6D2thj5YXa0TqK3L3GUkwgnew== - -pull-window@^2.1.4: - version "2.1.4" - resolved "https://registry.npmjs.org/pull-window/-/pull-window-2.1.4.tgz" - integrity sha1-/DuG/uvRkgx64pdpHiP3BfiFUvA= - dependencies: - looper "^2.0.0" - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= - -punycode@2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz" - integrity sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0= - -punycode@^2.1.0, punycode@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -qs@6.10.3, qs@^6.4.0, qs@^6.7.0: - version "6.10.3" - resolved "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz" - integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== - dependencies: - side-channel "^1.0.4" - -qs@6.9.7: - version "6.9.7" - resolved "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz" - integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw== - -qs@~6.5.2: - version "6.5.3" - resolved "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz" - integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== - -query-string@^5.0.1: - version "5.1.1" - resolved "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz" - integrity sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw== - dependencies: - decode-uri-component "^0.2.0" - object-assign "^4.1.0" - strict-uri-encode "^1.0.0" - -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.0.6, randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -randomfill@^1.0.3: - version "1.0.4" - resolved "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz" - integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - -range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@2.4.3: - version "2.4.3" - resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz" - integrity sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g== - dependencies: - bytes "3.1.2" - http-errors "1.8.1" - iconv-lite "0.4.24" - unpipe "1.0.0" - -raw-body@2.5.1, raw-body@^2.4.1: - version "2.5.1" - resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz" - integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== - dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.4.24" - unpipe "1.0.0" - -read-pkg-up@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz" - integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= - dependencies: - find-up "^1.0.0" - read-pkg "^1.0.0" - -read-pkg@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz" - integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= - dependencies: - load-json-file "^1.0.0" - normalize-package-data "^2.3.2" - path-type "^1.0.0" - -readable-stream@^1.0.33: - version "1.1.14" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz" - integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - -readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.2.2, readable-stream@^2.2.8, readable-stream@^2.2.9, readable-stream@^2.3.6, readable-stream@~2.3.6: - version "2.3.7" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.0.6, readable-stream@^3.1.0, readable-stream@^3.4.0, readable-stream@^3.6.0: - version "3.6.0" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== +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" -readable-stream@~1.0.15: - version "1.0.34" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz" - integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw= - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - -readdirp@~3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz" - integrity sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ== - dependencies: - picomatch "^2.0.4" - readdirp@~3.6.0: version "3.6.0" - resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== dependencies: picomatch "^2.2.1" reduce-flatten@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== -regenerate@^1.2.1: - version "1.4.2" - resolved "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz" - integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== - -regenerator-runtime@^0.11.0: - version "0.11.1" - resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz" - integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== - -regenerator-transform@^0.10.0: - version "0.10.1" - resolved "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz" - integrity sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q== - dependencies: - babel-runtime "^6.18.0" - babel-types "^6.19.0" - private "^0.1.6" - -regex-not@^1.0.0, regex-not@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz" - integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== - dependencies: - extend-shallow "^3.0.2" - safe-regex "^1.1.0" - -regexp.prototype.flags@^1.2.0: - version "1.4.1" - resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.1.tgz" - integrity sha512-pMR7hBVUUGI7PMA37m2ofIdQCsomVnas+Jn5UPGAHQ+/LlwKm/aTLJHdasmHRzlfeZwHiAOaRSo2rbBDm3nNUQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -regexpp@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz" - integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== - -regexpp@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz" - integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== - -regexpu-core@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz" - integrity sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA= - dependencies: - regenerate "^1.2.1" - regjsgen "^0.2.0" - regjsparser "^0.1.4" - -regjsgen@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz" - integrity sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc= - -regjsparser@^0.1.4: - version "0.1.5" - resolved "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz" - integrity sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw= - dependencies: - jsesc "~0.5.0" - -repeat-element@^1.1.2: - version "1.1.4" - resolved "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz" - integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ== - -repeat-string@^1.6.1: - version "1.6.1" - resolved "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz" - integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= - -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz" - integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= - dependencies: - is-finite "^1.0.0" - -req-cwd@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/req-cwd/-/req-cwd-2.0.0.tgz" - integrity sha1-1AgrTURZgDZkD7c93qAe1T20nrw= - dependencies: - req-from "^2.0.0" - -req-from@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/req-from/-/req-from-2.0.0.tgz" - integrity sha1-10GI5H+TeW9Kpx327jWuaJ8+DnA= - dependencies: - resolve-from "^3.0.0" - -request-promise-core@1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz" - integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw== - dependencies: - lodash "^4.17.19" - -request-promise-native@^1.0.5: - version "1.0.9" - resolved "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz" - integrity sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g== - dependencies: - request-promise-core "1.1.4" - stealthy-require "^1.1.1" - tough-cookie "^2.3.3" - -request@^2.79.0, request@^2.85.0, request@^2.88.0: - version "2.88.2" - resolved "https://registry.npmjs.org/request/-/request-2.88.2.tgz" - integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - require-directory@^2.1.1: version "2.1.1" - resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -require-from-string@^1.1.0: - version "1.2.1" - resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz" - integrity sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg= + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== -require-from-string@^2.0.0: +require-from-string@^2.0.2: version "2.0.2" - resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" + 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== -require-main-filename@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz" - integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= - -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - -resolve-from@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz" - integrity sha1-six699nWiBvItuZTM17rywoYh0g= - resolve-from@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== resolve-from@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve-url@^0.2.1: - version "0.2.1" - resolved "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz" - integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= - -resolve@1.17.0: - version "1.17.0" - resolved "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz" - integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== - dependencies: - path-parse "^1.0.6" - -resolve@^1.10.0, resolve@^1.8.1, resolve@~1.22.0: - version "1.22.0" - resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz" - integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== - dependencies: - is-core-module "^2.8.1" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -responselike@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz" - integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= - dependencies: - lowercase-keys "^1.0.0" - -restore-cursor@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz" - integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= - dependencies: - onetime "^2.0.0" - signal-exit "^3.0.2" - -resumer@~0.0.0: - version "0.0.0" - resolved "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz" - integrity sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k= - dependencies: - through "~2.3.4" - -ret@~0.1.10: - version "0.1.15" - resolved "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz" - integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== - reusify@^1.0.4: version "1.0.4" - resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@2.6.3: - version "2.6.3" - resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== - dependencies: - glob "^7.1.3" - -rimraf@^2.2.8, rimraf@^2.6.2, rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - rimraf@^3.0.2: version "3.0.2" - resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== dependencies: glob "^7.1.3" -ripemd160@^2.0.0, ripemd160@^2.0.1: - version "2.0.2" - resolved "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz" - integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - -rlp@^2.0.0, rlp@^2.2.1, rlp@^2.2.2, rlp@^2.2.3, rlp@^2.2.4: - version "2.2.7" - resolved "https://registry.npmjs.org/rlp/-/rlp-2.2.7.tgz" - integrity sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ== - dependencies: - bn.js "^5.2.0" - -rollup@^2.60.0: - version "2.70.1" - resolved "https://registry.npmjs.org/rollup/-/rollup-2.70.1.tgz" - integrity sha512-CRYsI5EuzLbXdxC6RnYhOuRdtz4bhejPMSWjsFLfVM/7w/85n2szZv6yExqUXsBdz5KT8eoubeyDUDjhLHEslA== +rollup@^2.74.1: + version "2.79.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7" + integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== optionalDependencies: fsevents "~2.3.2" -run-async@^2.2.0: - version "2.4.1" - resolved "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz" - integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== - run-parallel@^1.1.9: version "1.2.0" - resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== dependencies: queue-microtask "^1.2.2" -rustbn.js@~0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/rustbn.js/-/rustbn.js-0.2.0.tgz" - integrity sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA== - -rxjs@^6.4.0: - version "6.6.7" - resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz" - integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== - dependencies: - tslib "^1.9.0" - -safe-buffer@5.2.1, 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.1, safe-buffer@~5.2.0: +safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-event-emitter@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/safe-event-emitter/-/safe-event-emitter-1.0.1.tgz" - integrity sha512-e1wFe99A91XYYxoQbcq2ZJUWurxEyP8vfz7A7vuUe1s95q8r5ebraVaA1BukYJcpM6V16ugWoD9vngi8Ccu5fg== - dependencies: - events "^3.0.0" - -safe-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz" - integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= - dependencies: - ret "~0.1.10" - -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: - version "2.1.2" - resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -scrypt-js@2.0.4: - version "2.0.4" - resolved "https://registry.npmjs.org/scrypt-js/-/scrypt-js-2.0.4.tgz" - integrity sha512-4KsaGcPnuhtCZQCxFxN3GVYIhKFPTdLd8PLC552XwbMndtD0cjRFAhDuuydXQ0h08ZfPgzqe6EKHozpuH74iDw== - -scrypt-js@3.0.1, scrypt-js@^3.0.0, scrypt-js@^3.0.1: +scrypt-js@3.0.1: version "3.0.1" - resolved "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz" + resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312" integrity sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA== -scryptsy@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/scryptsy/-/scryptsy-1.2.1.tgz" - integrity sha1-oyJfpLJST4AnAHYeKFW987LZIWM= - dependencies: - pbkdf2 "^3.0.3" - -secp256k1@^4.0.1: - version "4.0.3" - resolved "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.3.tgz" - integrity sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA== - dependencies: - elliptic "^6.5.4" - node-addon-api "^2.0.0" - node-gyp-build "^4.2.0" - -seedrandom@3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.1.tgz" - integrity sha512-1/02Y/rUeU1CJBAGLebiC5Lbo5FnB22gQbIFFYTLkwvp1xdABZJH1sn4ZT1MzXmPpzv+Rf/Lu2NcsLJiK4rcDg== - -semaphore-async-await@^1.5.1: - version "1.5.1" - resolved "https://registry.npmjs.org/semaphore-async-await/-/semaphore-async-await-1.5.1.tgz" - integrity sha1-hXvvXjZEYBykuVcLh+nfXKEpdPo= - -semaphore@>=1.0.1, semaphore@^1.0.3, semaphore@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/semaphore/-/semaphore-1.1.0.tgz" - integrity sha512-O4OZEaNtkMd/K0i6js9SL+gqy0ZCBMgUvlSqHKi4IBdjhe7wB8pwztUk1BbZ1fmrvpwFrPbHzqd2w5pTcJH6LA== - -"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0: - version "5.7.1" - resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.3.0: - version "6.3.0" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -semver@^7.3.5: - version "7.3.7" - resolved "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz" - integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== +semver@^7.3.7, semver@^7.5.2, semver@^7.5.4: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" -semver@~5.4.1: - version "5.4.1" - resolved "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz" - integrity sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg== - -send@0.17.2: - version "0.17.2" - resolved "https://registry.npmjs.org/send/-/send-0.17.2.tgz" - integrity sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww== - dependencies: - debug "2.6.9" - depd "~1.1.2" - destroy "~1.0.4" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "1.8.1" - mime "1.6.0" - ms "2.1.3" - on-finished "~2.3.0" - range-parser "~1.2.1" - statuses "~1.5.0" - serialize-javascript@6.0.0: version "6.0.0" - resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== dependencies: randombytes "^2.1.0" -serve-static@1.14.2: - version "1.14.2" - resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz" - integrity sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.17.2" - -servify@^0.1.12: - version "0.1.12" - resolved "https://registry.npmjs.org/servify/-/servify-0.1.12.tgz" - integrity sha512-/xE6GvsKKqyo1BAY+KxOWXcLpPsUUyji7Qg3bVD7hh1eRze5bR1uYiuDA/k3Gof1s9BTzQZEJK8sNcNGFIzeWw== - dependencies: - body-parser "^1.16.0" - cors "^2.8.1" - express "^4.14.0" - request "^2.79.0" - xhr "^2.3.3" - -set-blocking@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= - -set-immediate-shim@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz" - integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E= - -set-value@^2.0.0, set-value@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz" - integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.3" - split-string "^3.0.1" - -setimmediate@1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.4.tgz" - integrity sha1-IOgd5iLUoCWIzgyNqJc8vPHTE48= - -setimmediate@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz" - integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= - -setprototypeof@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== - -sha.js@^2.4.0, sha.js@^2.4.8: - version "2.4.11" - resolved "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz" - integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -sha1@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/sha1/-/sha1-1.1.1.tgz" - integrity sha1-rdqnqTFo85PxnrKxUJFhjicA+Eg= - dependencies: - charenc ">= 0.0.1" - crypt ">= 0.0.1" - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= - dependencies: - shebang-regex "^1.0.0" - shebang-command@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" + 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@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - shebang-regex@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== - dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" - -signal-exit@^3.0.2, signal-exit@^3.0.3: +signal-exit@^3.0.3: version "3.0.7" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== -simple-concat@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz" - integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== - -simple-get@^2.7.0: - version "2.8.2" - resolved "https://registry.npmjs.org/simple-get/-/simple-get-2.8.2.tgz" - integrity sha512-Ijd/rV5o+mSBBs4F/x9oDPtTx9Zb6X9brmnXvMW4J7IR15ngi9q5xxqWBKU744jTZiaXtxaPL7uHG6vtN8kUkw== - dependencies: - decompress-response "^3.3.0" - once "^1.3.1" - simple-concat "^1.0.0" - -slash@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz" - integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= - -slash@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz" - integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== slash@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -slice-ansi@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz" - integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== - dependencies: - ansi-styles "^3.2.0" - astral-regex "^1.0.0" - is-fullwidth-code-point "^2.0.0" - -snapdragon-node@^2.0.1: - version "2.1.1" - resolved "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz" - integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== +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: - define-property "^1.0.0" - isobject "^3.0.0" - snapdragon-util "^3.0.1" + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" -snapdragon-util@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz" - integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== - dependencies: - kind-of "^3.2.0" - -snapdragon@^0.8.1: - version "0.8.2" - resolved "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz" - integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== - dependencies: - base "^0.11.1" - debug "^2.2.0" - define-property "^0.2.5" - extend-shallow "^2.0.1" - map-cache "^0.2.2" - source-map "^0.5.6" - source-map-resolve "^0.5.0" - use "^3.1.0" - -solc@0.7.3: - version "0.7.3" - resolved "https://registry.npmjs.org/solc/-/solc-0.7.3.tgz" - integrity sha512-GAsWNAjGzIDg7VxzP6mPjdurby3IkGCjQcM8GFYZT6RyaoUZKmMU6Y7YwG+tFGhv7dwZ8rmR4iwFDrrD99JwqA== - dependencies: - command-exists "^1.2.8" - commander "3.0.2" - follow-redirects "^1.12.1" - fs-extra "^0.30.0" - js-sha3 "0.8.0" - memorystream "^0.3.1" - require-from-string "^2.0.0" - semver "^5.5.0" - tmp "0.0.33" - -solc@^0.4.20: - version "0.4.26" - resolved "https://registry.npmjs.org/solc/-/solc-0.4.26.tgz" - integrity sha512-o+c6FpkiHd+HPjmjEVpQgH7fqZ14tJpXhho+/bQXlXbliLIS/xjXb42Vxh+qQY1WCSTMQ0+a5vR9vi0MfhU6mA== - dependencies: - fs-extra "^0.30.0" - memorystream "^0.3.1" - require-from-string "^1.1.0" - semver "^5.3.0" - yargs "^4.7.1" - -solc@^0.6.3: - version "0.6.12" - resolved "https://registry.npmjs.org/solc/-/solc-0.6.12.tgz" - integrity sha512-Lm0Ql2G9Qc7yPP2Ba+WNmzw2jwsrd3u4PobHYlSOxaut3TtUbj9+5ZrT6f4DUpNPEoBaFUOEg9Op9C0mk7ge9g== - dependencies: - command-exists "^1.2.8" - commander "3.0.2" - fs-extra "^0.30.0" - js-sha3 "0.8.0" - memorystream "^0.3.1" - require-from-string "^2.0.0" - semver "^5.5.0" - tmp "0.0.33" +solady@0.0.180: + version "0.0.180" + resolved "https://registry.yarnpkg.com/solady/-/solady-0.0.180.tgz#d806c84a0bf8b6e3d85a8fb0978980de086ff59e" + integrity sha512-9QVCyMph+wk78Aq/GxtDAQg7dvNoVWx2dS2Zwf11XlwFKDZ+YJG2lrQsK9NEIth9NOebwjBXAYk4itdwOOE4aw== solhint-plugin-prettier@^0.0.5: version "0.0.5" - resolved "https://registry.npmjs.org/solhint-plugin-prettier/-/solhint-plugin-prettier-0.0.5.tgz" + resolved "https://registry.yarnpkg.com/solhint-plugin-prettier/-/solhint-plugin-prettier-0.0.5.tgz#e3b22800ba435cd640a9eca805a7f8bc3e3e6a6b" integrity sha512-7jmWcnVshIrO2FFinIvDQmhQpfpS2rRRn3RejiYgnjIE68xO2bvrYvjqVNfrio4xH9ghOqn83tKuTzLjEbmGIA== dependencies: prettier-linter-helpers "^1.0.0" -solhint@^3.3.7: - version "3.3.7" - resolved "https://registry.npmjs.org/solhint/-/solhint-3.3.7.tgz" - integrity sha512-NjjjVmXI3ehKkb3aNtRJWw55SUVJ8HMKKodwe0HnejA+k0d2kmhw7jvpa+MCTbcEgt8IWSwx0Hu6aCo/iYOZzQ== - dependencies: - "@solidity-parser/parser" "^0.14.1" - ajv "^6.6.1" - antlr4 "4.7.1" - ast-parents "0.0.1" - chalk "^2.4.2" - commander "2.18.0" - cosmiconfig "^5.0.7" - eslint "^5.6.0" - fast-diff "^1.1.2" - glob "^7.1.3" - ignore "^4.0.6" - js-yaml "^3.12.0" - lodash "^4.17.11" - semver "^6.3.0" +solhint@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/solhint/-/solhint-3.6.2.tgz#2b2acbec8fdc37b2c68206a71ba89c7f519943fe" + integrity sha512-85EeLbmkcPwD+3JR7aEMKsVC9YrRSxd4qkXuMzrlf7+z2Eqdfm1wHWq1ffTuo5aDhoZxp2I9yF3QkxZOxOL7aQ== + 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 "^7.5.2" + strip-ansi "^6.0.1" + table "^6.8.1" + text-table "^0.2.0" optionalDependencies: - prettier "^1.14.3" - -solidity-comments-extractor@^0.0.7: - version "0.0.7" - resolved "https://registry.npmjs.org/solidity-comments-extractor/-/solidity-comments-extractor-0.0.7.tgz" - integrity sha512-wciNMLg/Irp8OKGrh3S2tfvZiZ0NEyILfcRCXCD4mp7SgK/i9gzLfhY2hY7VMCQJ3kH9UB9BzNdibIVMchzyYw== - -source-map-resolve@^0.5.0: - version "0.5.3" - resolved "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz" - integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== - dependencies: - atob "^2.1.2" - decode-uri-component "^0.2.0" - resolve-url "^0.2.1" - source-map-url "^0.4.0" - urix "^0.1.0" - -source-map-support@0.5.12: - version "0.5.12" - resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz" - integrity sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-support@^0.4.15: - version "0.4.18" - resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz" - integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA== - dependencies: - source-map "^0.5.6" - -source-map-support@^0.5.13: - version "0.5.21" - resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-url@^0.4.0: - version "0.4.1" - resolved "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz" - integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== - -source-map@^0.5.6, source-map@^0.5.7: - version "0.5.7" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -source-map@^0.6.0: - version "0.6.1" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -source-map@^0.7.3: - version "0.7.3" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz" - integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== - -spdx-correct@^3.0.0: - version "3.1.1" - resolved "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz" - integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.3.0" - resolved "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz" - integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== - -spdx-expression-parse@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz" - integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.11" - resolved "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz" - integrity sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g== - -split-string@^3.0.1, split-string@^3.0.2: - version "3.1.0" - resolved "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz" - integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== - dependencies: - extend-shallow "^3.0.0" + prettier "^2.8.3" -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - -squirrelly@^8.0.8: - version "8.0.8" - resolved "https://registry.npmjs.org/squirrelly/-/squirrelly-8.0.8.tgz" - integrity sha512-7dyZJ9Gw86MmH0dYLiESsjGOTj6KG8IWToTaqBuB6LwPI+hyNb6mbQaZwrfnAQ4cMDnSWMUvX/zAYDLTSWLk/w== - -sshpk@^1.7.0: - version "1.17.0" - resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz" - integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - bcrypt-pbkdf "^1.0.0" - dashdash "^1.12.0" - ecc-jsbn "~0.1.1" - getpass "^0.1.1" - jsbn "~0.1.0" - safer-buffer "^2.0.2" - tweetnacl "~0.14.0" - -stacktrace-parser@^0.1.10: - version "0.1.10" - resolved "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz" - integrity sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg== - dependencies: - type-fest "^0.7.1" - -static-extend@^0.1.1: - version "0.1.2" - resolved "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz" - integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= - dependencies: - define-property "^0.2.5" - object-copy "^0.1.0" - -statuses@2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" - integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== - -"statuses@>= 1.5.0 < 2", statuses@~1.5.0: - version "1.5.0" - resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" - integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= - -stealthy-require@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz" - integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= +solidity-comments-extractor@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/solidity-comments-extractor/-/solidity-comments-extractor-0.0.8.tgz#f6e148ab0c49f30c1abcbecb8b8df01ed8e879f8" + integrity sha512-htM7Vn6LhHreR+EglVMd2s+sZhcXAirB1Zlyrv5zBuTxieCvjfnRpd7iZk75m/u6NOlEyQ94C6TWbBn2cY7w8g== -stream-to-pull-stream@^1.7.1: - version "1.7.3" - resolved "https://registry.npmjs.org/stream-to-pull-stream/-/stream-to-pull-stream-1.7.3.tgz" - integrity sha512-6sNyqJpr5dIOQdgNy/xcDWwDuzAsAwVzhzrWlAPAQ7Lkjx/rv0wgvxEyKwTq6FmNd5rjTrELt/CLmaSw7crMGg== +source-map@0.8.0-beta.0: + version "0.8.0-beta.0" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.8.0-beta.0.tgz#d4c1bb42c3f7ee925f005927ba10709e0d1d1f11" + integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA== dependencies: - looper "^3.0.0" - pull-stream "^3.2.3" - -strict-uri-encode@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz" - integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= + whatwg-url "^7.0.0" string-format@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/string-format/-/string-format-2.0.0.tgz#f2df2e7097440d3b65de31b6d40d54c96eaffb9b" integrity sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA== -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -"string-width@^1.0.2 || 2", string-width@^2.1.0, string-width@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string-width@^3.0.0, string-width@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + 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.prototype.trim@~1.2.5: - version "1.2.5" - resolved "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.5.tgz" - integrity sha512-Lnh17webJVsD6ECeovpVN17RlAKjmz4rF9S+8Y45CkMc/ufVpTkU3vZIyIC7sllQ1FCvObZnnCdNs/HXTUOTlg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - -string.prototype.trimend@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz" - integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -string.prototype.trimstart@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz" - integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" + 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.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + 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" -string_decoder@~0.10.x: - version "0.10.31" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" - integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - -strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: - version "5.2.0" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.0" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + 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-bom@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz" - integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== dependencies: - is-utf8 "^0.2.0" + ansi-regex "^6.0.1" strip-final-newline@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -strip-hex-prefix@1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz" - integrity sha1-DF8VX+8RUTczd96du1iNoFUA428= - dependencies: - is-hex-prefixed "1.0.0" - -strip-json-comments@2.0.1, strip-json-comments@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - -strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-json-comments@3.1.1, strip-json-comments@^3.1.1: version "3.1.1" - resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== sucrase@^3.20.3: - version "3.21.0" - resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.21.0.tgz" - integrity sha512-FjAhMJjDcifARI7bZej0Bi1yekjWQHoEvWIXhLPwDhC6O4iZ5PtGb86WV56riW87hzpgB13wwBKO9vKAiWu5VQ== + version "3.35.0" + resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.0.tgz#57f17a3d7e19b36d8995f06679d121be914ae263" + integrity sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA== dependencies: + "@jridgewell/gen-mapping" "^0.3.2" commander "^4.0.0" - glob "7.1.6" + glob "^10.3.10" lines-and-columns "^1.1.6" mz "^2.7.0" pirates "^4.0.1" ts-interface-checker "^0.1.9" -supports-color@6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz" - integrity sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg== - dependencies: - has-flag "^3.0.0" - supports-color@8.1.1: version "8.1.1" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== dependencies: has-flag "^4.0.0" -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - supports-color@^5.3.0: version "5.5.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" + 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.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + 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" -supports-preserve-symlinks-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== - -swarm-js@^0.1.40: - version "0.1.40" - resolved "https://registry.npmjs.org/swarm-js/-/swarm-js-0.1.40.tgz" - integrity sha512-yqiOCEoA4/IShXkY3WKwP5PvZhmoOOD8clsKA7EEcRILMkTEYHCQ21HDCAcVpmIxZq4LyZvWeRJ6quIyHk1caA== - dependencies: - bluebird "^3.5.0" - buffer "^5.0.5" - eth-lib "^0.1.26" - fs-extra "^4.0.2" - got "^7.1.0" - mime-types "^2.1.16" - mkdirp-promise "^5.0.1" - mock-fs "^4.1.0" - setimmediate "^1.0.5" - tar "^4.0.2" - xhr-request "^1.0.1" - -sync-request@^6.0.0: - version "6.1.0" - resolved "https://registry.npmjs.org/sync-request/-/sync-request-6.1.0.tgz" - integrity sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw== - dependencies: - http-response-object "^3.0.1" - sync-rpc "^1.2.1" - then-request "^6.0.0" - -sync-rpc@^1.2.1: - version "1.3.6" - resolved "https://registry.npmjs.org/sync-rpc/-/sync-rpc-1.3.6.tgz" - integrity sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw== - dependencies: - get-port "^3.1.0" - -table-layout@^1.0.1: +table-layout@^1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-1.0.2.tgz#c4038a1853b0136d63365a734b6931cf4fad4a04" integrity sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A== dependencies: array-back "^4.0.1" @@ -8867,262 +2604,86 @@ table-layout@^1.0.1: typical "^5.2.0" wordwrapjs "^4.0.0" -table@^5.2.3: - version "5.4.6" - resolved "https://registry.npmjs.org/table/-/table-5.4.6.tgz" - integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== - dependencies: - ajv "^6.10.2" - lodash "^4.17.14" - slice-ansi "^2.1.0" - string-width "^3.0.0" - -tape@^4.6.3: - version "4.15.1" - resolved "https://registry.npmjs.org/tape/-/tape-4.15.1.tgz" - integrity sha512-k7F5pyr91n9D/yjSJwbLLYDCrTWXxMSXbbmHX2n334lSIc2rxeXyFkaBv4UuUd2gBYMrAOalPutAiCxC6q1qbw== - dependencies: - call-bind "~1.0.2" - deep-equal "~1.1.1" - defined "~1.0.0" - dotignore "~0.1.2" - for-each "~0.3.3" - glob "~7.2.0" - has "~1.0.3" - inherits "~2.0.4" - is-regex "~1.1.4" - minimist "~1.2.6" - object-inspect "~1.12.0" - resolve "~1.22.0" - resumer "~0.0.0" - string.prototype.trim "~1.2.5" - through "~2.3.8" - -tar@^4.0.2: - version "4.4.19" - resolved "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz" - integrity sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA== - dependencies: - chownr "^1.1.4" - fs-minipass "^1.2.7" - minipass "^2.9.0" - minizlib "^1.3.3" - mkdirp "^0.5.5" - safe-buffer "^5.2.1" - yallist "^3.1.1" - -test-value@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/test-value/-/test-value-2.1.0.tgz" - integrity sha1-Edpv9nDzRxpztiXKTz/c97t0gpE= +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: - array-back "^1.0.3" - typical "^2.6.0" - -testrpc@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/testrpc/-/testrpc-0.0.1.tgz" - integrity sha512-afH1hO+SQ/VPlmaLUFj2636QMeDvPCeQMc/9RBMW0IfjNe9gFD9Ra3ShqYkB7py0do1ZcCna/9acHyzTJ+GcNA== + 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.npmjs.org/text-table/-/text-table-0.2.0.tgz" - integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= - -then-request@^6.0.0: - version "6.0.2" - resolved "https://registry.npmjs.org/then-request/-/then-request-6.0.2.tgz" - integrity sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA== - dependencies: - "@types/concat-stream" "^1.6.0" - "@types/form-data" "0.0.33" - "@types/node" "^8.0.0" - "@types/qs" "^6.2.31" - caseless "~0.12.0" - concat-stream "^1.6.0" - form-data "^2.2.0" - http-basic "^8.1.1" - http-response-object "^3.0.1" - promise "^8.0.0" - qs "^6.4.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== thenify-all@^1.0.0: version "1.6.0" - resolved "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz" - integrity sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY= + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== dependencies: thenify ">= 3.1.0 < 4" "thenify@>= 3.1.0 < 4": version "3.3.1" - resolved "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== dependencies: any-promise "^1.0.0" -through2@^2.0.3: - version "2.0.5" - resolved "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz" - integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== - dependencies: - readable-stream "~2.3.6" - xtend "~4.0.1" - -through@^2.3.6, through@~2.3.4, through@~2.3.8: - version "2.3.8" - resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= - -timed-out@^4.0.0, timed-out@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz" - integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8= - -tmp@0.0.33, tmp@^0.0.33: - version "0.0.33" - resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== - dependencies: - os-tmpdir "~1.0.2" - -tmp@0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz" - integrity sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw== - dependencies: - rimraf "^2.6.3" - -to-fast-properties@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz" - integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc= - -to-object-path@^0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz" - integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= - dependencies: - kind-of "^3.0.2" - -to-readable-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz" - integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== - -to-regex-range@^2.1.0: - version "2.1.1" - resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz" - integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= - dependencies: - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range@^5.0.1: version "5.0.1" - resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" + 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" -to-regex@^3.0.1, to-regex@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz" - integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== - dependencies: - define-property "^2.0.2" - extend-shallow "^3.0.2" - regex-not "^1.0.2" - safe-regex "^1.1.0" - -toidentifier@1.0.1: +tr46@^1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" - integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== - -tough-cookie@^2.3.3, tough-cookie@~2.5.0: - version "2.5.0" - resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + integrity sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA== dependencies: - psl "^1.1.28" - punycode "^2.1.1" - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" - integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + punycode "^2.1.0" tree-kill@^1.2.2: version "1.2.2" - resolved "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== treeify@^1.1.0: version "1.1.0" - resolved "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/treeify/-/treeify-1.1.0.tgz#4e31c6a463accd0943879f30667c4fdaff411bb8" integrity sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A== -trim-right@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz" - integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= - -"true-case-path@^2.2.1": - version "2.2.1" - resolved "https://registry.npmjs.org/true-case-path/-/true-case-path-2.2.1.tgz" - integrity sha512-0z3j8R7MCjy10kc/g+qg7Ln3alJTodw9aDuVWZa3uiWqfuBMKeAeP2ocWcxoyM3D73yz3Jt/Pu4qPr4wHSdB/Q== - ts-command-line-args@^2.2.0: - version "2.2.1" - resolved "https://registry.npmjs.org/ts-command-line-args/-/ts-command-line-args-2.2.1.tgz" - integrity sha512-mnK68QA86FYzQYTSA/rxIjT/8EpKsvQw9QkawPic8I8t0gjAOw3Oa509NIRoaY1FmH7hdrncMp7t7o+vYoceNQ== + version "2.5.1" + resolved "https://registry.yarnpkg.com/ts-command-line-args/-/ts-command-line-args-2.5.1.tgz#e64456b580d1d4f6d948824c274cf6fa5f45f7f0" + integrity sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw== dependencies: chalk "^4.1.0" command-line-args "^5.1.1" command-line-usage "^6.1.0" string-format "^2.0.0" -ts-essentials@^1.0.0: - version "1.0.4" - resolved "https://registry.npmjs.org/ts-essentials/-/ts-essentials-1.0.4.tgz" - integrity sha512-q3N1xS4vZpRouhYHDPwO0bDW3EZ6SK9CrrDHxi/D6BPReSjpVgWIOpLS2o0gSBZm+7q/wyKp6RVM1AeeW7uyfQ== - -ts-essentials@^6.0.3: - version "6.0.7" - resolved "https://registry.npmjs.org/ts-essentials/-/ts-essentials-6.0.7.tgz" - integrity sha512-2E4HIIj4tQJlIHuATRHayv0EfMGK3ris/GRk1E3CFnsZzeNV+hUmelbaTZHLtXaZppM5oLhHRtO04gINC4Jusw== - ts-essentials@^7.0.1: version "7.0.3" - resolved "https://registry.npmjs.org/ts-essentials/-/ts-essentials-7.0.3.tgz" + resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-7.0.3.tgz#686fd155a02133eedcc5362dc8b5056cde3e5a38" integrity sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ== -ts-generator@^0.1.1: - version "0.1.1" - resolved "https://registry.npmjs.org/ts-generator/-/ts-generator-0.1.1.tgz" - integrity sha512-N+ahhZxTLYu1HNTQetwWcx3so8hcYbkKBHTr4b4/YgObFTIKkOSSsaa+nal12w8mfrJAyzJfETXawbNjSfP2gQ== - dependencies: - "@types/mkdirp" "^0.5.2" - "@types/prettier" "^2.1.1" - "@types/resolve" "^0.0.8" - chalk "^2.4.1" - glob "^7.1.2" - mkdirp "^0.5.1" - prettier "^2.1.2" - resolve "^1.8.1" - ts-essentials "^1.0.0" - ts-interface-checker@^0.1.9: version "0.1.13" - resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz" + resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== -ts-node@^10.6.0: - version "10.7.0" - resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz" - integrity sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A== +ts-node@^10.9.1: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== dependencies: - "@cspotcode/source-map-support" "0.7.0" + "@cspotcode/source-map-support" "^0.8.0" "@tsconfig/node10" "^1.0.7" "@tsconfig/node12" "^1.0.7" "@tsconfig/node14" "^1.0.0" @@ -9133,28 +2694,23 @@ ts-node@^10.6.0: create-require "^1.1.0" diff "^4.0.1" make-error "^1.1.1" - v8-compile-cache-lib "^3.0.0" + v8-compile-cache-lib "^3.0.1" yn "3.1.1" -tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: +tslib@^1.8.1: version "1.14.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.3.1: - version "2.3.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz" - integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== - -tsort@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/tsort/-/tsort-0.0.1.tgz" - integrity sha1-4igPXoF/i/QnVlf9D5rr1E9aJ4Y= +tslib@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== -tsup@^5.11.11: - version "5.12.5" - resolved "https://registry.npmjs.org/tsup/-/tsup-5.12.5.tgz" - integrity sha512-lKwzJsB49sDto51QjqOB4SdiBLKRvgTymEBuBCovcksdDwFEz3esrkbf3m497PXntUKVTzcgOfPdTgknMtvufw== +tsup@^5.12.9: + version "5.12.9" + resolved "https://registry.yarnpkg.com/tsup/-/tsup-5.12.9.tgz#8cdd9b4bc6493317cb92edf5f3476920dddcdb18" + integrity sha512-dUpuouWZYe40lLufo64qEhDpIDsWhRbr2expv5dHEMjwqeKJS2aXA/FPqs1dxO4T6mBojo7rvo3jP9NNzaKyDg== dependencies: bundle-require "^3.0.2" cac "^6.7.12" @@ -9166,104 +2722,34 @@ tsup@^5.11.11: joycon "^3.0.1" postcss-load-config "^3.0.1" resolve-from "^5.0.0" - rollup "^2.60.0" - source-map "^0.7.3" + rollup "^2.74.1" + source-map "0.8.0-beta.0" sucrase "^3.20.3" tree-kill "^1.2.2" tsutils@^3.21.0: version "3.21.0" - resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== dependencies: tslib "^1.8.1" -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz" - integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= - dependencies: - safe-buffer "^5.0.1" - -tweetnacl-util@^0.15.0, tweetnacl-util@^0.15.1: - version "0.15.1" - resolved "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz" - integrity sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw== - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" - integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= - -tweetnacl@^1.0.0, tweetnacl@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz" - integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== - type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" - resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== dependencies: prelude-ls "^1.2.1" -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz" - integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= - dependencies: - prelude-ls "~1.1.2" - type-fest@^0.20.2: version "0.20.2" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -type-fest@^0.21.3: - version "0.21.3" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz" - integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== - -type-fest@^0.7.1: - version "0.7.1" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz" - integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg== - -type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - -type@^1.0.1: - version "1.2.0" - resolved "https://registry.npmjs.org/type/-/type-1.2.0.tgz" - integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== - -type@^2.5.0: - version "2.6.0" - resolved "https://registry.npmjs.org/type/-/type-2.6.0.tgz" - integrity sha512-eiDBDOmkih5pMbo9OqsqPRGMljLodLcwd5XD5JbtNB0o89xZAwynY9EdCDsJU7LtcVCClu9DvM7/0Ep1hYX3EQ== - -typechain@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/typechain/-/typechain-3.0.0.tgz" - integrity sha512-ft4KVmiN3zH4JUFu2WJBrwfHeDf772Tt2d8bssDTo/YcckKW2D+OwFrHXRC6hJvO3mHjFQTihoMV6fJOi0Hngg== - dependencies: - command-line-args "^4.0.7" - debug "^4.1.1" - fs-extra "^7.0.0" - js-sha3 "^0.8.0" - lodash "^4.17.15" - ts-essentials "^6.0.3" - ts-generator "^0.1.1" - -typechain@^8.0.0: - version "8.0.0" - resolved "https://registry.npmjs.org/typechain/-/typechain-8.0.0.tgz" - integrity sha512-rqDfDYc9voVAhmfVfAwzg3VYFvhvs5ck1X9T/iWkX745Cul4t+V/smjnyqrbDzWDbzD93xfld1epg7Y/uFAesQ== +typechain@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/typechain/-/typechain-8.3.2.tgz#1090dd8d9c57b6ef2aed3640a516bdbf01b00d73" + integrity sha512-x/sQYr5w9K7yv3es7jo4KTX05CLxOf7TRWwoHlrjRh8H82G64g+k7VuWPJlgMo6qrjfCulOdfBjiaDtmhFYD/Q== dependencies: "@types/prettier" "^2.1.1" debug "^4.3.1" @@ -9276,632 +2762,82 @@ typechain@^8.0.0: ts-command-line-args "^2.2.0" ts-essentials "^7.0.1" -typedarray-to-buffer@^3.1.5: - version "3.1.5" - resolved "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz" - integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== - dependencies: - is-typedarray "^1.0.0" - -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" - integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= - -typescript@^4.4.4: - version "4.6.3" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz" - integrity sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw== - -typewise-core@^1.2, typewise-core@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/typewise-core/-/typewise-core-1.2.0.tgz" - integrity sha1-l+uRgFx/VdL5QXSPpQ0xXZke8ZU= - -typewise@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/typewise/-/typewise-1.0.3.tgz" - integrity sha1-EGeTZUCvl5N8xdz5kiSG6fooRlE= - dependencies: - typewise-core "^1.2.0" - -typewiselite@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/typewiselite/-/typewiselite-1.0.0.tgz" - integrity sha1-yIgvobsQksBgBal/NO9chQjjZk4= +typescript@^4.9.5: + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== -typical@^2.6.0, typical@^2.6.1: - version "2.6.1" - resolved "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz" - integrity sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0= +typescript@^5.3.2: + version "5.3.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" + integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== typical@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw== typical@^5.2.0: version "5.2.0" - resolved "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz" + resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066" integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg== -ultron@~1.1.0: - version "1.1.1" - resolved "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz" - integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og== - -unbox-primitive@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz" - integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== - dependencies: - function-bind "^1.1.1" - has-bigints "^1.0.1" - has-symbols "^1.0.2" - which-boxed-primitive "^1.0.2" - -underscore@1.9.1: - version "1.9.1" - resolved "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz" - integrity sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg== - -undici@^4.14.1: - version "4.16.0" - resolved "https://registry.npmjs.org/undici/-/undici-4.16.0.tgz" - integrity sha512-tkZSECUYi+/T1i4u+4+lwZmQgLXd4BLGlrc7KZPcLIW7Jpq99+Xpc30ONv7nS6F5UNOxp/HBZSSL9MafUrvJbw== - -union-value@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz" - integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== - dependencies: - arr-union "^3.1.0" - get-value "^2.0.6" - is-extendable "^0.1.1" - set-value "^2.0.1" +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== universalify@^0.1.0: version "0.1.2" - resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== - -unorm@^1.3.3: - version "1.6.0" - resolved "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz" - integrity sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA== - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" - integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= - -unset-value@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz" - integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= - dependencies: - has-value "^0.3.1" - isobject "^3.0.0" + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== uri-js@^4.2.2: version "4.4.1" - resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== dependencies: punycode "^2.1.0" -urix@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz" - integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= - -url-parse-lax@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz" - integrity sha1-evjzA2Rem9eaJy56FKxovAYJ2nM= - dependencies: - prepend-http "^1.0.1" - -url-parse-lax@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz" - integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= - dependencies: - prepend-http "^2.0.0" - -url-set-query@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/url-set-query/-/url-set-query-1.0.0.tgz" - integrity sha1-AW6M/Xwg7gXK/neV6JK9BwL6ozk= - -url-to-options@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz" - integrity sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k= - -url@^0.11.0: - version "0.11.0" - resolved "https://registry.npmjs.org/url/-/url-0.11.0.tgz" - integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= - dependencies: - punycode "1.3.2" - querystring "0.2.0" - -use@^3.1.0: - version "3.1.1" - resolved "https://registry.npmjs.org/use/-/use-3.1.1.tgz" - integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== - -utf-8-validate@^5.0.2: - version "5.0.9" - resolved "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.9.tgz" - integrity sha512-Yek7dAy0v3Kl0orwMlvi7TPtiCNrdfHNd7Gcc/pLq4BLXqfAmd0J7OWMizUQnTTJsyjKn02mU7anqwfmUP4J8Q== - dependencies: - node-gyp-build "^4.3.0" - -utf8@3.0.0, utf8@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz" - integrity sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ== - -util-deprecate@^1.0.1, util-deprecate@~1.0.1: +util-deprecate@^1.0.1: version "1.0.2" - resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -util.promisify@^1.0.0: - version "1.1.1" - resolved "https://registry.npmjs.org/util.promisify/-/util.promisify-1.1.1.tgz" - integrity sha512-/s3UsZUrIfa6xDhr7zZhnE9SLQ5RIXyYfiVnMMyMDzOc8WhWN4Nbh36H842OyurKbCDAesZOJaVyvmSl6fhGQw== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - for-each "^0.3.3" - has-symbols "^1.0.1" - object.getownpropertydescriptors "^2.1.1" - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" - integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= - -uuid@2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/uuid/-/uuid-2.0.1.tgz" - integrity sha1-wqMN7bPlNdcsz4LjQ5QaULqFM6w= - -uuid@3.3.2: - version "3.3.2" - resolved "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz" - integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== - -uuid@^3.3.2: - version "3.4.0" - resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== - -uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - -v8-compile-cache-lib@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz" - integrity sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA== - -v8-compile-cache@^2.0.3: - version "2.3.0" - resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz" - integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== - -validate-npm-package-license@^3.0.1: - version "3.0.4" - resolved "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - -varint@^5.0.0: - version "5.0.2" - resolved "https://registry.npmjs.org/varint/-/varint-5.0.2.tgz" - integrity sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow== + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -vary@^1, vary@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" - integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= - -verror@1.10.0: - version "1.10.0" - resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz" - integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - -web3-bzz@1.2.11: - version "1.2.11" - resolved "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.2.11.tgz" - integrity sha512-XGpWUEElGypBjeFyUhTkiPXFbDVD6Nr/S5jznE3t8cWUA0FxRf1n3n/NuIZeb0H9RkN2Ctd/jNma/k8XGa3YKg== - dependencies: - "@types/node" "^12.12.6" - got "9.6.0" - swarm-js "^0.1.40" - underscore "1.9.1" - -web3-core-helpers@1.2.11: - version "1.2.11" - resolved "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.2.11.tgz" - integrity sha512-PEPoAoZd5ME7UfbnCZBdzIerpe74GEvlwT4AjOmHeCVZoIFk7EqvOZDejJHt+feJA6kMVTdd0xzRNN295UhC1A== - dependencies: - underscore "1.9.1" - web3-eth-iban "1.2.11" - web3-utils "1.2.11" - -web3-core-method@1.2.11: - version "1.2.11" - resolved "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.2.11.tgz" - integrity sha512-ff0q76Cde94HAxLDZ6DbdmKniYCQVtvuaYh+rtOUMB6kssa5FX0q3vPmixi7NPooFnbKmmZCM6NvXg4IreTPIw== - dependencies: - "@ethersproject/transactions" "^5.0.0-beta.135" - underscore "1.9.1" - web3-core-helpers "1.2.11" - web3-core-promievent "1.2.11" - web3-core-subscriptions "1.2.11" - web3-utils "1.2.11" - -web3-core-promievent@1.2.11: - version "1.2.11" - resolved "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.2.11.tgz" - integrity sha512-il4McoDa/Ox9Agh4kyfQ8Ak/9ABYpnF8poBLL33R/EnxLsJOGQG2nZhkJa3I067hocrPSjEdlPt/0bHXsln4qA== - dependencies: - eventemitter3 "4.0.4" - -web3-core-requestmanager@1.2.11: - version "1.2.11" - resolved "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.2.11.tgz" - integrity sha512-oFhBtLfOiIbmfl6T6gYjjj9igOvtyxJ+fjS+byRxiwFJyJ5BQOz4/9/17gWR1Cq74paTlI7vDGxYfuvfE/mKvA== - dependencies: - underscore "1.9.1" - web3-core-helpers "1.2.11" - web3-providers-http "1.2.11" - web3-providers-ipc "1.2.11" - web3-providers-ws "1.2.11" - -web3-core-subscriptions@1.2.11: - version "1.2.11" - resolved "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.2.11.tgz" - integrity sha512-qEF/OVqkCvQ7MPs1JylIZCZkin0aKK9lDxpAtQ1F8niEDGFqn7DT8E/vzbIa0GsOjL2fZjDhWJsaW+BSoAW1gg== - dependencies: - eventemitter3 "4.0.4" - underscore "1.9.1" - web3-core-helpers "1.2.11" - -web3-core@1.2.11: - version "1.2.11" - resolved "https://registry.npmjs.org/web3-core/-/web3-core-1.2.11.tgz" - integrity sha512-CN7MEYOY5ryo5iVleIWRE3a3cZqVaLlIbIzDPsvQRUfzYnvzZQRZBm9Mq+ttDi2STOOzc1MKylspz/o3yq/LjQ== - dependencies: - "@types/bn.js" "^4.11.5" - "@types/node" "^12.12.6" - bignumber.js "^9.0.0" - web3-core-helpers "1.2.11" - web3-core-method "1.2.11" - web3-core-requestmanager "1.2.11" - web3-utils "1.2.11" - -web3-eth-abi@1.2.11: - version "1.2.11" - resolved "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.2.11.tgz" - integrity sha512-PkRYc0+MjuLSgg03QVWqWlQivJqRwKItKtEpRUaxUAeLE7i/uU39gmzm2keHGcQXo3POXAbOnMqkDvOep89Crg== - dependencies: - "@ethersproject/abi" "5.0.0-beta.153" - underscore "1.9.1" - web3-utils "1.2.11" - -web3-eth-accounts@1.2.11: - version "1.2.11" - resolved "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.2.11.tgz" - integrity sha512-6FwPqEpCfKIh3nSSGeo3uBm2iFSnFJDfwL3oS9pyegRBXNsGRVpgiW63yhNzL0796StsvjHWwQnQHsZNxWAkGw== - dependencies: - crypto-browserify "3.12.0" - eth-lib "0.2.8" - ethereumjs-common "^1.3.2" - ethereumjs-tx "^2.1.1" - scrypt-js "^3.0.1" - underscore "1.9.1" - uuid "3.3.2" - web3-core "1.2.11" - web3-core-helpers "1.2.11" - web3-core-method "1.2.11" - web3-utils "1.2.11" - -web3-eth-contract@1.2.11: - version "1.2.11" - resolved "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.2.11.tgz" - integrity sha512-MzYuI/Rq2o6gn7vCGcnQgco63isPNK5lMAan2E51AJLknjSLnOxwNY3gM8BcKoy4Z+v5Dv00a03Xuk78JowFow== - dependencies: - "@types/bn.js" "^4.11.5" - underscore "1.9.1" - web3-core "1.2.11" - web3-core-helpers "1.2.11" - web3-core-method "1.2.11" - web3-core-promievent "1.2.11" - web3-core-subscriptions "1.2.11" - web3-eth-abi "1.2.11" - web3-utils "1.2.11" - -web3-eth-ens@1.2.11: - version "1.2.11" - resolved "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.2.11.tgz" - integrity sha512-dbW7dXP6HqT1EAPvnniZVnmw6TmQEKF6/1KgAxbo8iBBYrVTMDGFQUUnZ+C4VETGrwwaqtX4L9d/FrQhZ6SUiA== - dependencies: - content-hash "^2.5.2" - eth-ens-namehash "2.0.8" - underscore "1.9.1" - web3-core "1.2.11" - web3-core-helpers "1.2.11" - web3-core-promievent "1.2.11" - web3-eth-abi "1.2.11" - web3-eth-contract "1.2.11" - web3-utils "1.2.11" - -web3-eth-iban@1.2.11: - version "1.2.11" - resolved "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.2.11.tgz" - integrity sha512-ozuVlZ5jwFC2hJY4+fH9pIcuH1xP0HEFhtWsR69u9uDIANHLPQQtWYmdj7xQ3p2YT4bQLq/axKhZi7EZVetmxQ== - dependencies: - bn.js "^4.11.9" - web3-utils "1.2.11" - -web3-eth-personal@1.2.11: - version "1.2.11" - resolved "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.2.11.tgz" - integrity sha512-42IzUtKq9iHZ8K9VN0vAI50iSU9tOA1V7XU2BhF/tb7We2iKBVdkley2fg26TxlOcKNEHm7o6HRtiiFsVK4Ifw== - dependencies: - "@types/node" "^12.12.6" - web3-core "1.2.11" - web3-core-helpers "1.2.11" - web3-core-method "1.2.11" - web3-net "1.2.11" - web3-utils "1.2.11" - -web3-eth@1.2.11: - version "1.2.11" - resolved "https://registry.npmjs.org/web3-eth/-/web3-eth-1.2.11.tgz" - integrity sha512-REvxW1wJ58AgHPcXPJOL49d1K/dPmuw4LjPLBPStOVkQjzDTVmJEIsiLwn2YeuNDd4pfakBwT8L3bz1G1/wVsQ== - dependencies: - underscore "1.9.1" - web3-core "1.2.11" - web3-core-helpers "1.2.11" - web3-core-method "1.2.11" - web3-core-subscriptions "1.2.11" - web3-eth-abi "1.2.11" - web3-eth-accounts "1.2.11" - web3-eth-contract "1.2.11" - web3-eth-ens "1.2.11" - web3-eth-iban "1.2.11" - web3-eth-personal "1.2.11" - web3-net "1.2.11" - web3-utils "1.2.11" - -web3-net@1.2.11: - version "1.2.11" - resolved "https://registry.npmjs.org/web3-net/-/web3-net-1.2.11.tgz" - integrity sha512-sjrSDj0pTfZouR5BSTItCuZ5K/oZPVdVciPQ6981PPPIwJJkCMeVjD7I4zO3qDPCnBjBSbWvVnLdwqUBPtHxyg== - dependencies: - web3-core "1.2.11" - web3-core-method "1.2.11" - web3-utils "1.2.11" - -web3-provider-engine@14.2.1: - version "14.2.1" - resolved "https://registry.npmjs.org/web3-provider-engine/-/web3-provider-engine-14.2.1.tgz" - integrity sha512-iSv31h2qXkr9vrL6UZDm4leZMc32SjWJFGOp/D92JXfcEboCqraZyuExDkpxKw8ziTufXieNM7LSXNHzszYdJw== - dependencies: - async "^2.5.0" - backoff "^2.5.0" - clone "^2.0.0" - cross-fetch "^2.1.0" - eth-block-tracker "^3.0.0" - eth-json-rpc-infura "^3.1.0" - eth-sig-util "^1.4.2" - ethereumjs-block "^1.2.2" - ethereumjs-tx "^1.2.0" - ethereumjs-util "^5.1.5" - ethereumjs-vm "^2.3.4" - json-rpc-error "^2.0.0" - json-stable-stringify "^1.0.1" - promise-to-callback "^1.0.0" - readable-stream "^2.2.9" - request "^2.85.0" - semaphore "^1.0.3" - ws "^5.1.1" - xhr "^2.2.0" - xtend "^4.0.1" - -web3-providers-http@1.2.11: - version "1.2.11" - resolved "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.2.11.tgz" - integrity sha512-psh4hYGb1+ijWywfwpB2cvvOIMISlR44F/rJtYkRmQ5jMvG4FOCPlQJPiHQZo+2cc3HbktvvSJzIhkWQJdmvrA== - dependencies: - web3-core-helpers "1.2.11" - xhr2-cookies "1.1.0" - -web3-providers-ipc@1.2.11: - version "1.2.11" - resolved "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.2.11.tgz" - integrity sha512-yhc7Y/k8hBV/KlELxynWjJDzmgDEDjIjBzXK+e0rHBsYEhdCNdIH5Psa456c+l0qTEU2YzycF8VAjYpWfPnBpQ== - dependencies: - oboe "2.1.4" - underscore "1.9.1" - web3-core-helpers "1.2.11" - -web3-providers-ws@1.2.11: - version "1.2.11" - resolved "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.2.11.tgz" - integrity sha512-ZxnjIY1Er8Ty+cE4migzr43zA/+72AF1myzsLaU5eVgdsfV7Jqx7Dix1hbevNZDKFlSoEyq/3j/jYalh3So1Zg== - dependencies: - eventemitter3 "4.0.4" - underscore "1.9.1" - web3-core-helpers "1.2.11" - websocket "^1.0.31" - -web3-shh@1.2.11: - version "1.2.11" - resolved "https://registry.npmjs.org/web3-shh/-/web3-shh-1.2.11.tgz" - integrity sha512-B3OrO3oG1L+bv3E1sTwCx66injW1A8hhwpknDUbV+sw3fehFazA06z9SGXUefuFI1kVs4q2vRi0n4oCcI4dZDg== - dependencies: - web3-core "1.2.11" - web3-core-method "1.2.11" - web3-core-subscriptions "1.2.11" - web3-net "1.2.11" - -web3-utils@1.2.11: - version "1.2.11" - resolved "https://registry.npmjs.org/web3-utils/-/web3-utils-1.2.11.tgz" - integrity sha512-3Tq09izhD+ThqHEaWYX4VOT7dNPdZiO+c/1QMA0s5X2lDFKK/xHJb7cyTRRVzN2LvlHbR7baS1tmQhSua51TcQ== - dependencies: - bn.js "^4.11.9" - eth-lib "0.2.8" - ethereum-bloom-filters "^1.0.6" - ethjs-unit "0.1.6" - number-to-bn "1.7.0" - randombytes "^2.1.0" - underscore "1.9.1" - utf8 "3.0.0" - -web3-utils@^1.0.0-beta.31, web3-utils@^1.3.4: - version "1.7.3" - resolved "https://registry.npmjs.org/web3-utils/-/web3-utils-1.7.3.tgz" - integrity sha512-g6nQgvb/bUpVUIxJE+ezVN+rYwYmlFyMvMIRSuqpi1dk6ApDD00YNArrk7sPcZnjvxOJ76813Xs2vIN2rgh4lg== - dependencies: - bn.js "^4.11.9" - 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" - -web3@1.2.11: - version "1.2.11" - resolved "https://registry.npmjs.org/web3/-/web3-1.2.11.tgz" - integrity sha512-mjQ8HeU41G6hgOYm1pmeH0mRAeNKJGnJEUzDMoerkpw7QUQT4exVREgF1MYPvL/z6vAshOXei25LE/t/Bxl8yQ== - dependencies: - web3-bzz "1.2.11" - web3-core "1.2.11" - web3-eth "1.2.11" - web3-eth-personal "1.2.11" - web3-net "1.2.11" - web3-shh "1.2.11" - web3-utils "1.2.11" - -webidl-conversions@^3.0.0: +v8-compile-cache-lib@^3.0.1: version "3.0.1" - resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" - integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= - -websocket@1.0.32: - version "1.0.32" - resolved "https://registry.npmjs.org/websocket/-/websocket-1.0.32.tgz" - integrity sha512-i4yhcllSP4wrpoPMU2N0TQ/q0O94LRG/eUQjEAamRltjQ1oT1PFFKOG4i877OlJgCG8rw6LrrowJp+TYCEWF7Q== - dependencies: - bufferutil "^4.0.1" - debug "^2.2.0" - es5-ext "^0.10.50" - typedarray-to-buffer "^3.1.5" - utf-8-validate "^5.0.2" - yaeti "^0.0.6" - -websocket@^1.0.31: - version "1.0.34" - resolved "https://registry.npmjs.org/websocket/-/websocket-1.0.34.tgz" - integrity sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ== - dependencies: - bufferutil "^4.0.1" - debug "^2.2.0" - es5-ext "^0.10.50" - typedarray-to-buffer "^3.1.5" - utf-8-validate "^5.0.2" - yaeti "^0.0.6" - -whatwg-fetch@^2.0.4: - version "2.0.4" - resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz" - integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng== - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz" - integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - -which-boxed-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz" - integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== - dependencies: - is-bigint "^1.0.1" - is-boolean-object "^1.1.0" - is-number-object "^1.0.4" - is-string "^1.0.5" - is-symbol "^1.0.3" - -which-module@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz" - integrity sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8= + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= +webidl-conversions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== -which@1.3.1, which@^1.2.9: - version "1.3.1" - resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== +whatwg-url@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" + integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== dependencies: - isexe "^2.0.0" + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" which@2.0.2, which@^2.0.1: version "2.0.2" - resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" -wide-align@1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== - dependencies: - string-width "^1.0.2 || 2" - -window-size@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/window-size/-/window-size-0.2.0.tgz" - integrity sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU= - -word-wrap@^1.2.3, word-wrap@~1.2.3: - version "1.2.3" - resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== - wordwrapjs@^4.0.0: version "4.0.1" - resolved "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz" + resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-4.0.1.tgz#d9790bccfb110a0fc7836b5ebce0937b37a8b98f" integrity sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA== dependencies: reduce-flatten "^2.0.0" @@ -9909,200 +2845,65 @@ wordwrapjs@^4.0.0: workerpool@6.2.0: version "6.2.0" - resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b" integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A== -wrap-ansi@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz" - integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - -wrap-ansi@^5.1.0: - version "5.1.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz" - integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== - dependencies: - ansi-styles "^3.2.0" - string-width "^3.0.0" - strip-ansi "^5.0.0" - -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + 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" +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + wrappy@1: version "1.0.2" - resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -write@1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/write/-/write-1.0.3.tgz" - integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== - dependencies: - mkdirp "^0.5.1" + 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.npmjs.org/ws/-/ws-7.4.6.tgz" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== -ws@^3.0.0: - version "3.3.3" - resolved "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz" - integrity sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA== - dependencies: - async-limiter "~1.0.0" - safe-buffer "~5.1.0" - ultron "~1.1.0" - -ws@^5.1.1: - version "5.2.3" - resolved "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz" - integrity sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA== - dependencies: - async-limiter "~1.0.0" - -ws@^7.4.6: - version "7.5.7" - resolved "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz" - integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A== - -xhr-request-promise@^0.1.2: - version "0.1.3" - resolved "https://registry.npmjs.org/xhr-request-promise/-/xhr-request-promise-0.1.3.tgz" - integrity sha512-YUBytBsuwgitWtdRzXDDkWAXzhdGB8bYm0sSzMPZT7Z2MBjMSTHFsyCT1yCRATY+XC69DUrQraRAEgcoCRaIPg== - dependencies: - xhr-request "^1.1.0" - -xhr-request@^1.0.1, xhr-request@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/xhr-request/-/xhr-request-1.1.0.tgz" - integrity sha512-Y7qzEaR3FDtL3fP30k9wO/e+FBnBByZeybKOhASsGP30NIkRAAkKD/sCnLvgEfAIEC1rcmK7YG8f4oEnIrrWzA== - dependencies: - buffer-to-arraybuffer "^0.0.5" - object-assign "^4.1.1" - query-string "^5.0.1" - simple-get "^2.7.0" - timed-out "^4.0.1" - url-set-query "^1.0.0" - xhr "^2.0.4" - -xhr2-cookies@1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/xhr2-cookies/-/xhr2-cookies-1.1.0.tgz" - integrity sha1-fXdEnQmZGX8VXLc7I99yUF7YnUg= - dependencies: - cookiejar "^2.1.1" - -xhr@^2.0.4, xhr@^2.2.0, xhr@^2.3.3: - version "2.6.0" - resolved "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz" - integrity sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA== - dependencies: - global "~4.4.0" - is-function "^1.0.1" - parse-headers "^2.0.0" - xtend "^4.0.0" - -xmlhttprequest@1.8.0: - version "1.8.0" - resolved "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz" - integrity sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw= - -xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.0, xtend@~4.0.1: - version "4.0.2" - resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -xtend@~2.1.1: - version "2.1.2" - resolved "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz" - integrity sha1-bv7MKk2tjmlixJAbM3znuoe10os= - dependencies: - object-keys "~0.4.0" - -y18n@^3.2.1: - version "3.2.2" - resolved "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz" - integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ== - -y18n@^4.0.0: - version "4.0.3" - resolved "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz" - integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== - y18n@^5.0.5: version "5.0.8" - resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== -yaeti@^0.0.6: - version "0.0.6" - resolved "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz" - integrity sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc= - -yallist@^3.0.0, yallist@^3.0.2, yallist@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - yallist@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== yaml@^1.10.2: version "1.10.2" - resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== -yargs-parser@13.1.2, yargs-parser@^13.1.2: - version "13.1.2" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz" - integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - yargs-parser@20.2.4: version "20.2.4" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== -yargs-parser@^2.4.1: - version "2.4.1" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-2.4.1.tgz" - integrity sha1-hVaN488VD/SfpRgl8DqMiA3cxcQ= - dependencies: - camelcase "^3.0.0" - lodash.assign "^4.0.6" - yargs-parser@^20.2.2: version "20.2.9" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs-unparser@1.6.0: - version "1.6.0" - resolved "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz" - integrity sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw== - dependencies: - flat "^4.1.0" - lodash "^4.17.15" - yargs "^13.3.0" - yargs-unparser@2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== dependencies: camelcase "^6.0.0" @@ -10110,25 +2911,9 @@ yargs-unparser@2.0.0: flat "^5.0.2" is-plain-obj "^2.1.0" -yargs@13.3.2, yargs@^13.3.0: - version "13.3.2" - resolved "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz" - integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== - dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.2" - yargs@16.2.0: version "16.2.0" - resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== dependencies: cliui "^7.0.2" @@ -10139,32 +2924,12 @@ yargs@16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^4.7.1: - version "4.8.1" - resolved "https://registry.npmjs.org/yargs/-/yargs-4.8.1.tgz" - integrity sha1-wMQpJMpKqmsObaFznfshZDn53cA= - dependencies: - cliui "^3.2.0" - decamelize "^1.1.1" - get-caller-file "^1.0.1" - lodash.assign "^4.0.3" - os-locale "^1.4.0" - read-pkg-up "^1.0.1" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^1.0.1" - which-module "^1.0.0" - window-size "^0.2.0" - y18n "^3.2.1" - yargs-parser "^2.4.1" - yn@3.1.1: version "3.1.1" - resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== yocto-queue@^0.1.0: version "0.1.0" - resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==